java-shiro
(主要看参考文章2)
Shiro简介
Apache Shiro 是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能,Shiro框架直观、易用、同时也能提供健壮的安全性。
Apache Shiro反序列化漏洞分为两种:Shiro-550、Shiro-721
Shiro-550反序列化漏洞
漏洞原理
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。 那么,Payload产生的过程: 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值 在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。
影响版本
Apache Shiro < 1.2.4
Shiro反序列化的特征
返回包中会包含rememberMe=deleteMe字段
这种情况大多会发生在登录处,返回包里包含remeberMe=deleteMe字段,这个是在返回包中(Response)
如果返回的数据包中没有remeberMe=deleteMe字段的话,可以在数据包中的Cookie中添加remeberMe=deleteMe字段这样也会在返回包中有这个字段
环境搭建
这里用的P神给的环境 :https://github.com/phith0n/JavaThings/tree/master/shirodemo
- JDK 8u65
- Tomcat 9
- Shiro 1.2.4
- Commons Collection 3.2.1
先把p神的项目下载下来
然后用idea
打开shirodemo
接着就是配置tomacat
了
先去官网把他下载下来
然后进入idea打开设置
添加tomacat的路径
接着点击这个
先配置这个
最后就完成了
漏洞的利用
先抓个包看看
默认账号密码
- root
- secret
(重点是得勾选这个remember me
)
固定会返回这个rememberMe=deleteMe
并且这个cookie
很长(这就说明了这个cookie存着一些信息)
我们就去代码里找一下看这个cookie
是怎么生成的
加密过程
在登录成功后会对cookie进行编码加密 ,我们来跟一下这个加密流程
入口是在 AbstractRememberMeManager.onSuccessfulLogin
方法
判断 token 是否为 true,然后调用 rememberIdentity
:
看一下这个 getIdentityToRemember
:
大致就是获取用户名赋值给 principals
。
回到rememberIdentity
跟进this.rememberIdentity(subject, principals)
:
跟进 convertPrincipalsToBytes
看看:
先对用户名进行序列化处理,然后调用了个this.getCipherService()
方法是否有返回值,跟进查看:
返回了一种 AES 的加密方式CBC。
回到convertPrincipalsToBytes
方法,接着调用this.encrypt(bytes)
对序列化后的用户名进行加密操作,跟进:
这里同样是先用getCipherService
方法获取一个加密方式,如果不是空则用该加密方式调用encrypt
方法进行加密,AES加密是个对称加密需要密钥,所以这里用getEncryptionCipherKey
获取一个密钥,跟进看看:
看来是直接返回了这个密钥,由于我们知道这个漏洞就是因为密钥是硬编码写好的造成的,所以我们往回找找这个密钥是哪里赋值的。
找到这个AbstractRememberMeManager类初始化的时候会,调用setCipherKey
方法来设置密钥:
跟进setCipherKey
方法瞧一眼:
正如上面说的AES是对称加密,加密和解密的密钥是同一个,这里就是用传进来的密钥分别赋值给加密密钥和解密密钥,跟进setEncryptionCipherKey
:
这里就是直接赋值了(吐槽下,真套呀,不过还能看得懂,没套晕)
回到AbstractRememberMeManager类初始化的this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
这里,这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES实在类定义里面写好的:
就是说这个 encryptionCipherKey
是 kPH+bIxk5D2deZiIxcaaaA==
的解密,是一个常量 就是说让用户名的序列化和一个常量进入 cipherService.encrypt
进行加密:
具体加密就不看了,不懂密码学。
总之对学列化后的用户名进行AES加密之后会返回字节到rememberIdentity
方法:
进入下一步的rememberSerializedIdentity
方法:
刚才都还是在AbstractRememberMeManager类里面调用,这时候就来到了CookieRememberMeManager类里面,看类名大概能猜到是处理cookie的了。
这里逻辑就是对传进来的字节进行base64加密,然后设置为名字为rememberMe的cookie值。(根据这个函数名得知,这里是会对cookie进行序列化处理的)
解密分析
现在我们从getRememberedIdentity
开始分析,文件位置 org/apache/shiro/mgt/DefaultSecurityManager.java
跟进到getRememberedPrincipals
:
继续跟到getRememberedSerializedIdentity
:
这里的逻辑是先获取cookie中rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。
返回 getRememberedPrincipals
方法,下一步跟进 convertBytesToPrincipals
方法:
可以看到就进行了两个操作 decrypt
和 deserialize
。解密就是和加密的逆过程,不多说,进入 deserialize
:
继续跟进套娃的deserialize
:
发现readObject
方法出现了,下面就可以愉快地进行反序列化了!
加密解密跟解密都跟完了
(如果我们能根据这个固定密钥来伪造cookie的话,这样的话就可以进行恶意操作了)
AES密钥判断
前面说到 Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。
那么如何判断密钥是否正确呢?文章一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe
字段,而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe
字段。
因此我们需要构造payload排除类型转换错误,进而准确判断密钥。
shiro在1.4.2版本之前, AES的模式为CBC, IV是随机生成的,并且IV并没有真正使用起来,所以整个AES加解密过程的key就很重要了,正是因为AES使用Key泄漏导致反序列化的cookie可控,从而引发反序列化漏洞。在1.4.2版本后,shiro已经更换加密模式 AES-CBC为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
URLDNS链
通过漏洞原理可以知道,构造Payload需要将利用链通过AES加密后在base64编码。将Payload的值设置为rememberMe的cookie值,这里借助ysoserial中的URLDNS链去打,由于URLDNS不依赖于Commons Collections包,只需要JDK的包就行,所有一半用于检测是否存在漏洞。
python脚本
1 | # -*-* coding:utf-8 |
将得到的payload用Burp传入rememberMe的cookie值中:
当存在 JSESSIONID 时,会忽略 rememberMe,所以在攻击时需要将 JSESSIONID 删掉
成功
CC6+TemplatesImpl链
但是仅仅是URLDNS是不够的,我们想要的是执行恶意代码,所以先引入Commons Collections 3.2.1 包来进行利用构造。
(这里用常规的cc6
链子是打不通的,用了话会报错,导致无法执行)
(就是反序列化流中包含非Java自身的数组,则会出现无法加载类的错误)
我们就得去找cc中还有没有没用数组的来替换cc6中使用数组的部分
这里感兴趣为啥的话可以看看 参考文章2
这次用这张图片 觉得写的不错
我们不难发现实际上CC4和CC2是没有用到Transformer数组的,但CC4依赖的是Commons Collections4这个包,当前环境无法使用这条链,拿还有啥方法呢?
我们可以尝试去改造CC6这条链的后半部分,在CC6链中,我们用到了一个类, TiedMapEntry
,其构造函数接受两个参数,参数1是一个Map,参数2是一个对象key。TiedMapEntry 类有个 getValue
方法,调用了map的get方法,并传入key:
1 | public Object getValue() { |
关键点就是这个key
当这个map是LazyMap
时,其get方法就是触发transform
的关键点:
1 | public Object get(Object key) { |
我们以往构造CommonsCollections Gadget的时候,对 LazyMap#get
方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。
但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个 LazyMap#get
的参数key,会被传进transform()
,实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。
我们LazyMap.get(key)
直接调用InvokerTransfomer.transform(key)
,然后像CC2那样调用TempalteImpl.newTransformer()
来完成后续调用。
最终exp
1 | package org.example; |
然后将生成的ser.bin
用加密脚本给进行加密
1 | import org.apache.shiro.crypto.AesCipherService; |
然后再将生成的编码进行cookie
传参
Commons-Beanutils1链
上面的CC6+TemplatesImpl链是依赖于Commmons Collections软件包的,如果项目中没有用到的话就无法实现代码执行,那有没有只用Shiro自己的类就能实现代码执行的链呢?答案是有的。这里用到了Apache Commons Beanutils包。
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。关于JavaBean的说明可以参考这篇文章。
这里的话我也写了一篇文章来说分析这个cb链 java-Commons-BeanUtils
简单来说就是这个链子可以任意进行getter操作
如何利用这个PropertyUtils.getProperty()
方法去构造我们的利用链呢?回顾CC链中没有用到Commons Collections包的部分,再次搬出这张图
其中红框的部分就是没有用到Commons Collections包的部分,如此一来,CC3中的TemplatesImpl实现类加载任意代码执行是跑不掉的,所以我们要找找那里能调用TemplatesImpl.newTransformer()
方法,然后我们找到了TemplatesImpl.getOutputProperties()
:
1 | public synchronized Properties getOutputProperties() { |
它的内部调用了 newTransformer()
,而 getOutputProperties
这个名字,是以 get
开头,正符合getter的定义。
所以, PropertyUtils.getProperty( obj, property )
这段代码,当obj是一个 TemplatesImpl
对象,而 property
的值为 outputProperties
时,将会自动调用getter,也就是 TemplatesImpl.getOutputProperties()
方法,触发代码执行。
这就是一条cb链而已 但是这个cb链和上面给的链接的cb链子不是一回事
因为用的依赖不同 所以如果直接用上面的cb链来打的话会执行失败并且报错
所以这里得重新构造(但是区别不是很大)
1 | package org.example; |
这里和上面常规的cb链不同的是
将这里进行了修改 因为就是如果不修改的话会报错
就是会报这个错误
然后就解决了这个问题,就可以成功进行恶意代码执行了
Shiro中常见的三种错误 在复现shiro的过程中如果遇到问题可以来看看这里 基本都能得到解决
Shiro-721反序列化漏洞
漏洞原理
在Shiro721漏洞中,由于Apache Shiro cookie中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过Padding Oracle Attack来构造恶意的rememberMe字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行。
虽然使用Padding Oracle Attack可以绕过密钥直接构造攻击密文,但是在进行攻击之前我们需要获取一个合法用户的Cookie。
漏洞流程
- 登录网站获取合法Cookie
- 使用rememberMe字段进行Padding Oracle Attack,获取intermediary
- 利用intermediary构造出恶意的反序列化密文作为Cookie
- 使用新的Cookie请求网站执行攻击
影响版本
- Shiro <=1.4.1
环境搭建
将shrio-550
的的版本换掉就行
漏洞分析
密钥分析
跟进generateNewKey
()
在接着跟进generateNewKey
然后接着跟进init
在接着跟进init
获取完新的key之后,回到这里进行编码
加密方法还是AES
最后跟进这个setCipherKey
就是将新生成的key来作为加密和解密的key
至此就是Shiro721完整的密钥生成过程。
布尔条件
我们知道,Padding Oracle Attack攻击是一种类似于sql盲注的攻击,这就要求服务器端有能够被我们利用的布尔条件。在CBC字节翻转攻击&Padding Oracle Attack原理解析这篇文章中,我们模拟的服务器环境如下
- 当收到一个有效的密文(一个被正确填充并包含有效数据的密文)时,应用程序正常响应(200 OK)
- 当收到无效的密文时(解密时填充错误的密文),应用程序会抛出加密异常(500 内部服务器错误)
- 当收到一个有效密文(解密时正确填充的密文)但解密为无效值时,应用程序会显示自定义错误消息 (200 OK)
我们可以通过响应头来判断明文填充是否正确,进而爆破出中间值。那么对于解密不正确的Cookie,Shiro是怎么处理的呢?
(这里的话Padding Oracle Attack
就不详细分析了,只讲结论)
- Padding正确,服务器正常响应
- Padding错误,服务器返回
Set-Cookie: rememberMe=deleteMe
漏洞利用
在Shiro550中,我们可以直接通过硬编码密钥直接生成攻击密文。但是Shiro721使用了动态密钥,无法直接获取密钥。但是仍然可以通过Padding Oracle Attack绕过密钥,直接生成攻击密文。
利用链和Shiro550类似,这里我们使用ShiroExploit.V2.51工具进行攻击测试。输入测试网址以及登录用户的Cookie
然后就可以利用工具进行测试了
1 | & 'C:\Program Files\Java\jdk1.8.0_202\bin\java.exe' -jar .\ShiroExploit.jar |
这是我的启动方式 (因为环境变量是17,用不了)