Shiro反序列化漏洞笔记一(原理篇)
0x1 前言
Shiro-550反序列化漏洞大约在2016年就被披露了,但感觉直到近一两年,在各种攻防演练中这个漏洞才真正走进了大家的视野,Shiro-550反序列化应该可以算是这一两年最好用的RCE漏洞之一,原因有很多:Shiro框架使用广泛,漏洞影响范围广;攻击payload经过AES加密,很多安全防护设备无法识别/拦截攻击……
最初碰到Shiro反序列化漏洞应该是在2017或者2018年的一个CTF线下赛。一直到现在,这个漏洞不断有新的知识出现,因此打算开个系列笔记重新记录漏洞学习过程。
0x2 环境搭建
直接从github上clone代码到本地。
1 | git clone https://github.com/apache/shiro.git |
编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。
1 | <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 | public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { |
调用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 | protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) { |
进入convertPrincipalsToBytes,调用serialize对用户名进行处理。
1 | protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { |
跟进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 | protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { |
解密后得到的结果为序列化字符串的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进行测试,成功的概率还是蛮高的。