Java安全之RMI協議分析

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

Exit mobile version