XStream反序列化漏洞学习
本文通过师傅们文章的学习、汇总来学习XStream
什么是XStream
XStream是一个实现javaBean与XML互相转换的工具。
它是一种OXMapping技术,是用来处理XML文件序列化的框架。Xstream不需要其它辅助类和映射文件,可以将JavaBean序列化成XML、json或将XML、jaon反序列化成JavaBean,使用非常方便。
使用XStream序列化时,对JavaBean没有任何限制。JavaBean的字段可以是私有的,也可以没有getter或setter方法,还可以没有默认的构造函数。XStream的序列化和反序列化主要依靠toXML函数和fromXML函数
- 官网说明:http://x-stream.github.io/
GitHub:https://github.com/x-stream/xstreamJavaDoc:http://x-stream.github.io/javadoc/index.html
XStream使用教程
xstream依赖如下:
1 | <dependency> |
1.4.18及其之后的版本由于对象默认开启安全防护,添加这条语句解决问题。尽量限制最低权限。这样才能将XML反序列化成bean
1 | xstream.addPermission(AnyTypePermission.ANY); |
添加如下依赖才能将XML反序列化成json
1 | <dependency> |
完整代码如下:
1 | //com.alte.Person.java |
1 | // com.alte.Test.java |
执行结果为:
1 | bean -> xml: |
前置知识
Converter转换器
XStream的核心包括一个Converters的注册表。Converters的责任是提供一种策略,用于将对象图中找到的特定类型的对象与XML之间进行转换。XStream为常见类型(如原语、字符串、文件、集合、数组和日期)提供了转换器。
转换器需要实现3个方法:
canConvert方法:告诉XStream对象,它能够转换的对象;marshal方法:能够将对象转换为XML时候的具体操作;unmarshal方法:能够将XML转换为对象时的具体操作;
详情请见官网
DynamicProxyConverter
DynamicProxyConverter即动态代理转换器,是XStream支持的一种转换器,其存在使得XStream能够把XML内容反序列化转换为动态代理类对象
它支持的类型是任何由java.lang.reflect.Proxy生成的动态代理
动态代理本身没有被序列化,但是它实现的接口和实际的InvocationHandler实例被序列化了。这允许代理在反序列化后被重新构建。
官网给的例子如下:
1 | <dynamic-proxy> |
dynamic-proxy标签在XStream反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah或com.foo.Woo这两个接口类中声明的方法时(即interface标签内指定的接口类),就会调用handler标签中的类方法com.foo.MyHandler。
Seebug的分析思路
这节介绍的是Seebug上的思路
在上面代码的Person.java文件中添加readObject方法
1 | private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { |
结果如下:
1 | bean -> xml: |
可以明显的发现当bean转为xml或json时存在差异性,另外在执行fromXML()方法时(即反序列化时)先执行了readObject方法。
可以看到,调用栈如下:
首先通过fromXML(String xml)传入xml
1 | <com.alter.Person serialization="custom"> |
然后xml转成Reader
然后经过
1 | unmarshal(this.hierarchicalStreamDriver.createReader(reader), (Object)null) |
进入上述过程的最后一个方法体内
来到com.thoughtworks.xstream.core.TreeUnmarshaller类的start方法,关键代码在于convertAnother((Object)null, type),可以看到,type值为class com.alter.Person
然后经过
1 | convertAnother((Object)null, type) |
上述过程最后一个方法是同文件的convertAnother方法。
起初,当converter == null时会获取一个converter,翻译为为转换器,XStream的思路是通过不同的converter来处理序列化数据中不同类型的数据。
跟进去看一下,可以发现实现了Serializable接口并且重写了readObject方法的Person类返回的是SerializableConverter
后面经过测试发现,如果只实现了Serializable接口但是没有重写readObject方法,这样返回的是ReflectionConverterconvertAnother方法会返回convert方法
然后经历
1 | convert(parent, type, converter) |
最后执行到Person#readObject方法。
这样的话,思路就清晰了,只要找到一条利用链,就能进行反序列化攻击了。
Mi1k7ea师傅分析思路
这节介绍的是Mi1k7ea师傅的分析思路
影响版本
在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内容后即可触发反序列化漏洞、达到任意代码执行的目的。
基于sorted-set的PoC
适用范围
1.4.5,1.4.6,1.4.10
payload如下:
1 | <sorted-set> |
详细的调试分析可参考Mi1k7ea师傅的文章
下面引用Mi1k7ea师傅的总结:
我们在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.3.1版本的原因
<=1.3.1以下版本不能成功识别出根标签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中有这么一句:
1 | java.bean.EventHandler no longer handled automatically because of severe security vulnerability. |
在ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤,导致不能成功利用
1.4.10能够成功的原因
在1.4.10中发现ReflectionConverter.canConvert()函数中把对EventHandler类的过滤又去掉了:
在利用的过程中虽然能够成功触发,但是控制台会输出提示未初始化XStream安全框架、会存在漏洞风险:
1 | Security framework of XStream not initialized, XStream is probably vulnerable. |
1.4.11修补方式
使用1.4.11运行,报错内容如下
1 | Security framework of XStream not initialized, XStream is probably vulnerable. |
先提醒未初始化安全框架,然后报错显示安全警告、拒绝反序列化目标类
此时,增加如下代码即可正常弹出计算器
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 | <tree-map> |
这里涉及到的转换器是TreeMapConverter,至于其整个调用过程以及原理和前面sorted-set(sorted-set是TreeSetConverter)的差不多,只是转换器不一样了。
能通杀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-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更为简单直接,不需要在dynamic-proxy外再加其他的转换器,直接利用的DynamicProxyConverter转换器来识别:
1 | <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-set的PoC的原因是一样的。
检测与防御
检测方法
查看目标环境中是否有存在漏洞版本的XStream的jar包,即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他的触发方式依赖的是HashMap、TreeSet这种类型自动调用的hashCode、compareTo串起来的,后续可以注意一下这种可能的调用链。wh1t3p1g师傅提到的所有POC,已经更新到GitHub上
用JNDI-Injection-Exploit工具,当jdk版本较高时,trustURLCodebase配置默认为false,如果要绕过这个配置必须得依靠tomcat,因此才要采用Tomcat 8+的那个payload。
总结
漏洞点有以下两处:
- 当系统给的
Bean实现了Serializable并且重写了readObject方法时,反序列化时会执行重写的readObject方法 XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。
用wh1t3p1g师傅的话说就是XStream的触发方式依赖的是HashMap、TreeSet这种类型自动调用的hashCode、compareTo串起来的
总的来说,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
