0x01基本概念
RMI的全稱是Rmote Method Invocation,即遠程方法調用,具體怎麼實現呢?遠程服務器提供具體的類和方法,本地會通過某種方式獲得遠程類的一個代理,然後通過這個代理調用遠程對象的方法,方法的參數是通過序列化與反序列化的方式傳遞的,所以,1. 只要服務端的對象提供了一個方法,這個方法接收的是一個Object類型的參數,2. 且遠程服務器的classpath中存在可利用pop鏈,那麼我們就可以通過在客戶端調用這個方法,並傳遞一個精心構造的對象的方式來攻擊rmi服務。
0x02 實現機制
上面說了本地會通過某種方式獲得遠程對象的代理,那麼具體是怎麼的實現機制呢?RMI模式中除了有Client與Server,還藉助了一個Registry(註冊中心)。
Server | Registry | Client |
---|---|---|
提供具體的遠程對象 | 一個註冊表,存放着遠程對象的位置(ip、端口、標識符) | 遠程對象的使用者 |
其中Server與Registry可以在同一服務器上實現,也可以佈置在不同服務器上,現在一個完整的RMI流程可以大概描述爲:
- Registry先啓動,並監聽一個端口,一般爲1099
- Server向Registry註冊遠程對象
- Client從Registry獲得遠程對象的代理(這個代理知道遠程對象的在網絡中的具體位置:ip、端口、標識符),然後Client通過這個代理調用遠程方法,Server也是有一個代理的,Server端的代理會收到Client端的調用的方法、參數等,然後代理執行對應方法,並將結果通過網絡返回給Client。
兩圖勝千言:
整體流程:
ps: 圖中的stub就是客戶端代理,skeleton就是服務端代理,老外起的這英文名字我實在是理解不了~
遠程方法調用的通信模式:
不知道有沒有人和我一樣想過爲什麼需要這個註冊表?
0x03 代碼實現
我們已經知道了大體的流程了,那麼用代碼如何實現上述流程呢?我們自己動手創建一個項目吧,項目結構如下:
1.首先創建一個接口Hello:
package model;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
public String welcome(String name) throws RemoteException;
}
2.基於這個接口實現一個類Helloimpl:
package model.impl;
import model.Hello;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Helloimpl extends UnicastRemoteObject implements Hello {
public Helloimpl() throws RemoteException {
}
@Override
public String welcome(String name) throws RemoteException {
return "Hello, "+name;
}
}
3.創建服務端,服務端創建了一個註冊表,並註冊了客戶端需要的對象
package server;
import model.Hello;
import model.impl.Helloimpl;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws RemoteException{
// 創建對象
Hello hello = new Helloimpl();
// 創建註冊表
Registry registry = LocateRegistry.createRegistry(1099);
// 綁定對象到註冊表,並給他取名爲hello
registry.rebind("hello", hello);
}
}
4.客戶端調用遠程對象
package client;
import model.Hello;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 獲取到註冊表的代理
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 利用註冊表的代理去查詢遠程註冊表中名爲hello的對象
Hello hello = (Hello) registry.lookup("hello");
// 調用遠程方法
System.out.println(hello.welcome("axin"));
}
}
先啓動服務端,在啓動客戶端,客戶端成功執行遠程方法並獲得返回的數據:
在寫代碼的時候有幾點需要注意:
- 接口需要集成Remote接口,且方法需要拋出RemoteException錯誤
- 接口的實現類需要繼承UnicastRemoteObject,同樣的方法需要拋出RemoteException錯誤
- 如果遠程方法需要傳參,需要保證參數是可序列化的,我這裏傳參只是傳了字符串,字符串是可序列化的,如果傳參是自定義的對象,那麼這個對象需要實現Serilizable接口
注意一點,由於我這裏服務端與客戶端都是在一臺機器上實現的,所以看起來比較簡單,如果服務端與客戶端不在同一主機,需要保證調用的遠程對象實現的那個接口在客戶端與服務端都存在!
0x04 其他
到這裏就差不多了,暫時滿足我們後續學習rmi反序列化漏洞的需要,如果好奇rmi底層代碼實現,可以再去讀一下jdk的源碼,這樣會加深你對rmi的理解~
放一篇從源碼層面解析rmi實現的文章:https://xz.aliyun.com/t/2223
看文章的同時,最好結合着實踐
關注我的公衆號,一起玩耍