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
方法,这样返回的是ReflectionConverter
convertAnother
方法会返回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