Apache Shiro RememberMe 反序列化:从原理到告警研判证据链
前言:研判 Shiro 告警要回答的五个问题
Shiro 反序列化是 Java Web 安全中非常典型的告警类型。刚接触时容易把几个概念混在一起:
- 响应出现
rememberMe=deleteMe,是不是已经攻击成功? - Cookie 中出现很长的
rememberMe,是不是一定是恶意 Payload? - Shiro-550 和 Shiro-721 都是反序列化,到底有什么区别?
- 安全设备命中”Shiro 反序列化”后,应该看响应包,还是查服务器?
- 查到旧版本、默认密钥或 Gadget 特征,能不能直接定性为 RCE 成功?
核心结论先给出:
Shiro 框架指纹、受影响版本、恶意反序列化对象和命令执行结果,是四个不同层次的证据。只有把流量证据、应用证据和主机证据关联起来,才能判断风险是否真正落地。
一、RememberMe 为什么会和反序列化扯上关系
Apache Shiro 是 Java 生态的安全框架,提供认证、授权、会话管理和 RememberMe(记住我)能力。
传统 RememberMe 机制的写入流程:
1 | 登录成功并勾选"记住我" |
浏览器后续访问时流程反过来:Base64 解码 → AES 解密 → Java 反序列化 → 恢复身份对象。
危险点在于:**rememberMe 来自客户端,是外部可控数据**。只要攻击者能构造出服务端可正确解密的数据,且解密结果是恶意 Java 序列化对象、目标依赖里存在可利用的 Gadget Chain,就可能在反序列化过程中触发非预期代码执行。
一条完整利用链需要同时满足:
- 目标启用了 RememberMe 处理;
- 目标处于受影响版本或存在等价的不安全实现;
- 攻击者能生成服务端可接受的密文;
- 服务端类路径存在匹配的 Gadget Chain;
- 恶意对象确实进入反序列化流程;
- 最终操作未被 Java 版本、权限、容器、EDR 等阻断。
所以:发现 Shiro ≠ 存在漏洞;存在旧版本 ≠ 一定可利用;出现恶意 Cookie ≠ 命令已执行。
二、Shiro-550 vs Shiro-721
| 对比项 | Shiro-550 | Shiro-721 |
|---|---|---|
| 对应 CVE | CVE-2016-4437 | CVE-2019-12422 |
| NVD CVSS v3.1 | 9.8 Critical | 7.5 High |
| 影响范围 | Shiro ≤ 1.2.4(修复起 1.2.5) | Shiro ≤ 1.4.1(修复起 1.4.2) |
| 主要突破点 | 已知或泄露的密钥 | AES-CBC Padding Oracle |
| 是否必须知道密钥 | 通常需要 | 不一定 |
| 是否需要合法 Cookie | 不一定 | 通常需要 |
| 利用交互次数 | 相对较少 | 需要多次请求 |
一句话区分:
**Shiro-550 是”门锁钥匙被公开”**——旧版本存在公开固定的默认 RememberMe 密钥(
kPH+bIxk5D2deZiIxcaaaA==),攻击者知道密钥就能加密恶意序列化对象。**Shiro-721 是”不知道钥匙,但可以根据门锁对不同输入的反馈逐步推导有效密文”**——利用 CBC 模式和服务端响应差异做 Padding Oracle 攻击。
一个容易被忽略的点:升级到 1.2.5 只是解决了已知默认密钥。如果开发后来又配置了从公开示例复制的固定密钥、多系统共用的弱密钥、可从配置/镜像/仓库泄露的密钥、或长期不轮换的硬编码密钥,相似风险会重新出现。**”不是默认密钥”不代表密钥管理安全。**
三、研判第一步:rememberMe=deleteMe 是什么
探测 Shiro 的常见方式:
1 | Cookie: rememberMe=1 |
如果目标由 Shiro 处理,响应可能出现:
1 | Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0 |
它能说明什么:请求到达了处理 RememberMe 的组件;目标很可能用了 Shiro;如果由陌生外部源发起,可能处于框架探测阶段。
它不能说明什么:不能说明是受影响版本、用了默认密钥、攻击者已找到正确密钥、恶意对象已反序列化、命令已执行或主机已失陷。
误报来源:用户保存了失效的旧 Cookie、应用更新或密钥轮换、多节点密钥配置不一致、Cookie 被代理截断污染、自动化测试携带异常 Cookie、安全设备只按 rememberMe 长度或关键字匹配。
rememberMe=deleteMe是框架指纹和探测线索,不是漏洞利用成功的标志。
四、研判第二步:长 Cookie 是正常业务还是恶意 Payload
正常与恶意 Cookie 外观上都是:长、Base64 字符、肉眼不可读。只按”Cookie 很长”告警会产生大量误报。
区分方法(在已授权的离线环境用实际密钥解密分析):
正常身份数据的解密结果指向 Shiro 身份对象:PrincipalCollection、Realm 名称、用户名/用户 ID 等业务身份结构。
恶意对象的解密结果出现这些特征时风险等级立即上调:
ysoserial等Payload 生成工具痕迹TemplatesImpl、BeanComparator等高风险 Gadget 类Runtime.getRuntime().exec、ProcessBuilder等进程创建路径whoami、cmd.exe、curl、wget等命令字符串- DNS/HTTP/LDAP/RMI 外联地址
- 与身份对象完全无关的复杂对象图
但仍要区分两个结论:
- 解密后发现恶意对象结构 → 证明有明确攻击意图,且攻击者可能掌握有效构造条件;
- 恶意对象已执行成功 → 还需要响应、进程、文件、网络或日志证据。
Gadget 不匹配、类不存在、Java 版本不兼容或被安全控制阻断时,Payload 再漂亮也不会形成实际命令执行。
实操红线:不要把生产环境 Cookie 上传到在线解密网站,解密结果可能包含用户身份、Realm/类名/技术栈、内部业务字段甚至认证材料。现场用经过审批的本地离线工具;只做字节和结构分析,不要通过 ObjectInputStream.readObject() 或加载 Gadget 类来“查看”未知载荷,避免分析过程触发执行。
五、研判第三步:攻击到底有没有成功
有回显场景
响应中出现与测试命令严格对应的内容(当前用户名、主机名、唯一随机标记等)是强成功证据。但要排除:页面本来就含同样字符串、网关/扫描器回显了请求内容、响应属于缓存或其他请求。用请求 ID、五元组、时间戳和唯一标记确认请求与响应的对应关系。
无回显场景(更常见)
Java 反序列化经常没有 HTTP 回显。不能因为”响应正常”就判断攻击失败,继续查四类主机证据:
| 证据类型 | 排查点 |
|---|---|
| 进程 | Java 进程是否创建 cmd.exe/powershell/curl/wget 子进程;父子关系与命令行参数是否与 Payload 一致;时间是否吻合 |
| 文件 | Web/临时/上传目录新文件;脚本、WebShell、计划任务、异常 JAR;哈希是否命中情报 |
| 网络 | Java 进程异常 DNS 查询;向陌生 IP/域名的 HTTP/LDAP/RMI 连接;第二阶段载荷下载;反连/横向 |
| 日志 | 应用日志的 ClassNotFoundException、BadPaddingException 等解密/反序列化异常;EDR/HIDS 的 Java 子进程记录 |
HTTP 状态码不能直接定性
| 现象 | 正确理解 |
|---|---|
| HTTP 200 | 正常返回页面,不代表命令执行成功 |
| HTTP 302 | 可能只是跳登录页 |
| HTTP 500 | 可能是 Payload 触发异常,也可能是普通应用错误 |
rememberMe=deleteMe |
Cookie 被判无效或处理失败 |
| 响应变慢 | 可能与测试行为有关,也受负载影响 |
| DNS 外联命中唯一域名 | 较强的带外执行证据,仍需确认来源进程和时间 |
六、版本核验的正确姿势
不要只依赖登录页指纹或扫描器猜版本。更可靠的方式:
- 查 Maven
pom.xml/ Gradle 依赖锁定文件; - 查部署包
WEB-INF/lib里的shiro-core-*.jar、shiro-web-*.jar; - 读 JAR 的
META-INF/MANIFEST.MF或算哈希比对; - 核对运行进程实际加载的 JAR,而不是源码声明——防止”代码已升级、运行包未替换”;
- 容器镜像和历史遗留包单独排查。
七、把研判固化成流程
1 | Shiro 告警 |
每一层只回答自己能回答的问题,不越层下结论——这就是 Shiro 告警研判最核心的方法论。
修复建议(给防守方)
- 升级到最新版本,杜绝默认密钥;
- 配置随机、独立、定期轮换的 RememberMe 密钥,禁止从示例代码复制、多系统共用;
- 评估是否真的需要 RememberMe 功能,不需要就关闭;
- 对
rememberMeCookie 设置合理的长度上限与频率监控; - 主机侧对 Java 进程的子进程创建、异常外联做检测。
本文整理自驻场期间对 Shiro 告警的专题研究与研判实践,所有 Payload 相关内容仅用于防御研判学习。