漏洞介绍

Apache Shiro 是一个强大且易于使用的Java安全框架,提供了身份验证(Authentication)、授权(Authorization)、会话管理(Session Management)和密码学支持等功能。Apache Shiro 550反序列化漏洞(CVE-2016-4437)是一个严重的Java反序列化漏洞,它允许攻击者通过特制的Java序列化对象在目标系统上执行任意代码。该漏洞影响了使用默认“rememberMe”功能的Apache Shiro。

漏洞范围

Apache Shiro <= 1.2.4

漏洞靶场

使用vulhub的靶场

使用版本为Apache Shiro 1.2.4

vulhub-master/shiro/CVE-2016-4437

1
docker-compose up -d

漏洞原理

Apache Shiro 提供了一种“rememberMe”功能,用于在用户关闭浏览器后仍然保持会话,当启用该功能时,Shiro 会将用户的会话信息序列化并存储在一个 cookie 中,以便在用户重新访问时反序列化并恢复会话;在这边攻击者可以通过修改“rememberMe” cookie,注入特制的恶意 Java 序列化对象;当 Shiro 反序列化这个恶意对象时,攻击者可以在目标系统上执行任意代码。

Apache Shiro如何记住我?

1
2
3
4
序列化用户身份对象.(这里为SimplePrincipalCollection)
对序列化后的数据进行AES加密,密钥为常量.
base64编码
设置到cookie中,cookie name等于rememberMe.

Apache Shiro如何解析我?

1
2
3
4
读取cookie中的rememberMe的值.
对其值进行base64解码.
AES解密
反序列化

如何攻击?

1
2
3
通过以上,我们可以看出,只要手动构造一个反序列化 payload,进行AES加密(密钥我们知道的)和base64编码,即可在反序列化时执行任何操作.

整体流程就是:命令 => 序列化 =>AES加密 => base64编码 => RememberMe Cookie值 => base64解密 => AES解密 => 反序列化 => 执行命令
漏洞相关源代码片段

漏洞出现在 Shiro 的 RememberMeManager 组件中,特别是处理“rememberMe”cookie 的代码部分,以下是与漏洞相关的关键代码片段的简化示例,帮助我们理解漏洞的原理。

1
2
3
4
5
6
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//清理旧的身份验证信息c
forgetIdentity(subject);
//生成新的身份验证信息
if (isRememberMe(token)) { //如果有勾选remember me
rememberIdentity(subject, token, info);//生成新的cookie中的RememberMe字段

shiro-shiro-root-1.2.4\core\src\main\java\org\apache\shiro\mgt\AbstractRememberMeManager.java#L291-L297

在Shiro中,当用户成功登录时,可以通过onSuccessfulLogin方法来处理“记住我”功能的逻辑。这个方法会清理旧的身份验证信息,并根据需要生成新的身份验证信息。

forgetIdentity方法会删除存储在Cookie中的身份信息,以确保旧的身份信息不会被重新使用。isRememberMe(token):检查登录请求中是否包含“记住我”选项。如果用户在登录时选择了“记住我”,这个方法会返回truerememberIdentity(subject, token, info):如果用户选择了“记住我”,这个方法会生成并存储新的身份验证信息;接着我们再继续追踪rememberIdentity

1
2
3
4
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
rememberIdentity(subject, principals);
}

shiro-shiro-root-1.2.4\core\src\main\java\org\apache\shiro\mgt\AbstractRememberMeManager.java#L319-L322

getIdentityToRemember(subject, authcInfo)方法用于从authcInfo(身份验证信息)中提取用户的身份信息。接着调用rememberIdentity(subject, principals)方法,使用提取到的身份信息来设置“记住我”功能,这边继续跟进rememberIdentity:

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

shiro-shiro-root-1.2.4\core\src\main\java\org\apache\shiro\mgt\AbstractRememberMeManager.java#L345-L348

1.convertPrincipalsToBytes

这个函数中使用了convertPrincipalsToBytes(accountPrincipals)方法用于将PrincipalCollection(包含用户身份信息的对象)转换为字节数组;此时我们进入convertPrincipalsToBytes函数查看具体针对信息数据的操作是什么样的:

1
2
3
4
5
6
7
 protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

shiro-shiro-root-1.2.4\core\src\main\java\org\apache\shiro\mgt\AbstractRememberMeManager.java#L359-L365

可以看到再这个函数中首先使用了serialize(principals)方法将PrincipalCollection对象序列化为字节数组。接着进行一个获取密钥服务的操作,当CipherService不为空,则对序列化后的字节数组进行加密;接着我们进入encryp函数中查看具体的加密操作是如何进行的:

1
2
3
4
5
6
7
8
9
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) { //如果cipherService不为空
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}

shiro-shiro-root-1.2.4\core\src\main\java\org\apache\shiro\mgt\AbstractRememberMeManager.java#L469-L477

这里调用了getCipherService()方法获取配置的加密服务,并赋值给变量cipherService;进入getCipherService()方法后可以发现此处的加密为AES加密(除了指定加密的方式之外还指定了其他的一些加密细节)

img

获取到加密的信息后就是对序列化的数据进行加密了,在加密时,程序中使用了getEncryptionCipherKey()方法进行密钥获取:

img

此时可以看到默认的加密密钥实际上在shiro中是一串固定的base64解码后的字符串;其base64编码的值是“kPH+bIxk5D2deZiIxcaaaA==”

2.rememberSerializedIdentity

在清楚如何针对信息数据转化为字节数组后,接着再看一下rememberSerializedIdentity方法中具体做了哪些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized); //进行base64编码
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64); //将base64编码的信息整合到cookie当中
cookie.saveTo(request, response);
}

shiro-shiro-root-1.2.4\web\src\main\java\org\apache\shiro\web\mgt\CookieRememberMeManager.java#L135-L158

在该方法中使用了Base64.encodeToString(serialized);对序列化数据进行Base64编码;并将Base64编码后的数据整合到Cookie中;至此我们就可以知道RememberMe的具体生成步骤:

1
用户信息-->序列化-->AES加密-->Base64编码-->Cookie信息

那么对应的解密过程就是:

1
Cookie信息-->Base64解码-->AES解密-->反序列化-->用户信息

漏洞复现

访问http://127.0.0.1:8080/进入靶场页面

image-20250228205940821

默认的账号密码为admin:vulhub;这个时候我们可以先输入任意Username和passwrod进行登录并抓包(Remember me选项必须勾选);抓到数据包后将数据包发送至BP的重放模块Repeater,进行发包,查看相应包内容:

image-20250228210617652

发现相应包中包含有Set-Cookie: rememberMe=deleteMe字段就基本上可以断定这边使用了shiro框架

exp使用

https://github.com/SummerSec/ShiroAttack2

1
java -jar shiro_attack-4.7.0-SNAPSHOT-all.jar

image-20250228210939341

image-20250228211009210

命令执行成功

参考

https://blog.csdn.net/WolvenSec/article/details/140507395

https://www.cnblogs.com/EddieMurphy-blogs/p/18068118