Java RMI學習與解讀(二)
寫在前面
接上篇文章,這篇主要是跟着看下整個RMI過程中的源碼並對其做簡單的分析
RMI源碼分析
還是先回顧下RMI流程:
- 創建遠程對象接口(RemoteInterface)
- 創建遠程對象類(RemoteObject)實現遠程對象接口(RemoteInterface)並繼承UnicastRemoteObject類
- 創建Registry&Server端,一般Registry和Server都在同一端。
- 創建注冊中心(Registry)
LocateRegistry.getRegistry("ip", port);
- 創建Server端:主要是實例化遠程對象
- 注冊遠程對象:通過
Naming.bind(rmi://ip:port/name ,RemoteObject)
將name與遠程對象(RemoteObject)進行綁定
- 創建注冊中心(Registry)
- 遠程對象接口(RemoteInterface)應在Client/Registry/Server三個角色中都存在
- 創建Client端
- 獲取注冊中心
LocateRegistry.getRegistry('ip', prot)
- 通過
registry.lookup(name)
方法,依據別名查找遠程對象的引用並返回存根(Stub)
- 獲取注冊中心
- 通過存根(Stub)實現RMI(Remote Method Invocation)
創建遠程接口與遠程對象
在new RemoteObject的過程中主要做了這三件事
- 創建本地存根stub,用於客戶端(Client)訪問。
- 啟動 socket,監聽本地端口。
- Target注冊與查找。
先拋出一段RemoteInterface和RemoteObject的代碼
RemoteInterface
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteInterface extends Remote{
String doSomething(String thing) throws RemoteException;
String say() throws RemoteException;
String sayGoodbye() throws RemoteException;
String sayServerLoadClient(Object name) throws RemoteException;
Object sayClientLoadServer() throws RemoteException;
}
RemoteObject
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String doSomething(String thing) throws RemoteException {
return new String("Doing " + thing);
}
@Override
public String say() throws RemoteException {
return "This is the say Method";
}
@Override
public String sayGoodbye() throws RemoteException {
return "GoodBye RMI";
}
@Override
public String sayServerLoadClient(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public Object sayClientLoadServer() throws RemoteException {
return new ServerObject();
}
}
那么接下來看看我們之前提到的在代碼中必須要寫的一些內容
Remote
那么首先看創建遠程對象接口(RemoteInterface)部分,這個接口在上篇文章中提到過,需要繼承java.rmi.Remote
接口且該接口中聲明的方法要拋出RemoteException
異常,在Remote
接口的注釋中提到:
這個接口用於識別某些接口是否可以從非本地虛擬機調用方法,且遠程對象必須間接或直接的實現這個接口;也提到了我們之前說的"特殊的遠程接口",當一個接口繼承了
java.rmi.Remote
接口后,在該接口上聲明的方法才可以被遠程調用。
個人感覺有點像一個類似於序列化的標記式接口,用來標記這個接口的實現類是否可以被遠程調用該類中的方法。
RemoteException
異常類,注釋中說明了,任何一個繼承了java.rmi.Remote
的遠程接口,在其接口中的方法需要throws RemoteException異常,該異常是指遠程方法調用執行過程中可能發生的與通信相關的異常。
流程與代碼分析
用於使用JRMP導出遠程對象(export remote object)並獲取存根,通過存根與遠程對象進行通信
主要是構造方法和exportObject(Remote)
,這個點在Longofo師傅的文章有提到,當實現了遠程接口而沒有繼承UnicastRemoteObject
類的話需要自己調UnicastRemoteObject.exportObject(Remote)
方法導出遠程對象。
構造方法
/**
* Creates and exports a new UnicastRemoteObject object using an
* anonymous port.
* @throws RemoteException if failed to export object
* @since JDK1.1
*/
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}
exportObject(Remote)
/**
* Exports the remote object to make it available to receive incoming
* calls using an anonymous port.
* @param obj the remote object to be exported
* @return remote object stub
* @exception RemoteException if export fails
* @since JDK1.1
*/
public static RemoteStub exportObject(Remote obj)
throws RemoteException
{
/*
* Use UnicastServerRef constructor passing the boolean value true
* to indicate that only a generated stub class should be used. A
* generated stub class must be used instead of a dynamic proxy
* because the return value of this method is RemoteStub which a
* dynamic proxy class cannot extend.
*/
return (RemoteStub) exportObject(obj, new UnicastServerRef(true));
}
這兩個方法最終都會走向重載的exportObject(Remote obj, UnicastServerRef sref)
方法
初始化時會創建UnicastServerRef
對象並調用其exportObject
方法
在方法中會通過createProxy()
方法,創建RemoteObjectInvocationHandler處理器,給RemoteInterface接口創建動態代理
之后回到UnicastServerRef#exportObject
方法,new了一個Target對象,在該對象中封裝了遠程對象的相關信息,其中就包括stub屬性(一個動態代理對象,代理了我們定義的遠程接口)
之后調用liveRef的exportObject方法
接着調用sun.rmi.transport.tcp.TCPEndpoint#exportObject
方法(調用棧如下圖),最終調用的是TCPTransport#exportObject()
方法在該方法中開啟了監聽本地端口,並調用了Transport#exportObject()
在該方法中調用了ObjectTable.putTarget()
方法,將 Target 實例注冊到 ObjectTable 對象中。
而在ObjectTarget類中提供了兩種方式(getTarget的兩種重載方法)去查找注冊的Target,分別是參數為ObjectEndpoint
類型對象以及參數為Remote
類型的對象
回過頭看一下動態代理RemoteObjectInvocationHandler,繼承 RemoteObject 實現 InvocationHandler,因此這是一個可序列化的、可使用 RMI 遠程傳輸的動態代理類。主要是關注invoke方法,如果傳入的method對象所代表的類或接口的 class對象是Object.class就走invokeObjectMethod
否則走invokeRemoteMethod
在invokeRemoteMethod
方法中最終調用的是UnicastRef.invoke
方法,UnicastRef 的 invoke 方法是一個建立連接,執行調用,並讀取結果並反序列化的過程。反序列化在 unmarshalValue
調用readObject實現
如上就是在創建遠程接口並實例化遠程對象過程中的底層代碼運行的流程(多摻雜了一點動態代理部分),這里借一張時序圖。
建議各位師傅也是打個斷點跟一下比較好,對於整體在實例化遠程對象時的一個流程就比較清晰了。
創建注冊中心
創建注冊中心主要是Registry registry = LocateRegistry.createRegistry(1099);
打斷點debug進去,首先是實例化了一個RegistryImpl對象
進入有參構造,先new LiveRef對象,之后new UnicastServerRef對象並作為參數調用setup方法
setup方法中依舊調用UnicastServerRef#exportObject方法,對RegistryImpl對象進行導出;與上一次不同的是這次會直接走進if中創建stub,因為if判斷中調用了stubClassExists方法,該方法會判斷傳入的類是否在本地有xxx_stub
類。
而RegistryImpl顯然是有的,所以會走進createStub方法
該方法中反射拿到構造方法然后實例化RegistryImple_Stub類來創建代理類。
調用setSkeleton創建骨架
也是反射操作,實例化RegistryImple_Skel類
最終賦值給UnicastServerRef.skel屬性
在UnicastServerRef類中通過dispatch方法實現了對遠程對象方法的調用並將結果進行序列化並通過網絡傳到Client端
public void dispatch(Remote var1, RemoteCall var2) throws IOException {
try {
long var4;
ObjectInput var40;
try {
var40 = var2.getInputStream();
int var3 = var40.readInt();
if (var3 >= 0) {
if (this.skel != null) {
this.oldDispatch(var1, var2, var3);
return;
}
throw new UnmarshalException("skeleton class not found but required for client version");
}
var4 = var40.readLong();
} catch (Exception var36) {
throw new UnmarshalException("error unmarshalling call header", var36);
}
MarshalInputStream var39 = (MarshalInputStream)var40;
var39.skipDefaultResolveClass();
Method var8 = (Method)this.hashToMethod_Map.get(var4);
if (var8 == null) {
throw new UnmarshalException("unrecognized method hash: method not supported by remote object");
}
this.logCall(var1, var8);
Class[] var9 = var8.getParameterTypes();
Object[] var10 = new Object[var9.length];
try {
this.unmarshalCustomCallData(var40);
for(int var11 = 0; var11 < var9.length; ++var11) {
var10[var11] = unmarshalValue(var9[var11], var40);
}
} catch (IOException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} catch (ClassNotFoundException var34) {
throw new UnmarshalException("error unmarshalling arguments", var34);
} finally {
var2.releaseInputStream();
}
Object var41;
try {
var41 = var8.invoke(var1, var10);
} catch (InvocationTargetException var32) {
throw var32.getTargetException();
}
try {
ObjectOutput var12 = var2.getResultStream(true);
Class var13 = var8.getReturnType();
if (var13 != Void.TYPE) {
marshalValue(var13, var41, var12);
}
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
} catch (Throwable var37) {
Object var6 = var37;
this.logCallException(var37);
ObjectOutput var7 = var2.getResultStream(false);
if (var37 instanceof Error) {
var6 = new ServerError("Error occurred in server thread", (Error)var37);
} else if (var37 instanceof RemoteException) {
var6 = new ServerException("RemoteException occurred in server thread", (Exception)var37);
}
if (suppressStackTraces) {
clearStackTraces((Throwable)var6);
}
var7.writeObject(var6);
} finally {
var2.releaseInputStream();
var2.releaseOutputStream();
}
}
注冊中心與遠程服務對象注冊的大部分流程相同,差異在:
- 遠程服務對象使用動態代理,invoke 方法最終調用 UnicastRef 的 invoke 方法,注冊中心使用 RegistryImpl_Stub,同時還創建了 RegistryImpl_Skel
- 遠程對象默認隨機端口,注冊中心默認是 1099(當然也可以指定)
服務注冊
這部分其實就是Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
的實現
依舊是打斷點跟進去看下
進入 java.rmi.Naming#bind()
方法后先會解析處理我們傳入的url。先調用java.rmi#parseURL(name)
方法后進入intParseURL(String str)
方法。該方法內部先會對我們傳入的url(rmi://127.0.0.1:1099/Zh1z3ven)做一些諸如協議是否為rmi,是否格式存在問題等判斷,之后做了字符串的處理操作,分別獲取到我們傳入的url中的host(127.0.0.1)、port(1099)、name(Zh1z3ven)字段並作為參數傳入java.rmi.Naming
的內置類ParsedNamingURL的有參構造方法中去
也就是對該內置類中的屬性進行賦值操作
之后回到Naming#bind()
方法,將實例化的ParsedNamingURL
對象賦值給parsed
並作為參數帶入java.rmi.Naming#getRegistry
方法
最終進入getRegistry(String host, int port, RMIClientSocketFactory csf)
方法,調用棧如下,后續依舊是創建動態代理的操作。動態代理部分和創建遠程對象時操作差不多,就不再跟了
來看一下java.rmi.Naming#bind()
中最后一步,此時會調用RegistryImpl_Stub#bind
方法進行name與遠程對象的一個綁定。
方法內邏輯也比較清晰,獲取輸出流之后進行序列化的然后調用UnicastRef#invoke
方法
大致服務注冊,也就是name與遠程對象綁定就是這么一個邏輯,這里與su18師傅文章中不太一樣的點就是,我跟入的是第二個invoke方法,而su18師傅進入的是第一個invoke方法,這里就有些不解了,待研究。
總結
借一張su18師傅的圖。Server/Registry/Client三個角色兩兩之間的通信都會用到java原生的反序列化操作。也就是說我們有一端可控或可以偽造,那么傳入一段惡意的序列化數據直接就可以RCE。也就是三個角色都有不通的攻擊場景。
END
調試的時候深感吃力,RMI源碼其實我上面提到的可能還是有很多不清楚的地方。
其實只要自己打斷點debug跟一下,對於RMI的一個工作流程就很清晰了,有些點如果沒有剛需可以不用跟的很深入。
后面就是針對RMI的攻擊手法了,下篇更。