Cursory Analysis Of XStream

简介

XStream是Java类库,提供了所有的基础类型、数组、集合等类型直接转换的支持,用来将对象序列化成XML或反序列化为对象。

基本使用

XStream反序列化分析采用依赖版本如下:

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>

定义接口类IndexInterface如下:

1
2
3
4
5
6
package org.example.deserialize.xstream;

public interface IndexInterface {

void output();
}
定义类Index实现IndexInterface接口如下:
1
2
3
4
5
6
7
8
9
10
package org.example.deserialize.xstream;

public class Index implements IndexInterface {

String name;
@Override
public void output() {
System.out.println("Hello, " + this.name);
}
}
定义类IndexXML实现XStream序列化与反序列化如下:
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
27
28
package org.example.deserialize.xstream;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.FileInputStream;

public class IndexXML {

public static void main(String[] args) throws Exception {
serialize();
deserialize();
}

public static void serialize() {
Index index = new Index();
index.name = "XStream";
XStream xStream = new XStream(new DomDriver());
String xml = xStream.toXML(index);
System.out.println(xml);
}

public static void deserialize() throws Exception {
FileInputStream fileInputStream = new FileInputStream("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/deserialize/xstream/index.xml");
XStream xStream = new XStream(new DomDriver());
Index index = (Index) xStream.fromXML(fileInputStream);
index.output();
}
}

前置知识

Converter

Converter的职责是提供一种策略,用于在对象图中找到的特定类型的对象与XML之间的转换,XStream为Java的常见类型(原始类型、字符串、文件、集合、数组和日期等)提供了Converter转换器。简而言之,就是输入XML后它能识别其中的标签字段并转换为相应的对象,反之亦然。

转换器需要实现的三个方法:

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

MapConverter

MapConverter是针对Map类型还原的Converter,跟进com.thoughtworks.xstream.converters.collections#unmarshal方法,依次调用unmarshal方法、populateMap方法和putCurrentEntryIntoMap方法,在putCurrentEntryIntoMap方法中会调用Map#put方法,后续就是对key调用hashCode函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map) {
this.populateMap(reader, context, map, map);
}

protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
while(reader.hasMoreChildren()) {
reader.moveDown();
this.putCurrentEntryIntoMap(reader, context, map, target);
reader.moveUp();
}

}

protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
reader.moveDown();
Object key = this.readItem(reader, context, map);
reader.moveUp();
reader.moveDown();
Object value = this.readItem(reader, context, map);
reader.moveUp();
target.put(key, value);
}

TreeSetConverter&TreeMapConverter

TreeSetConverter的反序列化处理方式为先转化为TreeMapConverter的方式,优先还原TreeSet里的TreeMap,再填充到TreeSet中。

TreeSetConverter的调用来看看整个的调用过程,先从TreeSet中提取出TreeMap,接着调用TreeMapConverter来还原TreeMap。在TreeMapConverter中利用sortedMap来填充需要还原的Entry,这里会回到上文提到的MapConverter类中的populateMapputCurrentEntryIntoMap方法,最后调用TreeMap#putAll方法,调用到java.util.AbstractMap#putAll方法。

DynamicProxyConverter

DynamicProxyConverter,即动态代理转换器,支持对动态代理的方式进行还原,使得XStream能够把XML内容反序列化转换为动态代理类对象,使用Proxy动态代理,可以扩展前面两种Converter自动调用函数的攻击面。

EventHandler

EventHandler类是一个实现了InvocationHandler的类,EventHandler类定义的代码如下:其含有target和action属性,函数调用链为EventHandler.invoke->EventHandler.invokeInternal->MethodUtil.invoke。

在invokeInternal方法中,首先会判断调用的函数名是否为hashCode、equals和toString,由于需要利用到后续部分的MethodUtil#invoke方法,因此上文提到的Map相关的Converter无法利用,但是可以利用TreeSet去触发compareTo函数。

