Java安全之RMI協議分析

nice_0e3發表於2021-01-15

Java安全之RMI協議分析

0x00 前言

在前面其實有講到過RMI,但是隻是簡單描述了一下RMI反序列化漏洞的利用。但是RMI底層的實現以及原理等方面並沒有去涉及到,以及RMI的各種攻擊方式。在其他師傅們的文章中發現RMI的攻擊方式很多。 所以在此去對RMI的底層做一個分析,後面再去對各種攻擊方式去做一個瞭解。

0x01 底層協議概述

RPC

RPC(Remote Procedure Call)—遠端過程呼叫,它是一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,為通訊程式之間攜帶資訊資料。在OSI網路通訊模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網路分散式多程式在內的應用程式更加容易。RPC採用客戶機/伺服器模式。請求程式就是一個客戶機,而服務提供程式就是一個伺服器。

小總結:

在最原始資料通訊中其實還是歸根到TCP/UDP協議,但是自使用了RPC後可以不需要了解底層網路協議,但是底層還是通過TCP/UDP去進行網路呼叫的。 其實RPC也只是遠端方法呼叫的統稱,重點在於方法呼叫中。而RMI實現就是Java版的一個RPC實現。

JMX

JMS:Java 訊息服務(Java Messaging Service) 是一種允許應用程式建立、傳送、接受和讀取訊息的Java API。JMS 在其中扮演的角色與JDBC 很相似, JDBC 提供了一套用於訪問各種不同關聯式資料庫的公共APIJMS 也提供了獨立於特定廠商的企業訊息系統訪問方式

JMS 的程式設計過程很簡單,概括為:應用程式A 傳送一條訊息到訊息伺服器(也就是JMS Provider)的某個目的地(Destination),然後訊息伺服器把訊息轉發給應用程式B。因為應用程式A 和應用程式B 沒有直接的程式碼關連。

RPC 和RMI的區別

RPC(Remote Procedure Call Protocol)遠端過程呼叫協議,它是一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。RPC不依賴於具體的網路傳輸協議,tcp、udp等都可以。

RPC是跨語言的通訊標準。

RMI可以被看作SUN對RPC的Java版本的實現,當然也還有其他的RPC

微軟的DCOM就是建立在ORPC協議之上實現的RPC。

RMI集合了Java序列化Java遠端方法協議(Java Remote Method Protocol)。這裡的Java遠端方法協議則是JRMP。

RMI和JMS的區別

傳輸方式

  • JMS 與 RMI 的區別在於:採用 JMS 服務,物件是在物理上被非同步從網路的某個 JVM 上直接移動到另一個 JVM 上。
  • RMI 物件是繫結在本地 JVM 中,只有函式引數和返回值是通過網路傳送的。

方法呼叫

  • RMI 一般都是同步的,也就是說,當client端呼叫Server端的一個方法的時候,需要等到對方的返回,才能繼續執行client端,這個過程跟呼叫本地方法感覺上是一樣的,這也是RMI的一個特點。
  • JMS 一般只是一個點發出一個Message(訊息)到Message Server端,發出之後一般不會關心誰用了這個message(訊息)。
  • 一般RMI的應用是緊耦合,JMS的應用相對來說是鬆散耦合的應用。

本段取自RMI與RPC的區別

由此得知其實RMI協議是傳送方法以及方法引數,請求到server端後,server進行執行後返回結果給client端。

0x02 RMI底層架構

底層架構概念

根據上面內容其實可以總結為一句話,RPC是為了隱藏網路通訊過程中的細節,方便使用。而在RMI中也是一樣的。RMI中為了隱藏網路通訊的過程細節採用了動態代理的方式來進行實現。

下面先來看到呼叫流程圖

在客戶端和伺服器各有一個代理,客戶端的代理叫Stub(存根),服務端的代理叫Skeleton(骨架),合在一起形成了 RMI 構架協議,負責網路通訊相關的功能。代理都是由服務端產生的,客戶端的代理是在服務端產生後動態載入過去的。

stub擔當遠端物件的客戶本地代表或代理人角色,負責把要呼叫的遠端物件方法的方法名及其引數編組打包,並將該包轉發給遠端物件所在的伺服器。

Stub編碼後傳送的資料包內容,包含如下內容:

1. 被使用的遠端物件的識別符號
2. 被呼叫的方法的描述
3. 編組後的引數

Skeleton接收到Stub傳送資料會執行如下操作:

1. 從資料包中定位要呼叫的遠端物件
2. 呼叫所需的方法,並傳遞客戶端提供的引數
3. 捕獲返回值或呼叫產生的異常。
4. 將打包返回值編組,返回給客戶端Stub

本段內容部分取自:RMI反序列化漏洞分析

總結大體的內容如下:

客戶端(Client):服務呼叫方。
    
客戶端存根(Client Stub):存放服務端地址資訊,將客戶端的請求引數資料資訊打包成網路訊息,再通過網路傳輸傳送給服務端。
    
服務端存根(Server Stub):接收客戶端傳送過來的請求訊息並進行解包,然後再呼叫本地服務進行處理。
    
服務端(Server):服務的真正提供者。

這裡值得注意的一點是前面說到的傳輸進行打包和解包的步驟其實就是序列化和反序列化,傳輸的是序列化的資料。

架構呼叫流程

RMI大致的遠端呼叫執行流程:

1. 客戶端發起請求,請求轉交至RMI客戶端的stub類;

2. stub類將請求的介面、方法、引數等資訊進行序列化;

3. 基於socket將序列化後的流傳輸至伺服器端;

4. 伺服器端接收到流後轉發至相應的Skeleton類;

