前言
看了Jackson、weblogic这些洞,发现很多原理及利用方式和xml解析有很大关系,所以顺着就继续学一下XStream系列的反序列化漏洞。
看XStream反序列化的时候我的学习方式:
- 看漏洞线:CVE库 + XStream官网News和ChangeLog
- 略看文档:架构 + 教程 + API
- 看博客复现漏洞
- todo:细看文档 + 使用XStream + 跟进新爆出来的洞并尝试挖掘
基本概念
一张图可以概括,摘自https://paper.seebug.org/1417/#43-xstream
EventHandler的动态代理
EventHandler可以作为动态代理类的handler,因为其实现了InvocationHandler。在设置target和action两个属性值后,在动态代理触发时可以调用任意类(target)的任意方法(action)。底层是使用MethodUtil#invoke
来实现的。
Demo
1 | package demo; |
调用链
调试一下,下断在proxy.compareTo(new Object());
执行后调用EventHandler#invoke
方法
之后在AccessController基于安全继续执行EventHandler#invokeInternal
方法
如果是可以直接处理的hashCode equals toString
这三个方法的话,就直接处理并返回
否则就用target和action来获取targetMethod,并使用MethodUtil.invoke来调用targetMethod
底层实现
接下来就分析一下MethodUtil.invoke在底层是如何通过反射调用来执行targetMethod的。
跟进MethodUtil#invoke
方法
看一下这个bounce是什么,这里回溯一下
继续跟进getTrampoline()
先使用MethodUtil.getTrampolineClass()
来获取Trampoline
类,该类是一个反射调用的封装类(default,且无法通过BootStrapClassLoader来加载),其中有invoke方法(private static)在实现了反射调用执行method
所以这里的MethodUtil
其实也是个ClassLoader,其继承了SecureClassLoader
,重写了loadClass
方法,目的就是为能够加载Trampoline
类
回到getTrampoline()中,获取到Trampoline类后,就是很正常的取出其invoke方法,设置访问控制权然后返回该方法,所以bounce就是(Method)Trampoline#invoke
总结一下就是反射调用经过两层封装,最后我们方便使用的就是MethodUtil#invoke
。参数关系如下
1 | MethodUtil.invoke(method,obj,args) |
这里给两个Demo提炼出其中过程
- Demo3:调用
Trampoline#invoke
1 | package demo; |
- Demo4:调用
MethodUtil#invode
1 | package demo; |
CVE-2013-7285 & 2019-10173
基于SortedSet
影响范围
- 1.4.5
- 1.4.6
- 1.4.10
下面会分析为何无法通杀的原因
POC
- poc1.xml
1 | <sorted-set> |
<interface>
标签表示代理的接口,还可以换成java.time.chrono.ChronoLocalDateTime等具有CompareTo
方法的接口类,笔者找到的合适的还有
- java.time.chrono.ChronoLocalDateTime
- com.sun.javadoc.Doc
同时<sorted-set>
还可以替换为(根标签才能这么替换,其他的需要根据具体的Converter来决定)
<java.util.SortedSet>
<xxx class="sorted-set">
<xxx class="java.util.SortedSet">
因为sorted-set
经过mapper获取后就是java.util.SortedSet
,而标签添加class
可以理解为class属性值是标签值的替代
调试分析
解析根标签
由于知道了Source点和Sink点,所以我们下断在开始的xstream.fromXML()
和EventHandler#invoke
中
跟进到XStream#unmarshal
方法中,可以看到获取具体解析策略,并调用其unmarshal
方法。根据前面对XStream解析模式的分析,包括ReferenceByXPath、ReferenceById、Tree(no Rerference)三种解析策略。这里获取到的是ReferenceByXPathMarshallingStrategy
跟进AbstractTreeMarshallingStrategy#unmarshal
,获取解析上下文,这里获取到的是ReferenceByXPathUnmarshaller
,继承关系如下
调用ReferenceByXPathUnmarshaller#start
开始解析,实际是调用TreeUnmarshaller#start
。在该方法中,先调用HierarchicalStreams.readClassType()
来获取根元素(标签)的类型,再调用this.convertAnother()
来继续解析根元素的其他子元素。
跟进readClassType()
方法,先根标签的解析class
属性,如果有class
属性的话,那么解析的对象就是class
属性的值,否则就是标签的值,可以理解为class属性替代了标签值。mapper默认先是CachingMapper
,也就是缓存的mapper,而mapper的作用是通过标签名解析来寻找对于的类名。且寻找的机制有点类似与类加载的委派机制。如果子mapper找不到的话,就会委托给逻辑父mapper来找(注意这里是逻辑父mapper,在代码上不一定有继承关系,而是用MapperWrapper抽象类来作为每个mapper的封装父类,其中的wrapped
变量定义了每一个mapper的逻辑父mapper)
跟进CachingMapper.readClass()
。先在一个缓存Map中获取(即是否有前面已经解析过的缓存),这里是第一次解析,所以没有,然后就会调用父mapper的readClass来解析。
根据父mapper的逻辑优先级顺序,会经过一顿调用到ArrayMapper#readClass
,该类重写了MapperWrapper的readClass方法。由于sorted-set
并非-array
结尾,所以进一步调用父mapper来解析
调用到ClassAliasingMapper
来解析,该类的nameToType
变量规定了标签名到类名的的映射关系,所以根据映射关系,可以得到类名为java.util.SortedSet
。如果我们设置的根标签是<java.util.SortedSet>
,那么这里的mappedName
就是null,最后的elementName
也还是一样的是java.util.SortedSet
。
之后在委派到父级的PackageAliasingMapper
处理后就会返回了,由于我们这里没设置类包的别名,所以这里还是原封不动,最后委派到DefaultMapper
来通过之前解析出来的类名来反射获取类Class。
获取到类java.util.SortedSet
之后,返回到CachingMapper.readClass()
中进行缓存,然后返回到TreeUnmarshaller#start
中,继续调用this.convertAnother()
来解析子标签。这里首先会进行一步转化,调用defaultImplementationOf(type)
获取SortedSet
接口的非内部实现类赋值给type,因为后面要初始化处理的话肯定得对类进行操作而非接口。所以这里获取到的是TreeSet
根据XStream中Converter的规定,TreeSet
类获取到的是TreeSetConverter
。同时,这里也会将类名和Converter缓存到DefaultConverterLookup.typeToConverterMap
中
解析子标签(Converter#unmarshal)
在获取到Convert后,调用this.convert
来正式开始解析子元素。解析过程使用到了FastStack堆栈数据结构以XPath的形式来保存标签元素。碰到起始标签就压栈该标签,碰到结尾标签就从栈中弹出,直到栈空则解析完毕。
调用父类TreeUnmarshaller#convert
继续解析。每一个标签的实际解析过程是在对应的Converter的unmarshal方法中实现的。根据不同类的成员变量结构,定制不同的unmarshal方法(也可以自己实现),所以调试unmarshal方法要结合类的结构来分析,否则可能会看不懂里面的一些操作
由于获取到的根标签Converter的TreeSetConverter,跟进。首先判断有没有<comparator>
标签,TreeSet是java中的一种数据结构,是Set的树结构形式,所以这里需要比较器。由于poc里没有设置,所以为null。
而Set类型的元素实际上是在内部维护的Map来存放的,因此比较或去重操作实际上也是Map来实现的。所以添加我们poc中的元素肯定会调用到TreeMap,而在TreeMap中可能就会调用比较的方法,比如CompareTo
,因此就会触发动态代理类的handler的invoke方法,进而到RCE。具体来跟一下。
获取完TreeMap
后,调用TreeSetConverter#
来进一步处理
跟进,而底层的比较又是通过PresortedMap
来实现的。继续跟进this.putCurrentEntryIntoMap
,该方法添加子元素到PresortedMap
中。其中的this.readItem
用来解析子标签,比如这里的<string>foo</string>
,过程和前面解析<sorted-set>
一样。
放置好第一个元素”foo”后,回到populateTreeMap
中,继续调用this.populateMap(reader, context, result, sortedMap);
来放置其他元素。
接下来就到了<dynamic-proxy>
标签的处理,和前面一样的获取过程,该标签获取到的type是DynamicProxyMapper$DynamicProxy
,Converter是DynamicProxyConverter
,所以之后会调用DynamicProxyConverter#unmarshal
来具体解析子标签。
<interface>
:可以有多个,表示代理的接口列表<handler>
:默认用class
属性表示handler类,可以有别名
解析完interface和handler后,初始化动态代理对象,然后再继续解析handler相关的子标签。而处理EventHandler
和ProcessBuilder
用的Converter都是优先级最低的AbstractReflectionConverter
,通过反射来处理标签,因此标签名和成员变量名是一致的
处理EventHandler
下的子标签结束后,返回DynamicProxyConverter#unmarshal
中,可以看到handler的两个关键成员变量已经被装配完成了
至此,proxy的解析也就完成了,然后返回到TreeMapConverter#populateTreeMap
中,调用result.putAll(sortedMap);
将PresortedMap
的两个元素放回TreeMap
中
TreeSet通过TreeMap#putAll添加元素
跟进到TreeMap#put
方法中,该方法中就是比较TreeMap的key(相当于TreeSet中的元素),以升序来排序。在防止完第一个元素”foo”后,可以看到,由于默认的comparator比较器为null,所以会调用元素自身的compareTo来进行比较,所以就会调用proxy.CompareTo
触发动态代理从而调用EventHandler#invoke
到RCE
1 | put:568, TreeMap (java.util) |
至此,整个流程就结束了,总结一下POC和解析的关键点。
- POC的中标签的名称和父标签对应的Converter的unmarshal方法有关
- TreeSet的初始化依赖TreeMap,TreeMap在没有比较器传入时,默认调用元素的CompareTo方法,因此会触发proxy.CompareTo
细节
无法通杀 <= 1.3.1
1.3.1及以下版本不支持对<sorted-set>
标签的解析,具体原因是在ClassAliasingMapper.nameToType
中没有规定sorted-set
的映射关系,即使换成java.util.SortedSet
也是不行的。
所以运行时会报错
1 | com.thoughtworks.xstream.mapper.CannotResolveClassException: sorted-set : sorted-set |
无法通杀1.4 - 1.4.4
定位到TreeSetConverter#unmarshal
方法,从前面的分析我们知道,这里就是恢复TreeSet
的过程,而对于TreeSet
的恢复,使用了两种策略:
- 第一种:直接使用
TreeSet#addAll(set)
来添加预先放置在set(PresortedSet)中的元素,因此不会进行排序,而是按照添加顺序默认排序的 - 第二种:调用
TreeMap#putAll(map)
经过排序后再添加回TreeSet
。上面分析的就是第二种方式。
而XStream决定这两种策略的关键变量是sortedMapField
在1.4 - 1.4.4版本中,XStream对SortedSet的默认处理是使用的第一种方式,所以sortedMapField
是null,导致treeMap
也是null,这样最终就会调用result.addAll(set);
来填充元素到TreeSet中,更底层一些只会调用TreeMap#addAllForTreeSet
而并不会调用TreeMap#putAll
,因此不会触发proxy.compareTo
。
无法通杀1.4.7 - 1.4.9
关于1.4.7 - 1.4.9无法通杀以及1.4.10又可以打通这一神奇现象,简单来说,就是开发人员整了个防御新活,同时把旧活删了,但新活默认不开(诶就是玩),因此又导致了原先的洞重新又可以打通了。
在1.4.7的ChangeLog中,对症下药把EventHandler
给ban了
1 | java.bean.EventHandler no longer handled automatically because of severe security vulnerability. |
具体是在ReflectionConverter#canConvert()
中,添加了对EventHandler
的过滤。根据前面的分析我们也知道,EventHandler
的是Converter是ReflectionConverter
1.4.10又能成功了
在>=1.4.7版本之后,XStream提供了可自定义的安全过滤,用户可通过addPermission()、allowTypes()、denyTypes()等
来设置黑白名单对类的convert进行限制。
在1.4.10,XStream推出了初始化默认安全框架的方法,也就是为用户默认集成好了一些过滤规则,是基于白名单的。但是默认不会开启,需要调用setupDefaultSecurity
来开启。但离谱的是,同时把1.4.7的修复给删了,所以导致在未显式调用setupDefaultSecurity
开启安全框架时,1.4.7之前的洞又成功了。
基于TreeMap
其实根据前面对于SortedSet的分析,这一部分就很简单了,简单记录一下。
影响范围
通杀1.4.x系列有漏洞的版本
- 1.4 - 1.4.6
- 1.4.10
POC
1 | <tree-map> |
补充几点:
tree-map
可以换成java.util.TreeMap
entry
可以换成任意字符串,如xxx
,原因在后面有解释
细节
调试分析参考sorted-set的,基本一样,只不过这里就是直接调用TreeMapConverter
了,并且putCurrentEntryIntoMap
使用的是原生的MapConverter
中的,而TreeSetConverter
中调用的TreeMapConverter#putCurrentEntryIntoMap
是经过重写了的,set只解析key而不是像正常的map一样需要解析key和value
无法通杀 <= 1.3.1
前面的原因是无法解析sorted-set。而这里的原因是默认不支持反序列化没有设置comparator的TreeMap,也就是说子标签中没有<comparator>
的话,就会报错
1 | com.thoughtworks.xstream.converters.ConversionException: TreeMap does not contain <comparator> element |
而我们不能设置<comparator class="xxx">
,根据前面的分析也知道,没有comparator
的时候才会默认调用元素的compareTo
方法,如果有comparator
的话比较逻辑就不一样的。
<entry>
标签为何能替换为<xxx>
关于标签的解析,可以看三个点:
- 根标签值或根标签class属性。根标签值和class属性等价
- 具体标签Converter的unmarshal方法,不同的Converter定制不同的子标签。比如
ReflectionConverter
,其子标签写法
1 | // field非对象(这里的对象指基本类型外,具有属性的) |
而我们这里需要关注到TreeMapConverter#unmarshal
,我们知道,具体解析TreeMap元素的方法在putCurrentEntryIntoMap
。这里仅仅是通过多一层的moveDown
和moveUp
来将reader移动到<entry>
下面的标签,而对entry
的值并没有啥要求,因为直接就移过了,所以我们在包裹key-value标签的时候,只需要包裹任意标签即可(但不能省略)。
基于接口
这种方式比较鸡肋,需要知道服务端动态代理的接口名,否则无法利用。原理:如果服务端使用XStream来恢复一个动态代理对象的话,后续肯定会调用该动态代理对象的其他方法,因此能够触发。
影响范围
通杀1.4.x系列有漏洞的版本
- 1.4 - 1.4.6
- 1.4.10
Demo
- TestInface接口
1 | package demo; |
- Test2.java
1 | package testpoc; |
POC
1 | <dynamic-proxy> |
漏洞修复
这种基于反序列化的洞的修复方式基本上都是黑白名单机制,所以这也是一样的
1.4.7 - 1.4.9
- ReflectionConverter.canConvert()限制了
EventHandler
- XStream提供了一个安全框架供用户使用,但必须手工设置,可以调用addPermission()、allowTypes()、denyTypes()等建立黑白名单
1 | XStream.addPermission(TypePermission); |
1.4.10
提供了XStream.setupDefaultSecurity()
函数来设置XStream反序列化类型的默认内置白名单,其本质还是调用XStream提供的安全框架里的addPermission()、allowTypes()、denyTypes()等函数。但必须手工开启,否则还是存在漏洞
1.4.11之后
新增了一个内置在XStream中的Converter类InternalBlackList
。过滤了EventHandler
类、以javax.crypto.
开头的类、以$LazyIterator
结尾的类,匹配到了之后,unmarshal
方法直接抛异常。并且InternalBlackList的优先级为PRIORITY_LOW高于ReflectionConverter的优先级PRIORITY_VERY_LOW。所以对于EventHandler类得到的Converter是InternalBlackList
,因此后续调用InternalBlackList.unmarshal()
直接抛出异常。
1 | private class InternalBlackList implements Converter { |