回顾XStream反序列化漏洞

 

0x00 前言

XStream也是一款用的比较多的序列化组件,可以将object转化为XML并能完整的还原回来。他也曾经出现过反序列化漏洞,本文主要整理XStream相关的安全问题XD

 

0x01 基础知识

XStream的序列化和反序列化主要依靠toXML函数和fromXML函数,如下代码所示

Person person = new Person("tom", 18);
XStream xStream = new XStream();
String xml = xStream.toXML(person);// object to xml
System.out.println(xml);
System.out.println(xStream.fromXML(xml)); // xml to object
// 输出
// <objects.Person>
//   <name>tom</name>
//   <age>18</age>
// </objects.Person>
// objects.Person@369f73a2

关于XStream的fromXML分析看这篇文章,XStream会去调用不同的Converters来处理还原的过程。

XStream反序列化同fastjson这种不一样的地方是fastjson会在反序列化的时候主动去调用getters和setters,而XStream的反序列化过程中赋值都有Java的反射机制来完成,所以并没有这样主动调用的特性。

但是还有一种利用方式,回想一下,在几条常规的java反序列化利用链上,都利用了HashMapPriorityQueue等对象(key不可重复等特性)会自动去调用hashCodeequalcompareTo等这种函数。

以这种想法来看XStream反序列化,当我们对Map这种类型的对象进行还原的时候,是否也同样会去调用上面提到的几种函数?接下来,看几个Converter的处理:

1. MapConverter

来看看针对Map类型还原的Converter

com.thoughtworks.xstream.converters.collections.MapConverter#unmarshal

3562

populateMap函数会去处理后续的值,这里我们直接来看具体put的地方

com.thoughtworks.xstream.converters.collections.MapConverter#putCurrentEntryIntoMap

1807

这里target作为接收者,会调用Map的put函数,后续就是我们熟悉的对key调用hashCode函数

8889

2. TreeSet/TreeMapConverter

这里TreeSet和TreeMap一起讲,因为TreeSet本身就是一个只用上了Key的TreeMap;TreeSetConverter的反序列化处理也是先转化为TreeMapConverter的方式来优先还原TreeSet里的TreeMap,再填充到TreeSet里。

从TreeSetConverter来讲

4637

从treeset中取出field treemap后,去进一步调用TreeMapConverter来还原TreeMap

com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap

1953

这里先用soredMap来填充需要还原的Entry,后续将调用TreeMap.putAll

3714

最终会调用到java.util.AbstractMap#putAll

8176

这里的put函数为TreeMap.put,不看具体的代码了,他的主要功能就是填充数据,并且在填充时将会比较当前存在key,如果是相同的key,则替换原有老的值。这个过程会去调用key的compareTo函数

3. DynamicProxyConverter

还需要提及的是XStream还支持对动态代理的方式进行还原

2443

这里的还原过程不说了,我们主要的关注点是使用Proxy动态代理,我们可以扩展前面两种的自动调用函数的攻击面,下一章会举EventHandler的例子

 

0x02 现有几种利用分析

结合上面基础知识中提到的几个Converter,我们想要利用XStream反序列化漏洞的话,得去充分利用前面提到的几个会自动调用的函数

1. EventHandler

XStream反序列化用的最多的EventHandler,来看看他的invoke函数

5854

主要实现在invokeInternal函数内

首先需要判断此时调用的函数是否为hashCodeequalstoString,如果是的话,采用以下的方式来处理。

1155

但是我们需要利用的是invokeInternal函数后续的部分,所以我们利用的时候不能用它来调用上面的3个函数,意味着我前面提到的Map的方式,不适合用在这个地方;而TreeSet这种调用compareTo函数,可以用来继续往下走。

继续往下看

9510

后续的就是经典的java反射机制来实现函数调用,并且这里的target和action都是可控的。

这里需要注意一个问题是,这里action函数参数是有要求的(看452行到461行)

  1. 无参数
  2. 单个参数,且参数的类型为Comparable,并且这个action函数是可利用的

第2种还没有找到这样可利用的函数,这里的第一种可以提两种:

  • 配置好cmd的ProcessBuilder,action填start
  • 配置好rmi url的JdbcRowSetImpl,action填getDatabaseMetaData,这里可以举一反三,主要思路就是可利用的getters

现在再来看看具体的POC

<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>

2. Groovy ConvertedClosure

利用条件:groovy <= 2.4.3,在后续的版本里,MethodClosure不允许反序列化调用。

除了上面这种EventHandler的动态代理方式,Groovy的ConvertedClosure也同样可以达到这种效果

MethodClosure

当前MethodClosure的主要作用就是封装我们需要执行的对象,比如

new MethodClosure(Runtime.getRuntime(), "exec");

封装Runtime对象,并设定后续需要调用的函数exec

ConvertedClosure

