Java反序列化Shiro篇02-Shiro721流程分析
Java反序列化Shiro篇02-Shiro721流程分析
在 Shiro550 漏洞中,Cookie 所使用的 AES 加密密钥为硬编码,所以我们可以构造恶意序列化数据并使用固定的 AES 密钥进行正确加密恶意序列发送给服务端,进而达到攻击的目的。但在该漏洞公布后,Shiro 官方修复了这一漏洞,将AES密钥修改成了动态生成。也就是说,对于每一个 Cookie,都是使用不同的密钥进行加解密的。
Shiro-721漏洞的产生源自AES-128-CBC模式,它受CBC字节反转攻击和Padding Oracle Attack(侧信道攻击)的影响,导致可以从一个正常的rememberMe的值基础上,根据Padding Oracle Attack的原理,通过爆破构造出恶意的RememberMe,重新发送到服务器端进行解析并触发反序列化达到RCE的效果
个人感觉比较鸡肋,因为需要一个正确的账号,这个条件就比较苛刻了,如果你是一个普通账号登录来利用这个漏洞,可能连命令执行的权限都没有,部分情况是这样的,必须是有执行权限的账户例如root账户,具体要看开发怎么接入的Shiro,所以就比较鸡肋
环境搭建
手动搭建
可以直接用Drunkbaby佬的:https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/Apache/Shiro/shiro721
如果要自己搭建,也可以跟着Drunkbaby佬的教程搭建:Java反序列化Shiro篇02-Shiro721环境搭建
IDEA导入文件然后配一下Tomcat环境就好了

记得还需要把再把 WEB-INF\lib 加入 project structure 中


docker搭建
推荐使用docker,因为基本不会出问题
1 | |

漏洞复现
利用条件
漏洞影响版本是 1.2.5 <= Apache Shiro <= 1.4.1
Apache Shiro Padding Oracle Attack 的漏洞利用必须满足如下前提条件:
- 开启 rememberMe 功能;
- rememberMe 值使用 AES-CBC 模式解密;
- 能获取到正常 Cookie,即用户正常登录的 Cookie 值;
- 密文可控;
漏洞复现
相较于550的利用就相对较为麻烦了,因为涉及到Padding Oracle Attack相关的处理以及绕过,并且关于密钥的碰撞时间也相对较长。流程为
- 登录网站(勾选Remember),并从Cookie中获取合法的RememberMe。
- 使用RememberMe cookie作为Padding Oracle Attack的前缀。
- 加密 ysoserial 的序列化 payload,以通过Padding Oracle Attack制作恶意RememberMe。
- 重放恶意RememberMe cookie,以执行反序列化攻击。
认证成功不会生成deleteMe的cookie字段,认证失败则会设置

使用Java反序列化工具 ysoserial 生成 Payload:
1 | |
利用GitHub的exp来进行 Padding Oracle Attack,安装脚本不需要 pip install paddingoracle,直接将 GitHub 项目的 paddingoracle.py 放到同目录即可:
通过 Padding Oracle Attack 生成 Evil Rememberme cookie:
注意: 此 exp 爆破时间较长,建议使用 ysoserial 生成较短的 payload 验证(eg: ping 、 touch /tmp/success, etc),约 1 个多小时可生成正确的 rememberme cookie,生成成功后将自动停止运行。
1 | |



也可以直接工具梭哈,工具就太多了
https://github.com/feihong-cs/ShiroExploit-Deprecated
漏洞分析
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 中,密钥是硬编码,就像下面这样

而在Shiro721中,硬编码取消了而是改为动态生成,如图代码

根据上图,那么我们的目标就很明确了,找到generateNewKey这个关键方法的所在地方,定位到org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey(int)

这段代码的功能是生成一个新的加密密钥:
通过KeyGenerator.getInstance()获取指定算法的密钥生成器实例
如果算法不存在则抛出异常
初始化密钥生成器的密钥长度
生成并返回新的密钥
核心作用是根据指定的算法名称和密钥位数创建对应的加密密钥。
初始化了一个KeyGenerator对象并调用init()方法初始化其参数,跟进看看参数是怎么被赋值的

获取了一个随机数生成器SecureRandom,继续跟进init()
往下看,这里 mySpi 是 AESKeyGenerator,跟进 engineInit() 方法,进行了 AES 算法的初始化。

engineInit()方法作用如下:
验证密钥长度是否为8的倍数且在有效范围内(128/192/256位)
将传入的密钥长度(bit)转换为字节单位存储
调用另一个初始化方法设置随机数生成器
如果密钥长度不符合要求则抛出参数异常。
回到 org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey(),跟进 generateKey() 方法

在 com.sun.crypto.provider.AESKeyGenerator#engineGenerateKey() 方法也下个断点,不然不会停在这里。
跟进到最后可见这里已经生成了一串16字节的随机序列,并且返回一个 SecretKeySpec 对象,再使用getEncoded() 方法获取 key 密钥序列。
关于这段随机序列的形成过程,在与于java.security.SecureRandom#nextBytes(byte[])和java.util.Arrays#fill(byte[], byte),不过并不重要,只是一些简单的形成

至此就是 Shiro721 完整的密钥生成过程。
在 shiro721 中的Padding Oracle Attack
要成功进行 Padding Oracle Attack 是需要服务端返回两个不同的响应特征来进行 Bool 判断的。
在 Apache Shiro 的场景中,这个服务端的两个不同的响应特征为:
- Padding Oracle 错误时,服务端响应报文的 Set-Cookie 头字段返回
rememberMe=deleteMe; - Padding Oracle 正确时,服务端返回正常的响应报文内容;
我们可以通过响应头来判断明文填充是否正确,进而爆破出中间值。那么对于解密不正确的 Cookie,Shiro 是怎么处理的呢?
0x01 Padding Oracle错误处理
跟550一样去找解密函数,在 org.apache.shiro.mgt.AbstractRememberMeManager#decrypt() 中

跟进关键方法cipherService.decrypt(),最后到crypt()中调用doFinal()方法


看一下doFinal()方法实现的功能,注释已经写的很清楚了

doFinal()方法有IllegalBlockSizeException和BadPaddingException这两个异常,分别用于捕获块大小异常和填充错误异常。异常会被抛出到crypt()方法中,最终被getRememberedPrincipals()方法捕获,并执行onRememberedPrincipalFailure()方法。


onRememberedPrincipalFailure()方法调用了forgetIdentity()。在Shiro550中我们分析过,该方法会调用removeFrom(),在response头部添加字段Set-Cookie: rememberMe=deleteMe。


倘若Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe 。
0x02 Padding正确,反序列化错误处理
Shiro中关于反序列化的处理在 org.apache.shiro.io.DefaultSerializer#deserialize() 方法下

如果反序列化的结果错误,则会抛出异常。最后异常仍会被getRememberedPrincipals()方法处理。
但是对于Java来说,反序列化是以Stream的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化,那么自然而然去构造一些恶意命令是允许的,也就是最终形成漏洞的原因
于是这里就构造出了布尔条件
- Padding 正确,服务器正常响应
- Padding 错误,服务器返回
Set-Cookie: rememberMe=deleteMe
总结
721主要的解密过程没有变,只要你成功padding进去了,就能走到反序列化那一步,个人感觉不用理解那么透彻,本身这个漏洞也挺鸡肋,需要登陆成功的身份认证cookie才能攻击。
根据验证成功的cookie来Padding,并在后方构造不会影响原本的序列化,形成漏洞