一个公开密钥放倒一片:Apache Shiro RememberMe 反序列化漏洞

Apache Shiro 的【记住我】功能会把身份信息序列化后 AES 加密存进 Cookie。问题是早期版本把加密密钥硬编码在了源码里,而且密钥是公开的------攻击者用这个密钥加密任意反序列化数据塞进 Cookie,服务端解密后直接反序列化,命令就执行了。这篇讲透 Shiro 反序列化的原理、指纹识别、密钥问题和利用思路。

一、先说结论

Shiro 反序列化漏洞(常称 Shiro-550)的根因有三个:

  1. 早期版本 AES 密钥硬编码在源码中,且开源公开
  2. 加密只保证机密性,没有完整性校验,无法识别 Cookie 被篡改
  3. 服务端解密后直接做 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 密钥爆破原理

对字典中每个密钥:

  1. 用它加密一个无害的简单序列化对象

  2. Base64 后作为 rememberMe Cookie 发送

  3. 观察响应:

    • 没有返回 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 包裹

八、修复建议

  1. 更换强随机密钥:不用任何教程示例密钥,每个部署实例生成独立 16 字节随机密钥。
  2. 升级到最新版本:新版本改用 GCM 认证加密模式,能识别篡改,也消除了 CBC 的 Padding Oracle 风险。
  3. 反序列化限制:对反序列化做类白名单。
  4. 监控异常请求:识别大量 rememberMe 爆破、超长 Cookie。
  5. 最小权限 + 限制出网:降低被利用后的影响。

GCM 模式同时提供机密性和完整性,篡改密文会在认证阶段被拒绝,比单纯 CBC 更安全。

九、写在最后

Shiro 这个漏洞的教训是:加密不等于安全认证。把对称密钥硬编码并公开,加密就形同虚设;解密后不加校验地反序列化,更是把最后一道门敞开。很多漏洞不是加密算法不够强,而是密钥管理和输入校验出了根本性问题。

复现时建议先用 Docker 靶场手工验证 deleteMe 指纹,再用默认密钥走通无害命令,理解每一步后再推进。

如果你想看 Shiro 从密钥验证到 GetShell 的完整过程(包括 Gadget 链字节码构造、不同链的踩坑、反弹 Shell 处理、Padding Oracle 的实际构造),以及更多真实漏洞的全链路拆解,可以访问我的安全学习站点:详见文章头部官网:【光跃Eason·安研社】。

本文仅用于合法的安全研究和教育目的,请确保测试系统拥有合法授权,禁止对未授权系统进行测试。

相关推荐
ym hyd 1119 小时前
试题库管理系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
AC赳赳老秦10 小时前
采集行为合规自检:OpenClaw 自动校验 robots 协议与采集频率,规避违规采集风险
java·开发语言·c++·python·php·deepseek·openclaw
钱栈up10 小时前
mvn compile 卡死在 javac 阶段:一次类型不匹配的排查与修复
java·开发语言
剑胆凌锋10 小时前
等级保护测评工程师的困境
网络·安全·web安全·网络安全·职场和发展
吠品11 小时前
CSS图片模糊过渡:一个transition加filter就够
java·数据库·notepad++
步行cgn12 小时前
Spring 全注解开发详解
java·后端·spring
长河12 小时前
Java的GC(垃圾回收)中,标记无用对象的核心方法是可达性分析算法
java·jvm·算法
code2cat12 小时前
Java进阶篇之LongAdder:把统计更新分散,再读取总和
java·并发编程·原子类·longadder
凯哥Java13 小时前
写代码怎么避免逻辑漏洞?
java·开发语言·人工智能·自动化