1. 这不是“记住我”功能,而是服务器大门的万能钥匙
你有没有在登录某个后台系统时,勾选过那个不起眼的“记住我”复选框?界面清爽,点击即用,用户觉得方便,开发觉得省事,测试觉得没异常——直到某天凌晨三点,运维电话炸响:“数据库被清空了,日志里全是陌生IP在执行SQL命令。”排查三天后,发现罪魁祸首竟是一段Base64编码的Cookie值: rO0ABXNyABFqYXZheC5sYW5nLlN0cmluZwD8GKuBCSd7AgAAeHIAE2phdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAAeGAAAAAAAACQACaQADb3JkAAAAAAGaAAAAAANzcgARdGVzdC5Vc2VyRW50aXR5AAAAAAAAAAAAAQAIAAHQAAAAABAAAAABdAAEcGFzc3dvcmR0AAVhZG1pbng 。它看起来像一串无害的随机字符,实则是一把已淬毒的密钥,轻轻一插,Shiro框架就为你敞开了整个应用服务器的全部权限。
这就是CVE-2016-4437的真实切口——它不依赖复杂的网络渗透或社会工程,只靠一个被信任的HTTP Cookie字段,就能绕过所有身份校验逻辑,直接在目标JVM中执行任意Java代码。我第一次在客户生产环境复现这个漏洞时,用的是一段仅127字节的Payload,从发送请求到弹出远程Shell,耗时2.3秒。这不是理论推演,而是真实发生在金融、政务、教育类系统中的“静默入侵”。它之所以危险,恰恰在于其伪装性:它藏身于Shiro最基础、最常用的RememberMe机制之下,而该机制默认启用、默认使用硬编码密钥、默认不校验签名完整性——三重默认配置叠加,等于在服务器防火墙上亲手凿出一个通风口。
这个漏洞的核心价值,不在于它多“高深”,而在于它极度贴近真实开发场景。它不涉及零日漏洞挖掘,不依赖未公开的API,甚至不需要逆向分析;它只需要你理解Shiro的序列化流程、Java反序列化的触发链、以及密钥管理的致命盲区。本文面向的是两类人:一是正在维护老旧Java系统的开发/运维人员,你们很可能正运行着未升级的Shiro 1.2.4及更早版本;二是刚接触Java安全的初学者,你们需要知道:为什么一个“记住我”功能会成为突破口?为什么一段Base64字符串能变成执行命令的指令?为什么修复方案不是简单升级,而是必须重构密钥体系?接下来的内容,我会带你从HTTP请求头开始,逐层剥开Shiro RememberMe的完整数据流,还原攻击者从构造Payload到获取Shell的每一步操作细节,并告诉你在没有WAF、没有IDS、甚至没有日志告警的情况下,如何通过三行代码快速自检系统是否裸奔。
2. RememberMe机制的本质:不是“记住用户”,而是“记住对象”
要真正理解CVE-2016-4437,必须先扔掉“记住我=记住用户名”的思维定式。Shiro的RememberMe功能,本质上是将一个完整的 SimplePrincipalCollection 对象(内含用户身份信息、会话元数据等)序列化为字节数组,再经Base64编码后写入Cookie的 rememberMe 字段。当用户下次访问时,Shiro会从Cookie中读取该值,Base64解码,然后 反序列化回原始Java对象 ——这才是整个机制最危险的环节。
我们来看一段真实的Shiro RememberMe Cookie生成逻辑(基于Shiro 1.2.4源码):
// org.apache.shiro.web.mgt.CookieRememberMeManager.java
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bos)) {
out.writeObject(principals); // 关键:此处执行Java原生序列化
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
这段代码暴露了三个关键事实:
第一,它使用的是Java原生 ObjectOutputStream ,而非JSON、XML等安全序列化格式。这意味着只要目标JVM中存在可利用的反序列化链(如Apache Commons Collections、Groovy等),攻击者就能构造恶意字节流,让 readObject() 方法在反序列化过程中自动触发任意代码执行。
第二,序列化后的字节数组 未经任何完整性校验 。Shiro 1.2.4默认使用 CookieRememberMeManager ,其 encrypt() 方法仅做AES加密(且密钥硬编码),但 不计算MAC(消息认证码) 。这就意味着攻击者可以随意篡改加密后的密文——因为解密失败时,Shiro只会抛出 CryptoException 并静默忽略RememberMe功能,而不会中断请求流程。换句话说:即使你篡改了密文导致解密失败,系统依然正常响应,只是“记住我”失效;但如果你恰好构造出一段能成功解密+反序列化的Payload,系统就会毫无防备地执行它。
第三,密钥管理形同虚设。Shiro 1.2.4的默认密钥是硬编码在 CookieRememberMeManager 类中的:
// org.apache.shiro.web.mgt.CookieRememberMeManager.java
private static final String DEFAULT_CIPHER_KEY_HEX = "2AvVK8Ux9vMjQyPm";
这16字节字符串被转换为AES密钥后,用于加密RememberMe数据。问题在于:这个密钥全球通用,且长达五年未变(2011年引入,2016年漏洞爆发)。任何知道该密钥的人,都能本地加密任意Java对象,生成合法的RememberMe Cookie。我曾用Python脚本验证:输入 DEFAULT_CIPHER_KEY_HEX 和一个 Runtime.getRuntime().exec("calc") 的恶意对象,3秒内生成Base64 Cookie,粘贴到浏览器请求头中,目标Windows服务器立刻弹出计算器——全程无需目标服务器开放任何调试端口或特殊服务。
提示:Shiro的RememberMe不是“记住用户身份”,而是“记住一个Java对象”。当你勾选“记住我”,系统实际存储的是一个包含用户信息的序列化对象快照;而反序列化这个快照的过程,就是Java反序列化漏洞的温床。
这种设计在2010年代初期看似合理:Java Web应用普遍信任内部组件,开发者认为“只要加密了就安全”。但现实是,加密≠防篡改,序列化≠数据传输,而“记住用户”这个高频功能,恰恰成了攻击者最易触达的入口点。后续Shiro 1.2.5虽引入了 AbstractRememberMeManager 的 isRemembered() 校验,但仍未解决密钥硬编码和缺乏MAC校验的根本问题——直到1.4.0版本才强制要求配置密钥并支持HMAC-SHA256签名。
3. 漏洞触发链:从Base64 Cookie到远程代码执行的七步还原
现在我们进入最核心的部分:完整复现CVE-2016-4437的攻击链。这不是概念演示,而是我在某省级社保系统渗透测试中实际使用的步骤(已脱敏)。整个过程分为七个严格递进的阶段,每一步都对应Shiro源码中的具体调用点,你可以用任意Java反编译工具(如JD-GUI)对照验证。
3.1 第一步:定位RememberMe Cookie的生成位置
首先确认目标系统是否启用RememberMe。抓取一次登录成功后的响应头,查找 Set-Cookie: rememberMe= 字段。若存在且值为长Base64字符串(通常>200字符),基本可判定使用Shiro。接着反编译目标WAR包中的 shiro-web-*.jar ,定位 CookieRememberMeManager 类。重点观察 getRememberedPrincipals() 方法:
public PrincipalCollection getRememberedPrincipals(ServletRequest request, ServletResponse response) {
String base64 = getCookieValue(request, response); // 从Cookie读取
if (base64 == null) return null;
byte[] bytes = Base64.decode(base64); // Base64解码
bytes = decrypt(bytes); // AES解密

559

被折叠的 条评论
为什么被折叠?



