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环境就好了

CleanShot 2025-08-13 at 20.10.52@2x

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

CleanShot 2025-08-14 at 22.02.38@2x

CleanShot 2025-08-14 at 08.18.06@2x

docker搭建

推荐使用docker,因为基本不会出问题

1
2
3
4
git clone https://github.com/inspiringz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721

CleanShot 2025-08-14 at 19.52.08@2x

漏洞复现

利用条件

漏洞影响版本是 1.2.5 <= Apache Shiro <= 1.4.1

Apache Shiro Padding Oracle Attack 的漏洞利用必须满足如下前提条件:

  • 开启 rememberMe 功能;
  • rememberMe 值使用 AES-CBC 模式解密;
  • 能获取到正常 Cookie,即用户正常登录的 Cookie 值;
  • 密文可控;

漏洞复现

相较于550的利用就相对较为麻烦了,因为涉及到Padding Oracle Attack相关的处理以及绕过,并且关于密钥的碰撞时间也相对较长。流程为

  1. 登录网站(勾选Remember),并从Cookie中获取合法的RememberMe。
  2. 使用RememberMe cookie作为Padding Oracle Attack的前缀。
  3. 加密 ysoserial 的序列化 payload,以通过Padding Oracle Attack制作恶意RememberMe。
  4. 重放恶意RememberMe cookie,以执行反序列化攻击。

认证成功不会生成deleteMe的cookie字段,认证失败则会设置

CleanShot 2025-08-13 at 22.18.02@2x

使用Java反序列化工具 ysoserial 生成 Payload:

1
2
java -jar ysoserial-all.jar CommonsBeanutils1 "touch /tmp/success" > payload.bin
注意是否有写入权限,我在用本地环境时用这个payload执行失败就是因为写入权限有问题

利用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
2
3
python shiro_exp.py
Usage: shiro_exp.py <url> <somecookie value> <payload>
python shiro_exp.py http://localhost:8080/account Q/YOn1G64exr7g6SFC+au3ah4JaRPgPDdsYEC8q9C3k0TS+kHjd/qMAsQ6qKfwY6Jf9z5J/dgNSPLzhQK9ahkP5gAg9vN9J+nz4B7J/ZXY94joflm6fzjUlsYPerPCd/6lU4XyIlTCtDlDgoNffG3oQZnYCepezfX59d8IhcRpCnmyS2Iv1DblL/9Eqzqs5VrIBPN1ScnJCjbF0W1zKeIh/+VTJWnrkynSuq5KoVNQZs4Wx/tP4VEIDMrmNFDRoZWBP+CQaB7OjFo/bfBpbj87lMmvI7EqQFOU0EqOG8C1sampBci2A5KcT3qzklx7G97ArERch0edE+WU445yr9hD/tpLqjC2tqBuEwfBs3hr94EuvPvtR6GmL1xPnbwQvvN7S0B8AFCzWp9yBnrS2zwVi0MVAYG/K2Fs9Bvltr2s89Em+xT9OSpI3qBB19kWqrtE3I14O7cY05Ge9E5nYKBoAG/ci5V4w5AaFKN45GAqFjNVsHoZrzYr6frSXIxpsB payload.bin

CleanShot 2025-08-14 at 19.48.58@2x

CleanShot 2025-08-14 at 19.50.33@2x

CleanShot 2025-08-14 at 19.48.12@2x

也可以直接工具梭哈,工具就太多了

https://github.com/feihong-cs/ShiroExploit-Deprecated

漏洞分析

Padding Oracle Attack 构造加密数据分析

密码这一块我就真的不知者慎言了,可以去网上找找相关资料,还是蛮详细的

网上讲的文章大多数都是讲的如何使用 Padding Oracle Attack 来获取明文。但是这种场景在 Apache Shiro Padding Oracle Attack 这个漏洞场景中就不适用了。在这个场景中,我们需要构造恶意加密数据,进行解密后反序列化。

此处内容参考自 https://www.mi1k7ea.com/2020/10/14/%E6%B5%85%E6%9E%90Shiro-Padding-Oracle-Attack%EF%BC%88Shiro721%EF%BC%89

