Java RMI學習與解讀(二)

CoLoo發表於2021-10-29

Java RMI學習與解讀(二)

寫在前面

接上篇文章,這篇主要是跟著看下整個RMI過程中的原始碼並對其做簡單的分析

RMI原始碼分析

還是先回顧下RMI流程:

  1. 建立遠端物件介面(RemoteInterface)
  2. 建立遠端物件類(RemoteObject)實現遠端物件介面(RemoteInterface)並繼承UnicastRemoteObject類
  3. 建立Registry&Server端,一般Registry和Server都在同一端。
    • 建立註冊中心(Registry)LocateRegistry.getRegistry("ip", port);
    • 建立Server端:主要是例項化遠端物件
    • 註冊遠端物件:通過Naming.bind(rmi://ip:port/name ,RemoteObject) 將name與遠端物件(RemoteObject)進行繫結
  4. 遠端物件介面(RemoteInterface)應在Client/Registry/Server三個角色中都存在
  5. 建立Client端
    • 獲取註冊中心LocateRegistry.getRegistry('ip', prot)
    • 通過registry.lookup(name) 方法,依據別名查詢遠端物件的引用並返回存根(Stub)
  6. 通過存根(Stub)實現RMI(Remote Method Invocation)

建立遠端介面與遠端物件

在new RemoteObject的過程中主要做了這三件事

  1. 建立本地存根stub,用於客戶端(Client)訪問。
  2. 啟動 socket,監聽本地埠。
  3. 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的攻擊手法了,下篇更。

相關文章