RMI协议
RMI协议
CH0icoRMI协议
01. RMI简介
RMI概述
RMI(Remote Method Invocation,远程方法调用)是 Java 提供的一种机制,允许运行在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。需要被远程调用的对象必须实现一个继承自 java.rmi.Remote 的接口。
在调用过程中,参数会被编组(marshalled)后从本地虚拟机发送到远程虚拟机,并在远程虚拟机上解组(unmarshalled)。方法执行结束后,结果(或异常)同样会被编组,从远程机发送回调用方虚拟机。通过 RMI,开发者可以像调用本地方法一样调用远程方法,屏蔽底层网络通信细节,实现跨 JVM 的透明操作。
RMI的应用价值
- 跨服务调用:例如 A 站点可以通过 RMI 调用 B 站点暴露的服务接口(如根据 id 查询用户信息),数据库连接无需直接暴露给多个客户端,降低安全风险。
- 避免重复开发:服务器上已有的一系列对象和调用逻辑,通过 RMI 只暴露需要被调用的方法,客户端无需将服务端实现下载到本地,安全又便捷。
02. 基础与演变
什么是RPC
RPC(Remote Procedure Call)即远程过程调用,是一种通过网络从远程计算机请求服务而不需要了解底层网络技术的协议。RPC 假定底层存在 TCP 或 UDP 这样的传输层协议,实现请求与响应的传输。RPC 使得分布式应用程序的开发更容易,采用客户机/服务器模式。
RMI 可以看作是 Java 语言对 RPC 的具体实现。使用 RMI 后,开发者不再需要直接处理底层 Socket 通信,只需关注业务接口。
RPC的演变历程
最初的分布式系统中,服务拆分为不同服务器上的接口与实现。为了调用远程方法,需要自己编写网络通信代码,每次调用都要处理 Socket 连接、序列化与反序列化,客户端与服务端代码混杂,维护困难。RPC 的演进正是逐步屏蔽这些底层细节的过程。
第一版:直接Socket通信
客户端通过 Socket 发送查询 id 的二进制数据,服务端接收后调用本地服务,再将结果写回。缺点:代码量大,业务逻辑与网络代码耦合,更换调用方法需要大幅改动。
第二版:抽离网络传输为Stub
将网络传输部分抽取出来形成 Stub(存根),客户端和服务端只需关注业务调用。但仍只能调用固定的方法,新增方法需要修改 Stub。
第三版:基于动态代理的RPC
利用 JDK 动态代理实现一个通用的 Stub,可以代理某个接口的所有方法。服务端通过反射根据方法名和参数类型调用对应的实现。这已经大幅提升了灵活性,但只能代理单一的接口。
第四版:通用化RPC框架雏形
Stub 可以接受任意接口的 Class 对象,通过动态代理生成代理实例。客户端只需要传入接口类,就能调用任何暴露的服务。服务端通过注册中心(HashMap)保存接口名与实现类的映射,根据客户端传来的接口名、方法名、参数反射调用并返回结果。这一版已经是一个基本可用的 RPC 框架。
RPC通信流程
- 客户端调用 Stub 中的方法。
- Client Stub 将方法名、参数等打包成网络消息发送给 Server Stub。
- Server Stub 接收消息并解包,根据信息找到对应的服务实现并调用。
- 服务端执行方法后将结果打包返回。
- Client Stub 收到结果并解包返回给客户端。
在此过程中,协议可以自定义,如 TCP、UDP、HTTP 等均可作为底层传输机制。
RPC与HTTP的对比
- 传输协议:RPC 较灵活,可以使用多种传输层协议;HTTP 基于 TCP。
- 传输效率:RPC 通常采用二进制传输,体积小,效率高;HTTP/1.1 请求中包含较多头部等无用内容,效率相对较低,但 HTTP/2.0 经过封装也可作为 RPC 的一种实现。
- 开发约束:RPC 需要双方约定相同的协议和序列化方式(如 Hessian、Dubbo),服务端通常需要提供接口代码;HTTP 则遵循 REST 等规范即可,更为通用。
03. 执行流程
编码示例与步骤
定义远程接口
远程接口必须继承 java.rmi.Remote,且接口中的每个方法都需要声明抛出 java.rmi.RemoteException,因为网络传输可能发生 I/O 异常。
1 | public interface Hello extends Remote { |
实现远程对象
实现类需要继承 java.rmi.server.UnicastRemoteObject,该类构造时会通过 JRMP(Java Remote Method Protocol)导出远程对象,并生成与远程对象通信的 Stub(存根)。Java 1.5 之后,Stub 通常通过动态代理在运行时生成,不再需要使用 rmic 静态生成。
1 | public class HelloImpl extends UnicastRemoteObject implements Hello { |
编写服务端
创建 Registry 并在指定端口监听,将远程对象绑定到一个 URL(如 rmi://localhost:1099/hello)。客户端即可通过该 URL 找到并调用服务。
1 | Hello hello = new HelloImpl(); |
编写客户端
客户端通过 Naming.lookup() 获取远程对象代理,之后即可像本地方法一样调用。
1 | Hello hello = (Hello) Naming.lookup("rmi://localhost:1099/hello"); |
交互流程详解
整个 RMI 调用过程涉及三个角色:
- RMI Registry:注册中心,负责维护名称与远程对象的绑定关系。运行在
1099端口(默认)。 - RMI Server:远程对象的真实宿主,实际执行方法的服务端。
- RMI Client:调用远程方法的客户端。
调用时序如下:
- 服务注册:Server 创建远程对象实例,并在 Registry 上绑定一个名称。
- Client 查找:Client 连接 Registry 并请求查找指定名称的对象,此时网络传输的是序列化后的对象信息。
- 获取远程引用:Registry 返回一个包含远程对象地址的序列化数据(通常是
java.lang.reflect.Proxy类型的动态代理对象),其中标记了 Server 的真实 IP 和端口(非 Registry 端口,是一个随机分配的端口)。 - 建立新连接:Client 反序列化得到远程引用后,发现其指向另一个地址和端口,于是新建 TCP 连接到该地址(即直接与 Server 通信)。
- 远程方法调用:Client 通过该连接发送方法调用请求(Call),Server 执行后将返回值(ReturnData)返回。
- 垃圾回收通知:Client 可能发送
DgcAck消息,向 Server 确认已接收返回的远程对象,以及进行分布式垃圾收集的交互。 - 心跳维持:Client 与 Server 之间通过
Ping消息保持连接。
整个过程中,Registry 只负责索引查找,真正的远程方法调用在 Client 与 Server 之间直接完成。
04. 流量分析
抓包环境及过滤
使用 rmi 过滤器可抓取 RMI 协议相关数据包。跟踪 TCP 流可以观察到完整的交互过程,包括 TCP 三次握手、JRMI 协议交互等。
协议协商阶段
- 客户端首先向 Registry 发送
StreamProtocol确认包,协商通信协议。 - Registry 返回确认包,包含所看到的客户端 hostname 和 port。
对象查找阶段
- 客户端向 Registry 发送 Call 消息,内部包含编码(序列化)的类名,请求查找远程对象。序列化数据以
0xACED开头,可用SerializationDumper解析,可以看到请求的接口名。 - Registry 返回
ReturnData,内容为java.lang.reflect.Proxy对象,序列化数据中包含远程 Server 的 IP 地址和端口(例如UnicastRef中指定了192.168.x.x和端口0x83E9)。 - 客户端从
ReturnData中解析出 Server 的真实通信端点。
远程调用阶段
- 客户端与 Server 新建连接,发送 Call 消息(JRMI Call Data),包含方法名、参数等序列化数据。
- Server 执行方法后返回响应包(ReturnData),包含执行结果。
- 双方可能会发送 Ping/Pong 维持连接,以及 DgcAck 进行垃圾收集确认。
- 调用结束后,TCP 连接断开。
05. 远程调用配置
若 Server 和 Client 不在同一主机,需要正确配置:
- 服务端指定对外 IP:在代码最开始处调用
System.setProperty("java.rmi.server.hostname", "服务器IP"),否则客户端接收到的远程引用中会包含内网地址或 localhost,导致无法连接。 - 如果客户端以命令行方式运行,且使用静态存根,需先用
rmic生成 Stub 类,并将 Stub 拷贝至客户端。在 IDE 中通常无需此步骤。 - 确保客户端能访问服务端的 Registry 端口(默认 1099)以及服务端导出远程对象的动态端口。
06. 攻击面分析
RMI 中的三个角色 R S C(Registry、Server、Client)在通信时均涉及序列化与反序列化,因此任何一个节点都可能攻击另外两个节点。
S攻击R
Server 通过 bind() 或 rebind() 向 Registry 注册远程对象时,对象以序列化方式发送,Registry 接收到后会自动反序列化。如果 Server 传入恶意对象,Registry 在反序列化时即可触发代码执行。这一攻击常发生在 Server 与 Registry 分离部署且 Registry 可被同一主机访问的场景。
验证代码:向 Registry 的 bind 传递一个包含恶意行为的对象,虽然可能出现类型转换异常,但命令仍会执行。
C攻击R
Client 与 Registry 的交互通常只传递字符串参数(如 lookup),但通过伪造请求可发送恶意序列化对象。在较低版本 JDK(如 7u10)中,Client 可直接向 Registry 发起伪造的 lookup 请求,将参数替换为恶意对象,Registry 反序列化时触发漏洞。示例利用反射获取 Registry Stub 的 UnicastRef 和 Operation,构造一个携带恶意对象的 Call,触发反序列化。
R攻击C/S
使用 ysoserial 的 JRMPListener 启动一个恶意的 RMI 监听器,任何客户端执行 lookup 或服务端执行 bind 时,只要连接至该恶意 Registry,均可能遭受反序列化攻击。攻击方通过返回恶意的序列化数据,在对方反序列化时执行代码。
S攻击C
如果服务端实现的方法返回一个恶意构造的对象,当客户端调用该方法并对返回值进行处理(如反序列化)时,就会触发攻击。例如,服务端 sayHello() 方法返回一个利用 CommonsCollections 链构造的恶意对象,客户端接收后即可触发命令执行。
C攻击S
客户端调用远程方法时,传入的参数会被序列化后发送给服务端。如果服务端方法接收 Object 参数,客户端可以传入恶意对象,服务端反序列化时即会被攻击。即使服务端方法签名定义为 String 等非 Object 类型,攻击者仍可通过对客户端代码进行字节码修改、RASP 挂钩或网络流代理替换等方式,将实参替换为恶意对象,绕过类型检查。
远程加载类(Codebase)攻击
codebase 是用于指定 Java RMI(远程方法调用)应用程序中类的字节码位置的属性。这个属性告诉 RMI 服务器和客户端在哪里可以找到所需的类文件。设置 codebase 有助于确保客户端能够动态加载服务器上不存在的类
在旧版 Java(低于 7u21、6u45)或手动设置 java.rmi.server.useCodebaseOnly=false 并配置了 SecurityManager 的情况下,RMI 支持通过 codebase 属性指定远程类加载地址。攻击者可以控制序列化数据中的 codebase,使服务端或客户端在反序列化时从恶意 HTTP/FTP 服务器加载并执行任意类。不过此攻击条件较为苛刻,实战中较少遇到。
07. 绕过JEP290过滤
JEP290简介
JEP290 是 Java 官方引入的序列化过滤机制,旨在防止反序列化漏洞。针对 RMI Registry 的过滤规则位于 sun.rmi.registry.RegistryImpl#registryFilter,维护了一个白名单,仅允许以下类型的反序列化:
StringNumber子类RemoteProxyUnicastRefRMIClientSocketFactoryRMIServerSocketFactoryActivationIDUID
默认情况下,数组深度不得超过 20,数组长度不超过 1000000。不符合白名单的类在反序列化时会被拒绝。
绕过方法
场景一:服务端方法参数为 Object
如果远程方法签名直接使用 Object 作为参数类型,则客户端传入的恶意对象不会被 Registry 过滤检查。因为实际反序列化发生在 Server 端,而非 Registry 端。只需让 Server 正常注册,Client 调用时传入恶意对象即可绕过 JEP290。这是最简单的绕过方式。
场景二:服务端方法参数为 String 等非 Object 类型
当方法参数类型为 String 或其他非 Object 类型时,客户端无法直接传入恶意对象。攻击者可通过以下方式将 String 实参替换为恶意对象:
- 修改
java.rmi包代码并重打包 - 使用调试器在序列化前替换对象
- 使用 Javassist 等工具修改字节码
- 在网络流中使用代理替换已序列化的对象
例如,借助 RASP 技术,hook java.rmi.server.RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法,在第三个参数(非 Object 类型)序列化时替换为恶意对象,从而实现在 Server 端的反序列化攻击。
08. 小结
RMI 是 Java 分布式架构中的基础技术,其通信过程基于 RPC 理念,通过 Registry 实现服务发现与远程对象透明调用。由于涉及 Java 原生序列化机制,RMI 在历史上引发了诸多反序列化安全漏洞。理解 RMI 的通信流程、各角色间的数据交换以及 JEP290 等防御机制,对于学习和防范 Java 反序列化攻击至关重要。后续章节将继续深入探讨 RMI 相关的漏洞利用与防御细节。












