RMI 架构:

img

RMI 底层通讯采用了 Stub(运行在客户端)Skeleton(运行在服务端) 机制, RMI 调用远程方法的大致如下:

  1. RMI客户端 在调用远程方法时会先创建 Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub 会将 Remote 对象传递给 远程引用层(java.rmi.server.RemoteRef) 并创建 java.rmi.server.RemoteCall(远程调用) 对象。
  3. RemoteCall 序列化 RMI服务名称Remote 对象。
  4. RMI客户端远程引用层 传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef) 收到请求会请求传递给 Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton 调用 RemoteCall 反序列化 RMI客户端 传过来的序列化。
  7. Skeleton 处理客户端请求: bindlistlookuprebindunbind ,如果是 lookup 则查找 RMI服务名 绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI客户端 反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端 调用远程方法, RMI服务端 反射调用 RMI服务实现类 的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端 反序列化 RMI 远程方法调用结果。

# RMI 远程方法调用测试

第一步我们需要先启动 RMI服务端 ,并注册服务。

RMI 服务端注册服务代码:

package com.anbai.sec.rmi;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServerTest {
   // RMI 服务器 IP 地址
   public static final String RMI_HOST = "127.0.0.1";
   // RMI 服务端口
   public static final int RMI_PORT = 9527;
   // RMI 服务名称
   public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
   public static void main(String[] args) {
      try {
         // 注册 RMI 端口
         LocateRegistry.createRegistry(RMI_PORT);
         // 绑定 Remote 对象
         Naming.bind(RMI_NAME, new RMITestImpl());
         System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

程序运行结果:

RMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test

Naming.bind(RMI_NAME, new RMITestImpl()) 绑定的是服务端的一个类实例, RMI客户端 需要有这个实例的接口代码 ( RMITestInterface.java ), RMI客户端 调用服务器端的 RMI服务 时会返回这个服务所绑定的对象引用, RMI客户端 可以通过该引用对象调用远程的服务实现类的方法并获取方法执行结果。

RMITestInterface 示例代码:

package com.anbai.sec.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
 * RMI 测试接口
 */
public interface RMITestInterface extends Remote {
   /**
    * RMI 测试方法
    *
    * @return 返回测试字符串
    */
   String test() throws RemoteException;
}

这个区别于普通的接口调用,这个接口在 RMI客户端 中没有实现代码,接口的实现代码在 RMI服务端

服务端 RMITestInterface 实现代码示例代码:

package com.anbai.sec.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
   private static final long serialVersionUID = 1L;
   protected RMITestImpl() throws RemoteException {
      super();
   }
   /**
    * RMI 测试方法
    *
    * @return 返回测试字符串
    */
   @Override
   public String test() throws RemoteException {
      return "Hello RMI~";
   }
}

RMI 客户端示例代码:

package com.anbai.sec.rmi;
import java.rmi.Naming;
import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;
public class RMIClientTest {
   public static void main(String[] args) {
      try {
         // 查找远程 RMI 服务
         RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);
         // 调用远程接口 RMITestInterface 类的 test 方法
         String result = rt.test();
         // 输出 RMI 方法调用结果
         System.out.println(result);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

程序运行结果:

Hello RMI~

在 Java RMI(远程方法调用)架构中,客户端调用远程对象的方法实际上是调用远程对象实现类的方法。为了更好地理解这个过程,我们需要了解 RMI 的基本工作原理。

# RMI 基本工作原理

  1. 定义远程接口:这个接口定义了客户端可以调用的方法。接口必须继承 java.rmi.Remote 接口,并且所有方法都必须声明 java.rmi.RemoteException
  2. 实现远程接口:创建一个类来实现这个远程接口,这个类通常会继承 java.rmi.server.UnicastRemoteObject ,以便能够导出远程对象,使其能够接收远程调用。
  3. 启动 RMI 注册表并注册远程对象:在服务器端,需要启动 RMI 注册表(通常通过 rmiregistry 命令)并将实现了远程接口的对象绑定到注册表中,这样客户端就可以通过名字来查找这个远程对象。
  4. 客户端查找并调用远程对象:客户端通过 Naming.lookup 方法来查找注册表中的远程对象,并通过接口引用来调用远程方法。

# 具体示例说明

假设有以下代码:

# 远程接口

java
复制代码
package com.anbai.sec.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMITestInterface extends Remote {
    String test() throws RemoteException;
}

# 远程接口实现类

java
复制代码
package com.anbai.sec.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {

    protected RMITestImpl() throws RemoteException {
        super();
    }

    @Override
    public String test() throws RemoteException {
        return "Hello from RMI Server!";
    }
}

# RMI 服务器

java
复制代码
package com.anbai.sec.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServerTest {
    public static final String RMI_NAME = "rmi://localhost:1099/RMITest";

    public static void main(String[] args) {
        try {
            // 启动RMI注册表
            LocateRegistry.createRegistry(1099);

            // 创建远程对象实例
            RMITestInterface rt = new RMITestImpl();

            // 将远程对象绑定到RMI注册表
            Naming.rebind(RMI_NAME, rt);

            System.out.println("RMI Server is ready.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

# RMI 客户端

java
复制代码
package com.anbai.sec.rmi;

import java.rmi.Naming;

import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;

public class RMIClientTest {

    public static void main(String[] args) {
        try {
            // 查找远程RMI服务
            RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);

            // 调用远程接口RMITestInterface类的test方法
            String result = rt.test();

            // 输出RMI方法调用结果
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

# 为什么客户端会调用实现类的方法

  1. 查找远程对象:客户端使用 Naming.lookup(RMI_NAME) 查找到注册在 RMI 注册表中的远程对象引用。这个引用实际上是一个代理对象(Stub),它实现了远程接口 RMITestInterface
  2. 调用远程方法:当客户端调用 rt.test() 方法时,这个调用被代理对象截获,并通过网络传送到服务器端。
  3. 服务器处理请求:服务器端的 RMI 框架接收到方法调用请求后,将其转发给实际的实现类 RMITestImpltest 方法。
  4. 返回结果:实现类的方法执行完毕后,结果通过 RMI 框架返回给客户端的代理对象,最终在客户端获得结果并输出。

在这个过程中,客户端实际上调用的是代理对象的方法,但由于代理对象会将调用转发给远程对象的实际实现类,所以最终执行的是实现类的方法。这就是为什么客户端会调用实现类的方法的原因。

# RMI 反序列化漏洞

RMI 通信中所有的对象都是通过 Java 序列化传输的,在学习 Java 序列化机制的时候我们讲到只要有 Java 对象反序列化操作就有可能有漏洞。

既然 RMI 使用了反序列化机制来传输 Remote 对象,那么可以通过构建一个恶意的 Remote 对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。

首先我们依旧使用上述 com.anbai.sec.rmi.RMIServerTest 的代码,创建一个 RMI 服务,然后我们来构建一个恶意的 Remote 对象并通过 bind 请求发送给服务端。

RMI 客户端反序列化攻击示例代码:

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

尘落 微信支付

微信支付

尘落 支付宝

支付宝

尘落 贝宝

贝宝