文章目錄
- 介紹
- 實現
- 服務端 Server
- 客戶端 Client
- 通信過程
- 數據端與注冊中心(1099 端口)建立通訊
- 客戶端與服務端建立 TCP 通訊
- 客戶端序列化傳輸 調用函數的輸入參數至服務端
- 總結
介紹
RMI 全稱 Remote Method Invocation(遠程方法調用),即在一個 JVM 中 Java 程序調用在另一個遠程 JVM 中運行的 Java 程序,這個遠程 JVM 既可以在同一臺實體機上,也可以在不同的實體機上,兩者之間通過網絡進行通信。
RMI的一般要用到的組件:
- Remote Interface:遠程接口
需要定義一個接口,繼承自 java.rmi.Remote
,表明可以被遠程對象調用的方法。
遠程調用可能發生網絡異常 , 所以每個方法都必須顯式拋出 RemoteException
- Remote Object Implementation:遠程接口的具體實現
一般需要繼承UnicastRemoteObject
類, 將對象導出成一個 可以通過 TCP 調用的遠程對象
- Server:服務端,注冊遠程對象到 RMI 注冊中心。
- Client:客戶端,查找遠程對象并調用其方法。
- Registry:注冊端提供服務注冊與服務獲取。即 Server 端向 Registry 注冊服務,比如地址、端口等一些信息,Client 端從 Registry 獲取遠程對象的一些信息,如地址、端口等,然后進行遠程調用。
實現
服務端 Server
定義遠程接口
package RMI.Server;import java.rmi.Remote;
import java.rmi.RemoteException;public interface Hello extends Remote {public String sayHello(String name) throws RemoteException;
}
遠程接口的實現
package RMI.Server;import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;public class HelloImpl extends UnicastRemoteObject implements Hello {public HelloImpl() throws RemoteException {super(); //也可以什么都不寫,隱式調用//如果沒有繼承UnicastRemoteObject,就需要手動導出: UnicastRemoteObject.exportObject(this, 0); }@Overridepublic String sayHello(String name) throws RemoteException {return "Hello " + name;}
}
服務端
主要是創建 RMI 注冊表(使用默認端口 1099),創建服務實現類的實例,將遠程對象綁定到注冊表中
package RMI.Server;import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;public class RMIServer {public static void main(String[] args) {try{//實例化遠程對象HelloImpl obj = new HelloImpl();//啟動本地的RMI注冊服務(一般默認 1099 端口),創建注冊中心LocateRegistry.createRegistry(1099);Registry registry = LocateRegistry.getRegistry();//綁定遠程對象registry.bind("HelloImpl", obj);//或者import java.rmi.Naming;//Naming.bind("rmi://127.0.0.1:1099/HelloImpl", obj);}catch (Exception e){e.printStackTrace();}}
}
客戶端 Client
連接到本地(localhost)的 RMI 注冊表然后查找相應名字的遠程對象,最后調用遠程方法,傳入相應參數
package RMI.Client;import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import RMI.Server.Hello; // 導入服務器端的遠程接口public class RMIClient {public static void main(String[] args) throws Exception {//連接到服務器Registry registry = LocateRegistry.getRegistry("localhost", 1099);//通過名字查找遠程對象Hello hello = (Hello) registry.lookup("HelloImpl");//調用遠程對象上面的方法String response = hello.sayHello("xpw");System.out.println("response :"+response);}}
先運行服務端, 再運行客戶端, 在客戶端就可以看到調用了遠程對象的方法了
通信過程
很多復制粘貼的來自其他師傅的博客,了解了一下內部通信的知識,還沒有動手去嘗試抓包
數據端與注冊中心(1099 端口)建立通訊
- 客戶端查詢需要調用的函數的遠程引用,注冊中心返回遠程引用和提供該服務的服務端 IP 與端口。
客戶端與注冊中心(1099 端口)建立通訊完成后,客戶端 向注冊中心發送了?個 “Call” 消息,注冊中心回復了?個 “ReturnData” 消息,然后客戶端新建了?個 TCP 連接,連到服務端的 33769 端?
AC ED 00 05
是常見的 Java 反序列化 16 進制特征
注意以上兩個關鍵步驟都是使用序列化語句
客戶端與服務端建立 TCP 通訊
客戶端發送遠程引用給服務端,服務端返回函數唯一標識符,來確認可以被調用
同樣使用序列化的傳輸形式
以上兩個過程對應的代碼是這兩句
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj"); // 查找遠程對象
這里會返回一個 Proxy 類型函數,這個 Proxy 類型函數會在我們后續的攻擊中用到。
客戶端序列化傳輸 調用函數的輸入參數至服務端
- 這一步的同時:服務端返回序列化的執行結果至客戶端
以上調用通訊過程對應的代碼是這一句
remoteObj.sayHello("hello");
可以看出所有的數據流都是使用序列化傳輸的,那必然在客戶端和服務帶都存在反序列化的語句。
總結
整個過程進?了兩次TCP握?,也就是我們實際建?了兩次 TCP連接。
第?次建?TCP連接是連接遠端 ip 的1099端?,這也是我們在代碼?看到的端?,? 者進?溝通后,我向遠端發送了?個“Call”消息,遠端回復了?個“ReturnData”消息,然后我新建了? 個TCP連接,連到遠端的33769端?。
之所以是33769端口, 因為在“ReturnData”這個包中,返回了?標的IP地址,其后跟的?個字節 \x00\x00\x83\xE9
,剛好就是整數 33769 的網絡序列
所以捋一下整個的過程: 首先客戶端連接Registry,并在其中尋找Name是HelloImpl的對象,這個對應數據流中的Call消息;然后Registry返回?個序列化的數據,這個就是找到的Name=HelloImpl的對象,這個對應數據流中的ReturnData消息;客戶端反序列化該對象,發現該對象是?個遠程對象,地址在 127.0.0.1:33769 ,于是再與這個地址建?TCP連接;在這個新的連接中,才執?真正遠程 ?法調?,也就是 HelloImpl()
各個元素之間的關系
RMI Registry就像?個?關,他??是不會執?遠程?法的,但RMI Server可以在上?注冊?個Name 到對象的綁定關系;RMI Client通過Name向RMI Registry查詢,得到這個綁定關系,然后再連接RMI Server;最后,遠程?法實際上在RMI Server上調?。
參考文章
代碼審計社區 Java安全漫談
https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/#Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B-RMI-%E4%B8%93%E9%A2%98-01-RMI-%E5%9F%BA%E7%A1%80
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/