这个ConvertedClosure也是继承了InvocationHandler,可以在动态代理中作为handler的存在,来看一下他的invoke

ConvertedClosure调用的是父类org.codehaus.groovy.runtime.ConversionHandler#invoke

6889

主要看这个部分,对于当前调用的函数,如果非Object的函数(如toString、hashCode等),并且不是GroovyObject的函数,会去调用子类的invokeCustom,这里看org.codehaus.groovy.runtime.ConvertedClosure#invokeCustom

0630

这里的属性都是可控的,也就意味着我们可以去调用去调用前面构造好的MethodClosure,这里后续调用call的过程可以看最近的这篇文章

所以为了满足能调用invokeCustom函数,前面的两种MapConverterTreeSetConverter,选哪种呢?

很明显答案是选择TreeSetConverter,因为Map会去调用hashCode,而TreeSet会去调用compareTo,这里的hashCode是Object的函数,过不了上面checkMethod,具体怎么写POC,不详细说了,见ysomap

PS:这里需要提一下的是由于compareTo会带上一个参数,所以我们MethodClosure封装的后续需要调用的函数必须要存在一个String类型的参数,不然会找不到函数报错。(可能还有其他的解决方法,这里我没继续深入下去了,直接构造Runtime.exec可以解决这个问题)

3. Groovy Expando

前面用到了TreeSet的方式,这里我们去使用Map的类型来触发。以Map的类型来触发,那就是找可以利用的hashCode函数

groovy.util.Expando#hashCode

4715

如果在类属性expandoProperties中存在hashCode:methodclosure的内容,我们可以在这里直接调用MethodClosurecall函数,跟上面ConvertedClosure后续的调用一样,但是这里调用时没有函数参数过来,所以这里的思路是ProcessBuilder.start或者fastjson那种getters的利用,见POC

4. ImageIO$ContainsFilter

上面使用动态代理的方式利用了TreeSet调用put时触发的compareTo,而这里利用的是HashMap类型put时调用的hashCode函数;这个链相对来说复杂一点,我们一点一点来说(参考marshalsec的ImageIO,这里先膜一波大佬的思路,后文为顺势讲下去的,主要目的是到达Iterator.next,实际挖掘这种链还是得从后往前找):

从XStream处理Map类型时触发hashCode开始

关注jdk.nashorn.internal.objects.NativeString#hashCode

0270

后续调用getStringValue函数,在这个函数里去调用了this.value.toString(),这里的value的类型为CharSequence,所以我们接下来要找可以利用的CharSequence的实现类,这里用到的是com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#toString函数

0572

这里紧接着会去调用ByteArrayOutputStreamExreadFrom,这个函数用到的主要是这边传入的InputStream的read函数

2727

实际上is我们是可以控制的,因为这里调用的this.dataHandler.getDataSource().getInputStream(),他的值传递都可以用类属性的方式把他构建出来,分别是

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

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

用这种方法就可以获取一个可控的inputStream,并且后续会去调用read函数

继续看下去,javax.crypto.CipherInputStream#read

0515

1566

此时需要构造一个Cipher类型,并且后续调用Cipher.update函数,这里可以用javax.crypto.NullCipher来填充,因为最终用到的是父类Cipher.update,只要不重载update,其他的子类也可以。

继续看Cipher.update

4216

1157

说了那么久,我们终于到了至关重要的一个地方,serviceIterator.next函数

后续我们将调用ImageIO下的javax.imageio.spi.FilterIterator#next

6844

5068

advance函数会去调用filter.filter函数,而ImageIO存在一个有趣的filter

javax.imageio.ImageIO.ContainsFilter#filter

7622

我们可以指定一个Method对象去invoke,到了这里就是激动人心的Java反射机制了,我们提前构造好method对象,就可以调用任意的函数。

利用链比较长,整理一下过程

XStream 处理Map类型 去调用jdk.nashorn.internal.objects.NativeString#hashCode
    com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#toString
    javax.activation.DataHandler#getDataSource
        com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource#getInputStream
            javax.crypto.CipherInputStream#read -> getMoreData
            javax.crypto.NullCipher#update -> chooseFirstProvider
                javax.imageio.spi.FilterIterator#next
                    javax.imageio.ImageIO.ContainsFilter#filter
                        ProcessBuilder#start

从后面往前看的话,我们前面做的所有操作都是为了能去触发Iterator.next,而这种Iterator的遍历处理,我们很容易再找到一处,下一节就是不用javax.imageio.ImageIO.ContainsFilter#filter来实现利用,请继续往下看XD

5. ServiceFinder$LazyIterator

思路来自文章1文章2

先来看一下java.util.ServiceLoader.LazyIterator#next

5804

4625

当类属性acc为空时,会去调用nextService函数,而在该函数里面,我们看到了令人熟悉的Class.forName的调用。并且我们去实例化的classnameloader,都是类属性,属于我们可以控制的东西。

