Java XStream反序列化漏洞
/0x01 基本概念
XStream简介
XStream是Java类库,可以用来将对象序列化为XML格式或将XML反序列化为对象。
使用XStream实现序列化与反序列化
下面看下如何使用XStream进行序列化和反序列化操作的。
先定义IPerson.java接口类:
1 | public interface IPerson { |
接着定义Person类实现前面的接口:
1 | public class Person implements IPerson { |
XStream序列化是调用XStream.toXML()来实现的:
1 | public class Test { |
输出内容:
1 | <Person> |
将输出结果保存为person.xml文件。
XStream反序列化是用过调用XStream.fromXML()来实现的,其中获取XML文件内容的方式可以通过Scanner()或FileInputStream都可以:
1 | import com.thoughtworks.xstream.XStream; |
输出结果为:
1 | Hello, this is mi1k7ea, age 6 |
Java动态代理机制
在代码编译时未确定具体的被代理类,而在代码运行时动态加载被代理的类。在Java中java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口就可以生成动态代理对象。在程序运行过程中产生的这个对象,其实就是通过反射机制来生成的一个代理。
通过动态代理机制,可以为某一个对象动态生成一个代理对象来替代对原对象的访问,可以有效地控制对原对象的访问并且能够很好地隐藏和保护原对象,此外还能在原对象函数的基础上添加一些额外的操作,如打印日志等。
具体的可参考:Java动态代理机制
EventHandler类
EventHandler类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。
EventHandler类是实现了InvocationHandler的一个类,设计本意是为交互工具提供beans,建立从用户界面到应用程序逻辑的连接。
EventHandler类定义的代码如下,其含有target和action属性,在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用:
1 | public class EventHandler implements InvocationHandler { |
这里重点看下EventHandler.invokeInternal()函数的代码逻辑,如注释:
1 | private Object invokeInternal(Object var1, Method var2, Object[] var3) { |
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 | <dynamic-proxy> |
dynamic-proxy标签在XStream反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah或com.foo.Woo这两个接口类中声明的方法时(即interface标签内指定的接口类),就会调用handler标签中的类方法com.foo.MyHandler。
0x02 XStream反序列化漏洞
影响范围
在1.4.x系列版本中,<=1.4.6或=1.4.10是存在反序列化漏洞的。
基本原理
XStream是自己实现的一套序列化和反序列化机制,核心是通过Converter转换器来将XML和对象之间进行相互的转换,这便与原生的Java序列化和反序列化机制有所区别,因此两者的反序列化漏洞也是有着很大区别的。
XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。
几种PoC浅析
基于sorted-set的PoC
适用范围
1.4.5,1.4.6,1.4.10
复现
复现用的XStream版本是1.4.6。
payload1.xml:
1 | <sorted-set> |
运行触发:
如果想加入多条命令,如创建目录等,可改为如下的payload:
1 | // Windows |
调试分析
下面我们在xstream.fromXML()
语句中打上断点进行调试,同时在EventHandler类中的invoke()和invokeInternal()函数上也打上断点。
在AbstractTreeMarshallingStrategy.unmarshal()函数中,调用了TreeUnmarshaller.start()函数,即开始解析XML:
跟进start()函数,发现会调用HierarchicalStreams.readClassType()来获取到PoC XML中根标签的类类型java.util.SortedSet:
接着是调用convertAnother()函数对java.util.SortedSet类型进行转换,我们跟进去该函数,其中调用mapper.defaultImplementationOf()函数来寻找java.util.SortedSet类型的默认实现类型进行替换,这里转换为了java.util.TreeSet类型:
接着看到调用converterLookup.lookupConverterForType()来寻找TreeSet对应类型的转换器。我们跟进这个函数看看,其是变量所有转换器,通过调用Converter.canConvert()函数来判断该转换器是否能够转换出TreeSet类型,这里找到满足条件的TreeSetConverter转换器:
在XStream官网中可以查到类对应的Converter:http://x-stream.github.io/javadoc/com/thoughtworks/xstream/converters/collections/TreeMapConverter.html
接着是调用typeToConverterMap.put(type, converter);
将类型和转换器的对应关系放入Map表中,再返回转换器:
往下调试,在AbstractReferenceUnmarshaller.convert()函数中看到,会调用getCurrentReferenceKey()来获取当前的Reference键即标签名,接着将当前标签名压入parentStack栈中:
接着调用其父类即的FastStack.convert()方法,跟进去,显示将类型压入栈,然后调用转换器TreeSetConverter的unmarshal()方法:
往下调试,在TreeSetConverter.unmarshal()函数中调用了this.treeMapConverter.populateTreeMap():
跟进该函数,先判断是否是第一个元素,是的话就调用putCurrentEntryIntoMap()函数,即将当前内容缓存到Map中:
跟进去,发现调用readItem()函数读取标签内的内容并缓存到target这个Map中:
返回到populateTreeMap()函数中,调用了reader.moveUp();
即开始往下读其他元素,然后调用populateMap()函数:
跟进populateMap(),其中调用了populateCollection()函数,用来循环遍历子标签中的元素并添加到集合中,如图是将动态代理标签添加进集合中:
而调用的addCurrentElementToCollection()中,会调用readItem()读取标签内容,这里直接跳过具体的读取步骤,看到是成功获取到了该动态代理类并添加到了target这个Map缓存起来了:
继续跟进去几个函数,会发现调用DynamicProxyConverter.unmarshal()函数,这是由于PoC中含有dynamic-proxy标签会被程序识别并调用对应的DynamicProxyConverter转换器来实现将XML中该标签部分转换成动态代理类对象。而在该转换器的unmarshal()函数中,主要是扫描该标签的内容,然后调用Proxy.newProxyInstance()函数来生成新的动态代理类对象并返回(该动态代理类的target为EventHandler,action为start):
在上图我们可以看到DynamicProxyConverter.unmarshal()函数中调用了convertAnother()函数来转换得到EventHandler,跟进该函数会发现是调用了ReflectionConverter转换器来进行EventHandler的解析的:
下面继续往下调试,回到populateMap()调用的地方。
调用完populateMap()之后,会判断JVM是否已充分将TreeMap都缓存起来了,然后调用TreeMap类对象resullt的putAll()方法,可看到参数中包含动态代理类,该代理类指向EventHandler类,而该类正如前面介绍时说的那样通过传入target和action参数值来利用反射机制调用了ProcessBuilder(cmd).start()来执行任意命令:
再跟进去调试,调试到TreeMap.put()函数中发现会调用到动态代理类对象$Proxy0的compareTo()方法来比较动态代理类对象和另一个字符串对象:
由于我们PoC中interface标签写的是java.lang.Comparable
接口,而该接口声明了一个compareTo()方法,因此当调用了动态代理类对象中的Comparable.compareTo()方法时就能成功动态调用PoC中构造的恶意动态代理类,从而通过反射机制达到任意代码执行。
再往下,会调用到EventHandler.invoke(),其中会通过安全管理器获得权限来执行EventHandler.invokeInternal()函数,可以看到proxy参数是动态代理类对象、\9-]参数是compareTo方法、arguments参数是包含”foo”字符串的数组:
在EventHandler.invokeInternal()函数中,获取到目标动态代理类对象的实际方法后,就直接通过反射机制调用,从而导致弹计算器:
小结
我们在PoC中构造了一对sorted-set标签,其中包含实现了Comparable接口的dynamic-proxy标签,该代理标签中又包含一个指向EventHandler的handler标签,而Eventhandler中则包含了一个ProcessBuilder的target和值为’start’的action。
在XStream反序列化过程中,解析XML,将sorted-set标签识别出对应的TreeSetConverter转换器,再识别出sorted-set标签内有两个子元素,即string标签和dynamic-proxy标签;string标签会被识别出StringConverter转换器来解析出string标签内的字符串“foo”;dynamic-proxy标签会被识别出对应的DynamicProxyConverter转换器来解析出动态代理类对象;最后由于TreeSetConverter会对比两个子元素即调用$Proxy0.compareTo()来比较,而dynamic-proxy标签内实现了Comparable接口,因此由动态代理机制会触发dynamic-proxy标签内的handler标签指向的EventHandler类方法,从而利用反射机制实现任意代码执行。
整个调试过程主要的函数调用链如下:
1 | XStream.fromXML |
无法通杀<=1.3.1版本的原因
<=1.3.1以下版本不能成功识别出根标签sorted-set的类,也就是说低版本并不支持sorted-set:
1 | com.thoughtworks.xstream.mapper.CannotResolveClassException: sorted-set : sorted-set |
无法通杀1.4-1.4.5版本的原因
先看下TreeSetConverter.unmarshal()中的代码逻辑,当sortedMapField不为null时,treeMap才有可能不为null,treeMap不为null才能进入populateTreeMap():
在1.4-1.4.4版本中,sortedMapField默认为null,因此无法成功利用:
而在>=1.4.5版本中,sortedMapField默认不为null,因此能成功利用:
无法通杀1.4.7-1.4.9版本的原因
在1.4.7版本的Change Log中有这么一句:
java.bean.EventHandler no longer handled automatically because of severe security vulnerability.
运行PoC会报以下错误:
1 | Exception in thread "main" com.thoughtworks.xstream.converters.ConversionException: No converter specified for class java.beans.EventHandler |
在ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤,导致不能成功利用:
为何1.4.10能够成功
我们知道1.4.7-1.4.9版本中是因为在ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤导致不能成功利用。
但是我们在1.4.10中发现ReflectionConverter.canConvert()函数中把对EventHandler类的过滤又去掉了:
1 | public boolean canConvert(Class type) { |
在利用的过程中虽然能够成功触发,但是控制台会输出提示未初始化XStream安全框架、会存在漏洞风险:
1 | Security framework of XStream not initialized, XStream is probably vulnerable. |
看看1.4.11如何修补的
直接运行,先提醒未初始化安全框架,然后报错显示安全警告、拒绝反序列化目标类:
1 | Security framework of XStream not initialized, XStream is probably vulnerable. |
可以看到,1.4.11以后的版本XStream新增了一个Converter类InternalBlackList,可以看到其实现的canConverter()方法中对EventHandler类、以”javax.crypto.”开头的类、以”$LazyIterator”结尾的类都进行了匹配,而其marshal()和unmarshal()方法都是直接抛出异常的,换句话说就是匹配成功的直接抛出异常即黑名单过滤:
1 | private class InternalBlackList implements Converter { |
在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。
复现
payload2.xml:
1 | <tree-map> |
运行触发:
可以看到,该payload涉及到的转换器是TreeMapConverter,至于其整个调用过程以及原理和前面sorted-set的差不多,只是转换器不一样了,这里就不再调试分析了。
为何能通杀1.4-1.4.5版本
因为本次payload用的是TreeMapConverter转换器,和前面TreeSetConverter不一样,这里不存在类似sortedMapField是否为null的限制,因为两个转换器的代理逻辑完全不一样,调试一下就清楚了。
无法通杀<=1.3.1版本的原因
运行PoC会报错显示TreeMap没有包含comparator元素,即不支持PoC中两个子标签元素调用compareTo()进行比较,因此无法利用:
1 | com.thoughtworks.xstream.converters.ConversionException: TreeMap does not contain <comparator> element |
在TreeMapConverter.unmarshal()中看到,判断子标签节点是否有comparator,若两个if判断条件都不满足则直接抛出异常,不会进入后面的populateMap()函数,因此也不会成功触发:
无法通杀1.4.7-1.4.9版本的原因
和前面基于sorted-set的PoC的原因是一样的。
基于接口的PoC
适用范围
通杀1.4.x系列有漏洞的版本,即<=1.4.6或=1.4.10。但是缺点是,我们必须得知道服务端反序列化得到的是啥接口类。
接口特征
一般的,基于接口类型的payload,是需要按照接口形式来编写的,即interface标签内容指向接口类。比如官网给的例子,其中Contact是个接口类:
1 | <contact> |
1 | XStream xstream = new XStream(); |
这种方式是基于服务端解析XML之后会直接调用到XML中interface标签指向的接口类声明的方法,因此这种情形下必然会触发动态代理类对象的恶意方法。
复现
下面我们试下这个payload,ipayload.xml,这个更为简单直接,不需要在dynamic-proxy外再加其他的转换器,直接利用的DynamicProxyConverter转换器来识别:
1 | <dynamic-proxy> |
修改Test.java,将Person类改为IPerson接口类,和ipayload.xml中的interface标签内容相对应:
1 | public class Test { |
还有一点需要注意的是,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-set的PoC的原因是一样的。
0x03 检测与防御
检测方法
- 查看目标环境中是否有存在漏洞版本的XStream的jar包,即1.4.x系列版本中<=1.4.6或=1.4.10;
- 全局搜索是否存在
Xstream.fromXML(
的地方,若存在则进一步分析该参数是否外部可控;若为1.4.10版本的还需要确认是否开启了安全配置进行了有效的防御;
防御方法
将XStream升级到最新版,即1.4.11之后的版本;
若只想手动修改代码,可以参考1.4.7-1.4.9版本的修补方法,在ReflectionConverter.canConvert()函数中添加了对包括EventHandler等类的过滤,当然这只是黑名单过滤方式,存在绕过风险:
1
2
3
4public boolean canConvert(Class type) {
return ((this.type != null && this.type == type) || (this.type == null && type != null && type != eventHandlerType))
&& canAccess(type);
}若版本号>=1.4.7,XStream提供了一个安全框架供用户使用,但必须手工设置,可以调用addPermission()、allowTypes()、denyTypes()等对某些类进行限制,即建立黑白名单机制进行过滤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14XStream.addPermission(TypePermission);
XStream.allowTypes(Class []);
XStream.allowTypes(String []);
XStream.allowTypesByRegExp(String []);
XStream.allowTypesByRegExp(Pattern []);
XStream.allowTypesByWildcard(String []);
XStream.allowTypeHierary(Class);
XStream.denyPermission(TypePermission);
XStream.denyTypes(Class []);
XStream.denyTypes(String []);
XStream.denyTypesByRegExp(String []);
XStream.denyTypesByRegExp(Pattern []);
XStream.denyTypesByWildcard(String []);
XStream.denyTypeHierary(Class);若是1.4.10版本,提供了XStream.setupDefaultSecurity()函数来设置XStream反序列化类型的默认白名单,其本质还是调用XStream提供的安全框架里的addPermission()、allowTypes()、denyTypes()等函数,区别在于自己定义了一些默认白名单,但必须手工设置,否则还是存在漏洞:
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
86public static void setupDefaultSecurity(final XStream xstream) {
if (xstream.insecureWarning) {
xstream.addPermission(NoTypePermission.NONE);
xstream.addPermission(NullPermission.NULL);
xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
xstream.addPermission(ArrayTypePermission.ARRAYS);
xstream.addPermission(InterfaceTypePermission.INTERFACES);
xstream.allowTypeHierarchy(Calendar.class);
xstream.allowTypeHierarchy(Collection.class);
xstream.allowTypeHierarchy(Map.class);
xstream.allowTypeHierarchy(Map.Entry.class);
xstream.allowTypeHierarchy(Member.class);
xstream.allowTypeHierarchy(Number.class);
xstream.allowTypeHierarchy(Throwable.class);
xstream.allowTypeHierarchy(TimeZone.class);
Class type = JVM.loadClassForName("java.lang.Enum");
if (type != null) {
xstream.allowTypeHierarchy(type);
}
type = JVM.loadClassForName("java.nio.file.Path");
if (type != null) {
xstream.allowTypeHierarchy(type);
}
final Set types = new HashSet();
types.add(BitSet.class);
types.add(Charset.class);
types.add(Class.class);
types.add(Currency.class);
types.add(Date.class);
types.add(DecimalFormatSymbols.class);
types.add(File.class);
types.add(Locale.class);
types.add(Object.class);
types.add(Pattern.class);
types.add(StackTraceElement.class);
types.add(String.class);
types.add(StringBuffer.class);
types.add(JVM.loadClassForName("java.lang.StringBuilder"));
types.add(URL.class);
types.add(URI.class);
types.add(JVM.loadClassForName("java.util.UUID"));
if (JVM.isSQLAvailable()) {
types.add(JVM.loadClassForName("java.sql.Timestamp"));
types.add(JVM.loadClassForName("java.sql.Time"));
types.add(JVM.loadClassForName("java.sql.Date"));
}
if (JVM.is18()) {
xstream.allowTypeHierarchy(JVM.loadClassForName("java.time.Clock"));
types.add(JVM.loadClassForName("java.time.Duration"));
types.add(JVM.loadClassForName("java.time.Instant"));
types.add(JVM.loadClassForName("java.time.LocalDate"));
types.add(JVM.loadClassForName("java.time.LocalDateTime"));
types.add(JVM.loadClassForName("java.time.LocalTime"));
types.add(JVM.loadClassForName("java.time.MonthDay"));
types.add(JVM.loadClassForName("java.time.OffsetDateTime"));
types.add(JVM.loadClassForName("java.time.OffsetTime"));
types.add(JVM.loadClassForName("java.time.Period"));
types.add(JVM.loadClassForName("java.time.Ser"));
types.add(JVM.loadClassForName("java.time.Year"));
types.add(JVM.loadClassForName("java.time.YearMonth"));
types.add(JVM.loadClassForName("java.time.ZonedDateTime"));
xstream.allowTypeHierarchy(JVM.loadClassForName("java.time.ZoneId"));
types.add(JVM.loadClassForName("java.time.chrono.HijrahDate"));
types.add(JVM.loadClassForName("java.time.chrono.JapaneseDate"));
types.add(JVM.loadClassForName("java.time.chrono.JapaneseEra"));
types.add(JVM.loadClassForName("java.time.chrono.MinguoDate"));
types.add(JVM.loadClassForName("java.time.chrono.ThaiBuddhistDate"));
types.add(JVM.loadClassForName("java.time.chrono.Ser"));
xstream.allowTypeHierarchy(JVM.loadClassForName("java.time.chrono.Chronology"));
types.add(JVM.loadClassForName("java.time.temporal.ValueRange"));
types.add(JVM.loadClassForName("java.time.temporal.WeekFields"));
}
types.remove(null);
final Iterator iter = types.iterator();
final Class[] classes = new Class[types.size()];
for (int i = 0; i < classes.length; ++i) {
classes[i] = (Class)iter.next();
}
xstream.allowTypes(classes);
} else {
throw new IllegalArgumentException("Security framework of XStream instance already initialized");
}
}试下效果,在前面的Demo我们添加这个默认白名单过滤:
1
2
3
4
5
6
7
8
9
10public class Test {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream xml = new FileInputStream("ipayload.xml");
XStream xstream = new XStream(new DomDriver());
// 使用默认白名单过滤
XStream.setupDefaultSecurity(xstream);
Person p = (Person) xstream.fromXML(xml);
p.output();
}
}运行后会报错,显示禁止反序列化动态代理类,成功修补了漏洞: