Shiro反序列化漏洞笔记一(原理篇)

0x1 前言

Shiro-550反序列化漏洞大约在2016年就被披露了,但感觉直到近一两年,在各种攻防演练中这个漏洞才真正走进了大家的视野,Shiro-550反序列化应该可以算是这一两年最好用的RCE漏洞之一,原因有很多:Shiro框架使用广泛,漏洞影响范围广;攻击payload经过AES加密,很多安全防护设备无法识别/拦截攻击……
最初碰到Shiro反序列化漏洞应该是在2017或者2018年的一个CTF线下赛。一直到现在,这个漏洞不断有新的知识出现,因此打算开个系列笔记重新记录漏洞学习过程。

0x2 环境搭建

直接从github上clone代码到本地。

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

在IDEA中导入mvn项目,并配置tomcat环境。

0x3 漏洞分析

根据漏洞描述,Shiro≤1.2.4版本默认使用CookieRememberMeManager,当获取用户请求时,大致的关键处理过程如下:
· 获取Cookie中rememberMe的值
· 对rememberMe进行Base64解码
· 使用AES进行解密
· 对解密的值进行反序列化

由于AES加密的Key是硬编码的默认Key,因此攻击者可通过使用默认的Key对恶意构造的序列化数据进行加密,当CookieRememberMeManager对恶意的rememberMe进行以上过程处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。

加密过程

先分析一下加密的过程。
在org/apache/shiro/mgt/DefaultSecurityManager.java代码的rememberMeSuccessfulLogin方法下断点。

跟进onSuccessfulLogin方法,具体实现代码在AbstractRememberMeManager.java。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);

//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
} else {
if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. " +
"RememberMe functionality will not be executed for corresponding account.");
}
}
}

调用forgetIdentity方法对subject进行处理,subject对象表示单个用户的状态和安全操作,包含认证、授权等( http://shiro.apache.org/static/1.6.0/apidocs/org/apache/shiro/subject/Subject.html)。继续跟进forgetIdentity方法,getCookie方法获取请求的cookie,接着会进入到removeFrom方法。

removeForm主要在response头部添加Set-Cookie: rememberMe=deleteMe

然后再回到onSuccessfulLogin方法中,如果设置rememberMe则进入rememberIdentity。

rememberIdentity方法代码中,调用convertPrincipalsToBytes对用户名进行处理。

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

进入convertPrincipalsToBytes,调用serialize对用户名进行处理。

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

跟进serialize方法来到org/apache/shiro/io/DefaultSerializer.java,很明显这里对用户名进行了序列化。

再回到convertPrincipalsToBytes,接着对序列化的数据进行加密,跟进encrypt方法。加密算法为AES,模式为CBC,填充算法为PKCS5Padding。

getEncryptionCipherKey获取加密的密钥,在AbstractRememberMeManager.java定义了默认的加密密钥为kPH+bIxk5D2deZiIxcaaaA==。

加密完成后,继续回到rememberIdentity,跟进rememberSerializedIdentity方法。

对加密的bytes进行base64编码,保存在cookie中。至此,加密的流程基本就分析完了。

解密过程

对cookie中rememberMe的解密代码也是在AbstractRememberMeManager.java中实现。直接在getRememberedPrincipals下断点。

getRememberedSerializedIdentity返回cookie中rememberMe的base64解码后的bytes。

继续调用convertBytesToPrincipals方法对解码后的bytes处理,跟进convertBytesToPrincipals方法,调用decrypt方法对bytes进行解密。

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

解密后得到的结果为序列化字符串的bytes。

然后进入到deserialize方法进行反序列化,即用户可控的rememberMe值经过解密后进行反序列化从而引发反序列化漏洞。这里需要注意的是Shiro并不是使用原生的反序列化,而是重写了ObjectInputStream,这个重写也带来了反序列化漏洞利用过程中的一些坑,这个问题在这里先不深究,在后面的笔记再详细分析。

0x4 总结

从上面的分析可以知道,大体的漏洞触发流程为:构造恶意的序列化Cookie rememberMe的值——解密——反序列化。所以Shiro反序列化漏洞一个很关键的点就在于AES解密的密钥,攻击者需要知道密钥才能构造恶意的序列化数据。在Shiro≤1.2.4中默认密钥为kPH+bIxk5D2deZiIxcaaaA==。官方针对这个漏洞的修复方式是去掉了默认的Key,生成随机的Key。

但是并不是说Shiro>1.2.4版本就一定不存在反序列化漏洞,在平常的安全测试中发现一些应用即使使用高版本的Shiro也会存在问题,因为开发者通过自定义Key的方法又把默认的Key写回去了,或者一些开发者在写代码的时候习惯拷贝网上的一些代码,同时也拷贝了其他人自定义的Key,在进行漏洞测试时可以通过搜索网上常见的自定义的Shiro解密的Key进行测试,成功的概率还是蛮高的。