深入理解 RMI 之运行逻辑与漏洞原理
字数 2245 2025-08-12 11:34:05
深入理解RMI:运行逻辑与漏洞原理
1. RMI基础概念
1.1 RMI简介
RMI(Remote Method Invocation,远程方法调用)是Java中用于实现分布式应用的技术,允许一个JVM中的Java程序调用另一个远程JVM中的Java程序。RMI依赖JRMP(Java Remote Message Protocol)协议进行通信,该协议为Java定制,要求服务端与客户端都为Java编写。
1.2 RMI核心组件
- Server(服务端):通过绑定远程对象,封装网络操作
- Client(客户端):调用服务端的方法
- Registry(注册中心):提供服务注册与服务获取
2. RMI实现机制
2.1 服务端实现
- 定义远程接口:
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
要求:
- 作用域为public
- 继承Remote接口
- 接口方法抛出RemoteException
- 实现远程接口:
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
public RemoteObjImpl() throws RemoteException {
// 如果不能继承UnicastRemoteObject就需要手工导出
// UnicastRemoteObject.exportObject(this, 0);
}
@Override
public String sayHello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
要求:
- 实现远程接口
- 继承UnicastRemoteObject类(用于生成Stub和Skeleton)
- 构造函数抛出RemoteException
- 实现类中使用的对象必须都可序列化
- 注册远程对象:
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 实例化远程对象
RemoteObj remoteObj = new RemoteObjImpl();
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定对象示例到注册中心
registry.bind("remoteObj", remoteObj);
}
}
2.2 客户端实现
- 定义远程接口(与服务端相同)
- 获取远程对象并调用方法:
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("hello");
}
}
3. RMI通信原理
3.1 通信流程
- 客户端与注册中心(1099端口)建立连接
- 客户端查询需要调用的函数的远程引用
- 注册中心返回远程引用和提供该服务的服务端IP与端口
- 客户端新起一个端口与服务端建立TCP通讯
- 客户端发送远程引用给服务端
- 服务端返回函数唯一标识符确认可被调用
- 客户端序列化传输调用函数的输入参数至服务端
- 服务端返回序列化的执行结果至客户端
3.2 通信特点
- 实际建立两次TCP连接:
- 第一次:客户端连接Registry的1099端口
- 第二次:服务端发送给客户端的连接
- 所有数据流都使用序列化传输
- Registry作为网关,不执行远程方法,只维护绑定关系
4. RMI运行机制深入分析
4.1 创建远程服务
-
发布远程对象:
- 通过
exportObject()方法发布 - 端口为0时发布到随机端口
- 创建
LiveRef对象处理网络引用 - 创建
Stub(存根)和Skeleton(骨架)
- 通过
-
发布完成记录:
- 信息保存到静态的HashMap中
- 通过
ObjectTable.putTarget()存储封装数据
4.2 创建注册中心
- 通过
createRegistry()方法创建 - 创建
RegistryImpl对象 - 安全检查(端口是否为1099,是否开启SecurityManager)
- 创建
LiveRef和UnicastServerRef - 创建
Stub和Skeleton:- 通过反射创建
RegistryImpl_Stub - 通过
forName()创建Skeleton
- 通过反射创建
4.3 绑定过程
- 检查是否为本地绑定
- 检查
bindings中是否已有数据 bindings.put(name, obj)将IP和端口放入HashTable
4.4 客户端请求处理
-
获取注册中心:
- 创建
LiveRef - 通过
Util.createProxy()创建代理
- 创建
-
查找远程对象:
- 通过
invoke()方法激活 call.executeCall()处理网络请求- 存在反序列化漏洞点
- 通过
-
客户端请求服务端:
- 通过
ref.invoke()创建连接 marshalValue()序列化参数unmarshalValueSee()反序列化返回数据(存在漏洞点)
- 通过
4.5 注册中心处理请求
- 通过
Target处理请求 disp.dispatch()方法分发请求RegistryImpl_Skel#dispatch处理以下操作:- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
- 除
list外都存在反序列化漏洞
4.6 DGC(分布式垃圾回收)机制
-
自动创建过程:
- 通过
DGCImpl类初始化 - 创建
DGCImpl_Stub用于内存回收 - 端口随机分配
- 通过
-
漏洞点:
DGCImpl_Stub中的clean和dirty方法DGCImpl_Skel中的反序列化操作
5. RMI漏洞原理
5.1 漏洞点总结
-
客户端查找远程对象时:
call.executeCall()中异常处理时的反序列化- 服务端可向客户端发送恶意对象
-
客户端请求服务端时:
unmarshalValueSee方法中的反序列化
-
注册中心处理请求时:
bind、rebind、unbind、lookup操作中的反序列化
-
DGC机制中:
DGCImpl_Stub和DGCImpl_Skel中的反序列化
5.2 安全限制
- JDK8u121之后,
bind、rebind、unbind方法只能对localhost进行操作 - 多数攻击在JDK8u121之后已修复
6. 实际应用与防护建议
6.1 实际应用
- RMI多用于后续的Fastjson、Struts2等攻击组合
- 单独攻击RMI意义有限
6.2 防护建议
- 升级JDK至最新版本
- 限制RMI服务仅监听localhost
- 配置SecurityManager进行权限控制
- 对序列化数据进行严格验证
- 使用SSL/TLS加密RMI通信