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 客户端反序列化攻击示例代码: