深入理解Java RMI:架构、原理与反序列化漏洞
本文详细解析Java RMI(远程方法调用)的底层架构、Stub/Skeleton通信机制及工作原理,并通过代码示例演示服务搭建与客户端调用。文末深入探讨RMI通信中的Java反序列化漏洞及其潜在风险。
- 网络安全
- Java安全
RMI架构:

RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:
RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)。Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。RemoteCall序列化RMI服务名称、Remote对象。RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。RMI客户端反序列化服务端结果,获取远程对象的引用。RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。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 基本工作原理
- 定义远程接口:这个接口定义了客户端可以调用的方法。接口必须继承
java.rmi.Remote接口,并且所有方法都必须声明java.rmi.RemoteException。 - 实现远程接口:创建一个类来实现这个远程接口,这个类通常会继承
java.rmi.server.UnicastRemoteObject,以便能够导出远程对象,使其能够接收远程调用。 - 启动 RMI 注册表并注册远程对象:在服务器端,需要启动 RMI 注册表(通常通过
rmiregistry命令)并将实现了远程接口的对象绑定到注册表中,这样客户端就可以通过名字来查找这个远程对象。 - 客户端查找并调用远程对象:客户端通过
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();
}
}
}
为什么客户端会调用实现类的方法
- 查找远程对象:客户端使用
Naming.lookup(RMI_NAME)查找到注册在 RMI 注册表中的远程对象引用。这个引用实际上是一个代理对象(Stub),它实现了远程接口RMITestInterface。 - 调用远程方法:当客户端调用
rt.test()方法时,这个调用被代理对象截获,并通过网络传送到服务器端。 - 服务器处理请求:服务器端的 RMI 框架接收到方法调用请求后,将其转发给实际的实现类
RMITestImpl的test方法。 - 返回结果:实现类的方法执行完毕后,结果通过 RMI 框架返回给客户端的代理对象,最终在客户端获得结果并输出。
在这个过程中,客户端实际上调用的是代理对象的方法,但由于代理对象会将调用转发给远程对象的实际实现类,所以最终执行的是实现类的方法。这就是为什么客户端会调用实现类的方法的原因。
RMI反序列化漏洞
RMI通信中所有的对象都是通过Java序列化传输的,在学习Java序列化机制的时候我们讲到只要有Java对象反序列化操作就有可能有漏洞。
既然RMI使用了反序列化机制来传输Remote对象,那么可以通过构建一个恶意的Remote对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。
首先我们依旧使用上述com.anbai.sec.rmi.RMIServerTest的代码,创建一个RMI服务,然后我们来构建一个恶意的Remote对象并通过bind请求发送给服务端。
RMI客户端反序列化攻击示例代码:
留言讨论
0 条留言
正在加载留言...