到了这里自然而然的就想到了使用BCEL的ClassLoader来载入classname里的字节码了(这里我在fastjson那篇里提到过)。

所以我们可以在ImageIO那条链的基础上,在触发Iterator.next时使用这个LazyIterator来代替

修改BCEL ClassLoader构造POC

这里来提一下关于POC的构造,如果你使用了当前这个利用链,并且不对ClassLoader做处理的话,你会发现怎么都打不通,因为这里在实际还原ClassLoader的时候出现了错误

6222

这里有两种解决方案,一是去除这种还原有问题的类(会很麻烦),二是直接把ClassLoader里的一些无关紧要的东西剔除掉。

这里我选择了第二种,经过调试去除了以下几个属性的值

3428

这里由于我们剔除了ignored_packagesdeferTo,导致BCEL的ClassLoader在载入普通的类的时候会出现加载错误的问题

0161

来看看怎么解决这个问题

首先BCEL的ClassLoader.loadClass,一共尝试4次不同的载入方法

  1. 从当前ClassLoader的classes去找
    8446
  2. 对于默认忽略的包java./sun./javax.,使用deferTo去重新加载,这里的deferTo是系统的ClassLoader(ClassLoader.getSystemClassLoader())
    0882
  3. 对于classname以BCEL开头的,根据classname的值去defineClass,这边就是我们最喜欢的任意载入字节码的地方
    3866
  4. 最后一次是用repository去载入当前的classname,如果这里还没找到,就会爆没有找到Class的错误
    1954PS:这部分repository取的SyntheticRepository.getInstance(),还不是很清楚这个左右,后续整理一下ClassLoader相关的知识再做补充

再来看我们报错的原因,因为删除ignored_packagesdeferTo之后,相当于第二种情况无法载入了,而显然java.lang.Object不符合第三种情况。最后第4种里面也没有找到这个java.lang.Object,所以最终爆了ClassNotFoundException

这里其实已经很明显了,解决这个问题,我们得在classes里添加我们传入的class字节码里所用到的所有类,那么在第一次尝试载入的时候,就找到了相应的类,无需尝试后续的几种载入方式。

比如这里我产生的字节码里面用上了Runtime,就得加上这个类

7924

这里的Object必须加上,毕竟所有的对象都继承自Object

 

0x03 XStream的防御措施


XStream在1.4.7版本之后支持使用白名单和黑名单的方式来方式恶意类的反序列化security

Starting with XStream 1.4.7, it is possible to define permissions for types, to check the type of an object that should be unmarshalled. Those permissions can be used to allow or deny types explicitly With these permissions it is at least not possible to inject unexpected types into an object graph. The security framework supports the setup of a black or white listing scenario. Any application should use this feature to limit the danger of arbitrary command execution if it deserializes data from an external source.

XStream itself sets up a black list by default, i.e. it blocks all currently known critical classes of the Java runtime. Main reason for the black list is compatibility, because otherwise newer versions of XStream 1.4.x can no longer be used as drop-in replacement. Unfortunately this provides a false sense of security. Every XStream client should therefore switch to a white listing on its own as soon as possible. XStream itself will use white listing as default starting with 1.5.x and only clients that have also changed their setup will be able to use this newer version again as drop-in replacement.

这里主要看一下黑名单的处理

1.4.7-1.4.9

EventHandler的处理由ReflectionConverter来处理的,在1.4.7-1.4.9版本,在canConvert处添加了对EventHandler的限制

6291

所以EventHandler的POC就失效了,但是其他的几种并没有失效

>=1.4.10

在1.4.10版本增加了setupDefaultSecurity方式来设置默认的白名单,但是这个版本把上面EventHandler的限制去掉了,导致又可以使用最早的POC,需要注意的是这是没修补前的1.4.10,修复后已经不可以了

除了新增设置白名单的方式,也新增加了InternalBlackList这个converter,他设置的权限为LOW,而ReflectionConverter权限为Very_low,所以会先过一次黑名单检查(XStream在注册converters时,以权限的方式来决定次序)。

7552

所以这里1,4,5都跪了,只剩下groovy这种了,当然肯定还有其他没有发现的利用链,所以最安全的方法还是使用白名单的方式,不能依赖XStream的黑名单来做安全防御。

 

0x04 总结

本文主要回顾了现有的一些利用链,值得注意的是:

如果XStream用的不是白名单模式,还是存在利用的可能性的。现有内置的黑名单只禁止了几个现有的利用链,我们还是可以找到其他可以利用的利用链的,比如前面提到的Groovy的利用链。

需要记住的是XStream他的触发方式依赖的是HashMap、TreeSet这种类型自动调用的hashCodecompareTo串起来的,后续可以注意一下这种可能的调用链。

PS:本文提到的所有POC,已经更新到GitHub

(完)