Apache Shiro 的【记住我】功能会把身份信息序列化后 AES 加密存进 Cookie。问题是早期版本把加密密钥硬编码在了源码里,而且密钥是公开的------攻击者用这个密钥加密任意反序列化数据塞进 Cookie,服务端解密后直接反序列化,命令就执行了。这篇讲透 Shiro 反序列化的原理、指纹识别、密钥问题和利用思路。
一、先说结论
Shiro 反序列化漏洞(常称 Shiro-550)的根因有三个:
- 早期版本 AES 密钥硬编码在源码中,且开源公开
- 加密只保证机密性,没有完整性校验,无法识别 Cookie 被篡改
- 服务端解密后直接做 Java 反序列化,没有类白名单
攻击者知道密钥后,构造一个能触发命令执行的 Gadget 链对象,序列化成字节,用密钥 AES 加密、Base64 编码,塞进 rememberMe Cookie。服务端读取后解密、反序列化,Gadget 链触发,命令执行。
这篇从前置知识讲到指纹识别、密钥爆破、Gadget 链选择和修复。
二、前置知识
2.1 Shiro 和 RememberMe
Apache Shiro 是 Java 安全框架,提供认证、授权、加密、会话管理,因为轻量简单被大量 Web 项目使用。
它的【记住我】功能让用户勾选后,关闭浏览器再访问仍保持登录。服务端处理流程:
登录成功后:身份信息 → Java序列化 → AES加密 → Base64编码 → 写入 rememberMe Cookie
下次访问:读取 Cookie → Base64解码 → AES解密 → Java反序列化 → 恢复登录态
注意最后一步:解密后直接反序列化,没有任何完整性或来源校验。
2.2 Java 序列化
Java 序列化把对象转成字节,反序列化还原。序列化数据有固定特征:
- 十六进制以
AC ED 00 05开头 - Base64 编码后以
rO0ABX开头
反序列化时类的 readObject 等方法会自动调用,Gadget 链就是组合这些方法,最终走到命令执行。
2.3 AES-CBC
Shiro 用 AES-128-CBC 加密,密钥 16 字节,填充 PKCS5Padding。CBC 模式加解密用同一个密钥------只要密钥可知,攻击者就能加密任意数据。这正是 Shiro-550 的突破口。
三、漏洞原理
3.1 硬编码密钥
Shiro 1.2.4 及之前版本,类 AbstractRememberMeManager 中:
java
public static final byte[] DEFAULT_CIPHER_KEY_BYTES =
Base64.decode("kPH+bIxk****"); // 完整密钥在开源代码中可见,此处掩码
这个默认密钥在 Shiro 开源代码中公开可查,出于安全考虑此处不完整列出。如果开发者没有显式更换密钥(多数人不会),所有未改配置的 Shiro 实例都共用这把公开的钥匙。
3.2 完整攻击链
攻击者把服务端正常流程逆向操作:
构造恶意 Gadget 对象
→ Java 序列化得到字节
→ 用默认密钥 AES-CBC 加密
→ Base64 编码
→ 填入 rememberMe Cookie 发送
→ 服务端 Base64 解码、AES 解密、反序列化
→ Gadget 链触发,命令执行
全程没有签名、没有来源校验、没有类白名单。
3.3 影响版本
| 版本情况 | 说明 |
|---|---|
| Shiro <= 1.2.4 | 硬编码默认密钥,直接可利用 |
| 1.2.5 之后 | 默认随机密钥,但配置了常见弱密钥仍可爆破 |
| 升级到最新版 | 修复已知问题,建议升级 |
四、指纹识别
利用前先判断目标是否使用 Shiro。发送一个带任意 rememberMe Cookie 的请求:
http
GET / HTTP/1.1
Host: 目标
Cookie: rememberMe=test
如果使用 Shiro,解析失败时响应的 Set-Cookie 会返回:
http
Set-Cookie: rememberMe=deleteMe; ...
rememberMe=deleteMe 是 Shiro 的标志性响应,看到即可确认。
判断流程:
发送 Cookie: rememberMe=test
→ 响应有 rememberMe=deleteMe?
→ 有:确认 Shiro,进入密钥测试
→ 无:可能未使用,或换路径再测
有时 deleteMe 在重定向响应里才出现,可以多观察几个请求。
五、密钥问题
5.1 随机密钥为什么也可能被攻破
1.2.5 之后默认随机生成密钥,但现实中存在大量常见密钥:开发者从网上教程复制配置、历史项目泄露、旧默认密钥仍在用。社区整理了常见 Shiro 密钥字典,包含上百个。
5.2 密钥爆破原理
对字典中每个密钥:
-
用它加密一个无害的简单序列化对象
-
Base64 后作为 rememberMe Cookie 发送
-
观察响应:
- 没有返回 deleteMe:解密成功,密钥正确
- 返回 deleteMe:解密失败,继续下一个
加载密钥字典
→ 取密钥加密无害对象 → 发送
→ 响应有 deleteMe?
→ 有:换下一个密钥
→ 无:找到正确密钥
几个常见密钥示例(出于安全考虑密钥已掩码,实际测试需使用完整字典):
| 密钥(Base64,已掩码) | 来源 |
|---|---|
kPH+bIxk**** |
官方默认 |
2AvVhd**** |
教程常见 |
3AvVhm**** |
教程常见 |
fCq+/xW**** |
泄露密钥 |
5.3 爆破局限
目标若用了强随机密钥且不在字典中,Shiro-550 路径走不通。此时在满足条件时可考虑针对 CBC 模式的 Padding Oracle 攻击(Shiro-721),它不需要知道密钥,但需要一个合法登录的 rememberMe Cookie,且要发送大量请求逐字节构造密文。密钥爆破本身请求较多,注意控制速率。
六、Gadget 链选择
6.1 什么是 Gadget 链
光有加密能力不够,反序列化字节必须是能触发命令执行的对象链。Gadget 链由 classpath 上已有库组合而成,利用反序列化时自动调用的方法走到命令执行。
6.2 常用链对比
| Gadget 链 | 依赖库 | 适用情况 |
|---|---|---|
| Commons-Beanutils | commons-beanutils | Shiro 自带,最常用 |
| Commons-Collections | commons-collections 3.x | 老牌链,很多项目已升级 |
| Groovy | groovy | Groovy 项目 |
| Spring | spring 相关 | Spring 项目 |
| ROME | rome | 使用 ROME 的项目 |
6.3 为什么常用 Beanutils 链
Shiro 本身依赖 commons-beanutils,目标 classpath 上几乎必然存在,不依赖业务代码引入其他库,成功率最高。
但有个坑:Shiro 自带的 commons-beanutils 可能存在版本或包名差异,标准工具生成的链不一定兼容。解决办法是使用针对 Shiro 调整过的 Beanutils 链(noCC 版本,不依赖目标再装 commons-collections)。
Beanutils 链的触发过程大致是:
反序列化 PriorityQueue + BeanComparator
→ 触发 compare()
→ 调用 PropertyUtils.getProperty()
→ 触发 TemplatesImpl 加载恶意字节码
→ 静态代码块执行命令
命令执行逻辑藏在 TemplatesImpl 加载的字节码里,反序列化链只是触发器。
七、利用思路
7.1 完整流程
指纹识别确认 Shiro
→ 尝试默认密钥 / 字典爆破
→ 密钥可知:选 Gadget 链生成 Payload
→ 密钥不可知:有合法账号再考虑 Padding Oracle
→ 加密 + Base64 + 填入 Cookie
→ 先验证命令执行,再做后续
7.2 先验证命令执行
不要一上来就做完整利用,先用无害方式验证:
- 执行 sleep,通过响应延迟判断
- 写标记文件后查看
- 用带外回连确认
确认密钥和 Gadget 链都正确后,再推进到完整利用。
7.3 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 密钥对但不执行 | Gadget 依赖库不存在 | 换 Beanutils 链 |
| Beanutils 报错 | 版本/包名不兼容 | 用 Shiro 专用 noCC 版 |
| Cookie 过长被拒 | 序列化对象太大 | 精简 Payload |
| 命令执行但无进一步效果 | 命令未经 shell 解析 | bash -c 包裹 |
八、修复建议
- 更换强随机密钥:不用任何教程示例密钥,每个部署实例生成独立 16 字节随机密钥。
- 升级到最新版本:新版本改用 GCM 认证加密模式,能识别篡改,也消除了 CBC 的 Padding Oracle 风险。
- 反序列化限制:对反序列化做类白名单。
- 监控异常请求:识别大量 rememberMe 爆破、超长 Cookie。
- 最小权限 + 限制出网:降低被利用后的影响。
GCM 模式同时提供机密性和完整性,篡改密文会在认证阶段被拒绝,比单纯 CBC 更安全。
九、写在最后
Shiro 这个漏洞的教训是:加密不等于安全认证。把对称密钥硬编码并公开,加密就形同虚设;解密后不加校验地反序列化,更是把最后一道门敞开。很多漏洞不是加密算法不够强,而是密钥管理和输入校验出了根本性问题。
复现时建议先用 Docker 靶场手工验证 deleteMe 指纹,再用默认密钥走通无害命令,理解每一步后再推进。
如果你想看 Shiro 从密钥验证到 GetShell 的完整过程(包括 Gadget 链字节码构造、不同链的踩坑、反弹 Shell 处理、Padding Oracle 的实际构造),以及更多真实漏洞的全链路拆解,可以访问我的安全学习站点:详见文章头部官网:【光跃Eason·安研社】。
本文仅用于合法的安全研究和教育目的,请确保测试系统拥有合法授权,禁止对未授权系统进行测试。