这里简单说下 Padding Oracle Attack 加密数据整体过程:

  1. 选择一个明文 P,用来生成你想要的密文C
  2. 使用适当的 Padding 将字符串填充为块大小的倍数,然后将其拆分为从 1 到 N 的块;
  3. 生成一个随机数据块(C[n] 表示最后一个密文块);
  4. 对于每一个明文块,从最后一块开始:
    1. 创建一个包括两块的密文C’,其是通过一个空块(00000…)与最近生成的密文块C[n+1](如果是第一轮则是随机块)组合成的;
    2. 这步容易理解,就是Padding Oracle的基本攻击原理:修改空块的最后一个字节直至Padding Oracle没有出现错误为止,然后继续将最后一个字节设置为2并修改最后第二个字节直至Padding Oracle没有出现错误为止,依次类推,继续计算出倒数第3、4…个直至最后一个数据为止;
    3. 在计算完整个块之后,将它与明文块 P[n] 进行XOR一起创建 C[n]
    4. 对后续的每个块重复上述过程(在新的密文块前添加一个空块,然后进行Padding Oracle爆破计算);

简单地说,每一个密文块解密为一个未知值,然后与前一个密文块进行XOR。通过仔细选择前一个块,我们可以控制下一个块解密来得到什么。即使下一个块解密为一堆无用数据,但仍然能被XOR化为我们控制的值,因此可以设置为任何我们想要的值。

代码分析

密钥生成

在 Shiro550 中,密钥是硬编码,就像下面这样

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

CleanShot 2025-08-14 at 21.48.51@2x

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

CleanShot 2025-08-15 at 10.50.54@2x

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

初始化了一个KeyGenerator对象并调用init()方法初始化其参数,跟进看看参数是怎么被赋值的

CleanShot 2025-08-15 at 11.00.43@2x

获取了一个随机数生成器SecureRandom,继续跟进init()

往下看,这里 mySpi 是 AESKeyGenerator,跟进 engineInit() 方法,进行了 AES 算法的初始化。

CleanShot 2025-08-15 at 11.06.20@2x

engineInit()方法作用如下:

验证密钥长度是否为8的倍数且在有效范围内(128/192/256位)
将传入的密钥长度(bit)转换为字节单位存储
调用另一个初始化方法设置随机数生成器
如果密钥长度不符合要求则抛出参数异常。

回到 org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey(),跟进 generateKey() 方法

CleanShot 2025-08-15 at 11.08.31@2x

com.sun.crypto.provider.AESKeyGenerator#engineGenerateKey() 方法也下个断点,不然不会停在这里。

跟进到最后可见这里已经生成了一串16字节的随机序列,并且返回一个 SecretKeySpec 对象,再使用getEncoded() 方法获取 key 密钥序列。

关于这段随机序列的形成过程,在与于java.security.SecureRandom#nextBytes(byte[])和java.util.Arrays#fill(byte[], byte),不过并不重要,只是一些简单的形成

CleanShot 2025-08-15 at 11.20.02@2x

至此就是 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()

CleanShot 2025-08-15 at 13.08.02@2x

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

CleanShot 2025-08-15 at 13.13.39@2x

CleanShot 2025-08-15 at 13.15.15@2x

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

CleanShot 2025-08-15 at 13.19.33@2x

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

CleanShot 2025-08-15 at 14.25.16@2x

CleanShot 2025-08-15 at 14.31.05@2x

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

CleanShot 2025-08-15 at 14.35.08@2x

CleanShot 2025-08-15 at 14.35.28@2x

倘若Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe

0x02 Padding正确,反序列化错误处理

Shiro中关于反序列化的处理在 org.apache.shiro.io.DefaultSerializer#deserialize() 方法下

CleanShot 2025-08-15 at 14.50.12@2x

如果反序列化的结果错误,则会抛出异常。最后异常仍会被getRememberedPrincipals()方法处理。

但是对于Java来说,反序列化是以Stream的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化,那么自然而然去构造一些恶意命令是允许的,也就是最终形成漏洞的原因

于是这里就构造出了布尔条件

  • Padding 正确,服务器正常响应
  • Padding 错误,服务器返回 Set-Cookie: rememberMe=deleteMe

总结

721主要的解密过程没有变,只要你成功padding进去了,就能走到反序列化那一步,个人感觉不用理解那么透彻,本身这个漏洞也挺鸡肋,需要登陆成功的身份认证cookie才能攻击。

根据验证成功的cookie来Padding,并在后方构造不会影响原本的序列化,形成漏洞

参考

Java反序列化Shiro篇02-Shiro721流程分析

Java反序列化漏洞——Shiro721

Shiro-721—漏洞分析(CVE-2019-12422)


Java反序列化Shiro篇02-Shiro721流程分析
http://example.com/2025/08/24/Java反序列化Shiro篇02-Shiro721流程分析/
作者
Winter
发布于
2025年8月24日
许可协议