5. Skeleton類將請求的資訊反序列化後呼叫實際的處理類;

6. 處理類處理完畢後將結果返回給Skeleton類;

7. Skelton類將結果序列化,通過socket將流傳送給客戶端的stub;

8. stub在接收到流後反序列化,將反序列化後的Java Object返回給呼叫者。

socket層中執行流程

  1. server在遠端機器上監聽一個埠,這個埠是jvm或者os在執行時隨機選擇的一個埠。可以說server在遠端機器上在這個埠上匯出自己。

  2. client並不知道server在哪,以及sever監聽哪個埠,但是他有stub。stub知道所有這些東西,這樣client可以呼叫stub上他想呼叫的任何方法。

  3. client呼叫給你stub上的方法

  4. stub連結server監聽的埠併傳送引數,詳細過程如下:

    4.1 client連線server監聽的埠
    4.2 server收到請求並建立一個socket來處理這個連線
    4.3 server繼續監聽到來的請求
    4.4 使用雙方協定的歇息,傳送引數和結果
    4.5 協議可以是JRMP或者 iiop

  5. 方法在遠端server上執行,併發執行結果返回給stub

  6. stub返回結果給client,就好像是stub執行了這個方法一樣。

其實也是一樣對於了下面的這張圖

但是第二部分內容是值得思索的一點,為什麼client不知道server的監聽埠和server在哪,而stub卻知道呢?client不知道server的host和port的話,stub是如何建立一個知道所有這一切的stub物件呢?

這時候就引出了RMIRegistry(註冊中心)的作用了。

RMIRegistry作用

RMIRegistry 可以認為是一個服務,它提供了一個hashmap,裡面是 public_name, Stub_object 名值對。比如有一個遠端服務物件叫做 Scientific_Calculator,然後想把這個服務對外公佈為 calc,這樣會在server上建立一個stub物件,把他註冊到RMIRegistry ,這樣client就可以從RMIRegistry 中得到這個stub物件了,可以使用一個工具類java.rmi.Naming來方便的操作註冊和操作。

實現可看Java安全之RMI反序列化該篇文章。

總結:

簡單來說就是使用java.rmi.Naming將一個某一個類註冊進RMIRegistry 裡面,註冊後會在server端建立stub物件,而client就可以從RMIRegistry 中得到這個stub物件。前面內容說到過代理都是由服務端產生的,客戶端的代理是在服務端產生後動態載入過去的。RMIRegistry執行在server端當中。

RMIRegistry 執行流程詳解

  1. 首先RMIRegistry 執行在server端,RMIRegistry 自身也是一個遠端物件。有一點需要注意的是:所有的遠端物件(繼承了UnicastRemoteObject物件)都會在sever上任意的埠匯出自己,因為RMIRegistry 也是一個遠端物件,他也在server上匯出自己,這個埠是1099。

  2. 服務端執行在server上,在UnicastRemoteObject建構函式裡面,他把自己匯出在server上一個任意埠上,這個埠client是不知道的

  3. 當你呼叫Naming.rebind()的時候,會傳入一個CalcImpl 的引用(這裡叫做 c)作為第2個引數,Naming 就會構造一個stub物件,詳細如下:
    a. Naming會使用getClass來獲取類的名字,這裡就是CalcImpl
    b. 加上字尾_Stub 變成了 CalcImpl _Stub
    c. 載入CalcImpl_Stub.class到虛擬機器中
    d. 從c中獲取RemoteRef 物件

    e. 就是這個RemoteRef 物件中封裝了服務端細節,包括服務端的hostname、port
    f. 獲取了RemoteRef 物件之後,就可以構造stub物件了。
    g. 傳遞stud物件到RMIRegistry中進行繫結,即(publicname,stub)
    f. RMIRegistry 中內部使用一個hashmap來儲存(publicname,stub)

  4. 當客戶端使用 Naming.lookup()的時候,會傳入public name 作為引數,RMIRegistry 就會返回stub給客戶端呼叫

這裡作者說的匯出自己這個沒太理解這個的意思,我的理解是對映,將內容對映到1099埠中。

總結:

總體來說就是所以的遠端物件都會在server的任意的埠上對映,RMIRegistry 也會進行對映,但是RMIRegistry 對映的埠是1099(預設是,可以修改)。遠端物件進行對映的埠,client是不知道的。但是stub物件會知道。

在呼叫Naming.rebind()並並且傳入某一個引用的時候,Naming 就會構造一個stub物件。Naming內部採用getClass來獲取類的名字,並且新增_Stub字尾後價值到虛擬機器中,然後從引用中獲取 RemoteRef 物件,該物件就封裝了封裝了服務端的細節。而獲取到該RemoteRef 就可以構造stub物件了,構造完成後傳遞到RMIRegistry註冊中心中進行繫結,內部採用hashmap鍵值對的方式,即(publicname,stub),這時候使用Naming.lookup()傳入對應的方法名,則會返回對應的stub到client端。

RMIRegistry 存在的意義只是為了方便client獲取到stub物件,stub建構函式中需要一個RemoteRef 物件,這個物件只能在server端獲取。

本段內容部分摘取自:深入理解rmi原理

0x03 結尾

簡單的分析了一下RMI底層的架構,但是這些其實都僅僅是基於概念和理論層面的,具體的程式碼實現其實還沒去看。在其中也是看得暈頭轉向的,部分也摘取了其他師傅們的文章內容,感覺已經總結很到位了,瘋狂安利。摘取的內容下也貼出來 摘取內容的出處,感謝各位師傅們的詳細講解。

相關文章