shiro介绍
Apache Shiro是企业常见的JAVA安全框架,执行身份验证、授权、密码和会话管理。只要rememberMe的AES加密密钥泄露,无论shiro是什么版本都会导致反序列化漏洞。
shiro漏洞原理
Apache Shiro框架提供了记住我的功能(RemeberMe),用户登录成功后会生成经过加密并编码的cookie。cookie的key为RemeberMe,cookie的值是经过对相关信息进行序列化,然后使用aes加密,最后在使用base64编码处理形成的。
在服务端接收cookie值时,按以下步骤解析:
1.检索RemeberMe cookie的值
2.Base 64解码
3.使用ACE解密(加密密钥硬编码)
4.进行反序列化操作(未作过滤处理
在调用反序列化的时候未进行任何过滤,导致可以触发远程代码执行漏洞。
处理流程如下:
简单的环境搭建
shiro1.2.4的war包链接:
https://pan.baidu.com/s/1i-XEEZsoBgzMzSwclAh2gA 提取码:6mzq
*左右滑动查看更多
将war包放到tomcat的webapps下,双击bin目录下的startup.bat,然后访问http://localhost:端口/shiro/login.jsp即可。
使用root/secret登录,抓包,服务端会返回一串加密的字符。
源码分析
首先关注CookieRememberMeManager,是一个用于处理cookie的manager:
orgapacheshirowebmgtCookieRememberMeManager.java
*左右滑动查看更多
这个manager管理器会在默认的管理器DefaultWebSecurityManager里面对其进行实例化。
在CookieRememberMeManager里面,有关于rememberMe的序列化和反序列的函数:
对反序列化的过程进行分析,进入getRememberedSerializedIdentity。
服务器获取request和response里的cookie,先进行base64解码,但看文章一开始的流程图,是还有一个aes解密的,但到这里,已经返回null了,说明aes解密在另外一个函数里解决了,另外一个函数里肯定包含
getRememberedSerializedIdentity。
查找一下哪里调用了
getRememberedSerializedIdentity:
跳转到源码,发现下面是调用了
convertBytesToPrincipals:
这里插播一下,因为对SubjectContext对象的内容是什么比较感兴趣,于是去找调用,里面存放的是token和info信息:
继续找createSubject的调用,是在login方法里面:
继续找login的调用:
会发现其实到最后传入的参数就是request和response了,即客户端发起的请求,而AuthenticationToken其实是
UsernamePasswordToken。
插播结束,继续之前的内容。跟进去,可以看到这个函数做了两步操作,一个解密一个反序列化,解密应该就是aes解密,就不看了。
看反序列化函数。
接着跟进,看到是一个实现泛型的序列化接口:
点击绿色的标签:
看到deserialize的两个实现:
点击默认的序列化类,调用的是原生的反序列化接口,可以看到里面有readObject方法。
接下来只需要找到aes密钥那我们就可以自己构造参数了,回到刚刚的convertBytesToPrincipals函数跟进到decrypt。
可以看到先将getCipherService实例化为cipherService再去调decrypt进行解密。
跟进getDecryptionCipherKey方法,直接返回一个常量。
找到写它的地方:
跟进去可以看到是用来设置密钥的函数:
接着找哪里调用了它:
可以看到是用来设置密钥的函数。
查看哪里调用,可以看到
AbstractRememberMeManager函数里的setCipherKey是一个常量,跟进去:
然后看到密钥是一段固定值:
那么现在有了密钥我们就可以自己构造数据包,就是序列化aes加密base64加密,然后把它放到正常的执行流程里就可以利用了。
再看一下序列化的过程,就是上面的逆过程。
我们登录成功的时候,服务器生成cookie然后返回给客户端,这个就是序列化的过程:
rememberSerializedIdentity是将已经加密的aes数据进行base64编码,然后返回到前端,找一下调用的地方:
convertPrincipalsToBytes应该就是序列化和aes加密的函数,跟进
convertPrincipalsToBytes:
可以看到是先将认证信息进行序列化,然后aes加密:
serialize函数一直跟下去,就是原生的序列化方法:
返回convertPrincipalsToBytes,进入encrypt方法:
同样也是先实例化CipherService对象,然后调用encrypt方法进行aes加密:
getEncryptionCipherKey是获取key,后面的就和上面一样了,不做重复。
我们返回rememberSerializedIdentity方法,去查看它的调用:
发现最后是到DefaultSecurityManager的login方法,简单看一下,显示进行认证,如果最后成功登录,则跳到rememberMeSuccessfulLogin:
然后继续跟进,看到的是会先进行一个清楚的操作,然后生成一个新的认证。
至于具体的内容,后面就不深入,因为最后就会到rememberSerializedIdentity方法,有兴趣可以自己深入一下。
经过上面的调试,可以看出反序列化的过程中,先对SubjectContext的内容进行base64解密操作,然后先是aes解密,解密根据一个key进行解密,再调用原生的反序列化函数进行反序列化,这个过程没有进行过滤,且key不是随机的而且是我们能够得到的,那就意味着在已知key的情况下就可以复原这个过程。
思路:
我们构造恶意的poc,对其进行序列化然后aes加密(key已知)再base64加密,然后放入cookie中向服务器发起请求,这时服务器就会进行反序列化的操作,先base64解密、aes解密,最后就是反序列化操作,反序列化的过程并没有过滤,那么就能够进行一些恶意的操作。
修复方法
去掉或者替换默认的秘钥。
1、去掉默认秘钥
2、替换默认秘钥
这里可以自定义一个,或者使用下边自动生成的方式。
新建类GenerateCipherKey,添加静态方法getCipherKey()。
在初始化AbstractRememberMeManager的时候调用。
原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| shiro反序列化漏洞源码分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论