后面就是利用Java反射机制来实现函数调用,并且变量target和action都是可控的,对于参数action需要满足:

  • 无参数类型函数(ProcessBuilder#start、JdbcRowSetImpl#getDatabaseMetaData等)
  • 单个参数类型函数,且参数类型为Comparable,并且这个函数是可利用的

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<sorted-set>
<string>foo</string>
<dynamic-proxy> <!-- Proxy 动态代理,handler使用EventHandler -->
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>/System/Applications/Calculator.app</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example.deserialize.xstream;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

import java.io.FileInputStream;

public class VulnXML {

public static void main(String[] args) throws Exception {

FileInputStream fileInputStream = new FileInputStream("src/main/java/org/example/deserialize/xstream/CVE-2013-7285.xml");
XStream xStream = new XStream(new DomDriver());
Index index = (Index) xStream.fromXML(fileInputStream);
index.output();
}
}

在xStream.fromXML处下断点,同时在EventHandler#invoke方法和EventHandler#invokeInternal方法上也添加断点。

在多次调用unmarshal方法后,进入com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy#unmarshal方法,调用TreeUnmarshaller#start方法解析XML内容。

跟进TreeUnmarshaller#start方法,调用HierarchicalStreams#readClassType方法来获取XML中根标签的类型。

接着调用TreeUnmarshaller#convertAnother方法对java.util.SortedSet类型进行转换,跟进TreeUnmarshaller#convertAnother方法,调用com.thoughtworks.xstream.mapper.AnnotationMapper#defaultImplementationOf方法来寻找java.util.SortedSet类型的默认实现类型进行替换,替换为java.util.TreeSet类型。

接着调用com.thoughtworks.xstream.core.DefaultConverterLookup#lookupConverterForType方法来寻找TreeSet对应类型的转换器。跟进DefaultConverterLookup#lookupConverterForType方法方法,通过调用Converter#canConvert方法来判断该转换器是否能够转换出TreeSet类型,这里找到满足条件的TreeSetConverter转换器,接着调用typeToConverterMap#put方法将类型和转换器的对应关系放入Map表中,再返回转换器。

接着回到AbstractReferenceUnmarshaller#convert方法,调用getCurrentReferenceKey方法来获取当前的Reference键,即标签名,并将当前标签名压入parentStack栈中。

接着调用父类的convert方法,将类型压入栈,然后调用转换器TreeSetConverterunmarshal方法。

跟进TreeSetConverter#unmarshal方法,这里就回到了上文分析TreeSetConverter时的逻辑了,调用TreeMapConverter#populateTreeMap方法。

跟进TreeMapConverter#populateTreeMap方法,先判断是否是第一个元素,是的话调用putCurrentEntryIntoMap方法,跟进,调用readItem方法读取标签内容,并将当前内容缓存到Map中。

接着调用com.thoughtworks.xstream.io.xml.AbstractDocumentReader#moveUp方法,往下继续读取其他元素,然后调用populateMap方法。

跟进populateMap方法,调用populateCollection方法,用来循环遍历子标签中的元素并添加到集合中,先将动态代理标签添加进集合中,接着调用readItem方法读取标签内容,成功获取到了动态代理类并添加到Map中缓存起来。

调用完populateMap方法后,会判断JVM是否已充分将TreeMap都缓存起来了,然后调用TreeMap类的putAll方法,可看到参数中包含动态代理类,该代理类指向EventHandler类,而该类正如上文介绍时说的那样通过传入targetaction参数值来利用反射机制调用了ProcessBuilder(cmd).start()来执行任意命令。

由于动态代理类的机制,接着会调用到EventHandler#invoke方法,通过安全管理器获得权限来执行EventHandler#invokeInternal方法,在EventHandler#invokeInternal方法中,当获取到目标动态代理类对象的实际方法后,就直接通过反射机制调用,实现命令执行。

利用手法

除了上文提到的EventHandler#invokeInternal方法的反射机制调用实现命令执行,还有几种其它的利用姿势实现命令执行。

ConvertedClosure

在Groovy 2.4.3版本之前,MethodClosure方法支持被反序列化调用,可以利用MethodClosure方法封装需要执行的对象,例如new MethodClosure(Runtime.getRuntime(), “exec”);。

ConvertedClosure方法继承了InvocationHandler,会调用父类org.codehaus.groovy.runtime.ConversionHandler的invoke方法,接着再调用ConvertedClosure#invokeCustom方法,此时属性均可控,可以去调用去调用前面构造好的MethodClosure,结合TreeSetConverter实现命令执行。

Expando

在groovy.util.Expando#hashCode方法中,如果在类属性expandoProperties中存在hashCode:methodclosure的内容,便可以直接调用MethodClosurecall函数,跟上文ConvertedClosure后续的调用一样,但是这里调用时没有函数参数进来,因此这里的思路可以是ProcessBuilder.start或者fastjson那种getters的利用,结合Map类型触发hashCode实现命令执行。

ImageIO$ContainsFilter

在jdk.nashorn.internal.objects.NativeString#hashCode方法中会调用getStringValue方法,而getStringValue方法中的value类型为CharSequence。因此,接下来要找可以利用的CharSequence的实现类,这里用到的是com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#toString方法。

跟进com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#toString方法,先调用get方法,接着会去调用ByteArrayOutputStreamEx#readFrom方法

跟进ByteArrayOutputStreamEx#readFrom方法,这里的is是可控的,因为这里调用的this.dataHandler.getDataSource().getInputStream(),他的值传递都可以用类属性的方式把他构建出来。

1
2
3
4
5
1. this.dataHandler == 构造好的DataHandler
2. DataHandler的dataSource属性 == 构造好的XmlDataSource
3. XmlDataSource调用getInputStream()函数返回构造好的inputStream

// com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource

用这种方法就可以获取一个可控的inputStream,并且后续会继续调用javax.crypto.CipherInputStream#read方法,跟进javax.crypto.CipherInputStream#read方法,调用getMoreData方法,此时需要构造一个Cipher类型,并且后续调用Cipher.update方法,这里可以用javax.crypto.NullCipher来填充,因为最终用到的是父类Cipher.update,只要不重载update,其他的子类也可以。


跟进Cipher#update方法,最终调用到serviceIterator#next方法。

跟进javax.imageio.spi.FilterIterator#next方法,调用advance方法。

跟进advance方法,ImageIO存在一个有趣的filterjavax.imageio.ImageIO.ContainsFilter#filter,这里调用该filter方法,跟进发现可以指定一个Method对象去invoke,利用Java反射机制了,提前构造好method对象,就可以调用任意的函数。

ServiceFinder$LazyIterator

在上文ImageIO$ContainsFilter的构造方式的核心是在于可以去触发Iterator.next,因此寻找其它触发Iterator.next的方式也可以实现命令执行的目的,这里选择java.util.ServiceLoader.LazyIterator#next方法。

当类属性acc为空时,会去调用nextService函数,而在该函数里面,存在Class.forName的调用,并且去实例化的classname、loader,都是类属性,属于可以控制的东西,到了这里自然而然的就想到了使用BCEL的ClassLoader来载入classname里的字节码。

EXP

XStream的POC在官方的安全通告中都给出来了,https://x-stream.github.io/security.html