0x01 简介
RMI(Remote Method Invocation),即远程方法调用,在java中,一个JVM上的object可以通过RMI实现远程调用另外一个JVM的object方法。
RMI有些类似C语言中所用的远程过程调用RPC。RPC主要关注数据结构。将数据打包并传输是相对容易的,但对于Java而言,这还不够。在Java中,我们不仅仅使用数据结构,还使用包含数据和操作数据的方法的对象。我们不仅需要能够将对象的状态(数据)传输到网络中,还需要接收方能够在接收后与对象进行交互(使用其方法)。
RMI 可以使用以下协议实现:
- Java Remote Method Protocol (JRMP):专门为 RMI 设计的协议
- Internet Inter-ORB Protocol (IIOP) :基于
CORBA
实现的跨语言协议
在很多java反序列化的漏洞的poc中我们都可以看到rmi协议的身影,一般都是利用它来访问一个远程的恶意java对象的。
0x02 RMI相关概念
从RMI设计角度来讲,基本分为三层架构模式来实现RMI,分别为RMI注册中心、RMI客户端和RMI服务端。
注册表(rmi registry): 是一种名称服务(name service),提供远程对象(remote object)注册,即名称(name)到远程对象的绑定和查询,是一种特殊的远程对象
说明白些就是以URL形式注册远程对象,并向客户端回复对远程对象的引用。
客户端(rmi registry): 通过名称向 RMI registry
获取远程对象引用remote object reference (stub)
,调用其方法,框架如下:
存根/桩(Stub):远程对象在客户端上的代理;
远程引用层(Remote Reference Layer):解析并执行远程引用协议;
传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。
服务端(rmi server): 创建远程对象,将其注册到 RMI registry
,框架如下:
骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值;
远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。
官方文档的示例图如下:
该图描述了一个使用RMI注册表获取远程对象引用的RMI分布式应用程序。服务器调用RMI Registry
来将一个名称与一个远程对象关联(或绑定)。客户端在服务器的RMI Registry
中通过名称查找远程对象,然后调用其方法。图示还显示了RMI系统在需要时使用现有的Web服务器从服务器到客户端以及从客户端到服务器加载类定义的过程。
RMI server
和RMI registry
运行在一个主机的不同端口,RMI registry
默认运行在1099端口上。
RMI URL形式为:rmi://hostname:port/remoteObjectName
0x03 一个RMI实例
下面在本地演示一个RMI通信:
接口编写
Client端需要调用具体的远程方法,所以需要有服务端注册的远程对象类所实现的接口。如果是真实远程通信情况,服务端和客户端都要有该接口,我这里环境是本地。
编写一个继承自java.rmi.Remote的接口Hello,其中要定义一个我们要调用的远程方法,比如这里的sayHello()
,然后需要抛出 RemoteException
或其父类的异常:
package com.Anchor;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
String sayHello() throws RemoteException; //定义要调用的远程方法,并抛出RemoteException异常
}
java.rmi.Remote接口是一个空接口,和Serializable接口一样,只作标记作用,接口中的每个方法都需要抛出RemoteException异常。
RMI Server编写
编写RMI Server类,该类要先实现刚刚编写的Hello接口,在主函数中创建并导出远程对象,然后将远程对象注册到RMI Registry上,具体细节可以看注释:
package com.Anchor;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer implements Hello {
public static void main(String[] args) {
try {
RMIServer obj = new RMIServer();
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888);//该语句执行后会运行一个 rmi server,监听本地8888端口等待client的请求
Registry registry = LocateRegistry.createRegistry(1099); // 该语句执行后会创建并启动 rmi registry,监听在本地 1099 端口
//exportObject() 方法返回结果为 remote object stub (代理对象,实现了与 Hello 接口同样的方法,包含 rmi server 的 host、port 信息)
registry.bind("Hello", stub); // 将 stub 注册到 registry,并与 name Hello 绑定
} catch (Exception e) {
System.out.println("Server Exception: " + e.toString());
e.printStackTrace();
}
}
public String sayHello() throws RemoteException {
return "Hello, World";
}
}
RMI Client端编写
编写RMI Client类,先通过LocateRegistry.getRegistry
方法获取RMI registry
,然后通过registry.lookup
获取 remote object stub
:
package com.Anchor;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) {
String host = "localhost";
int port = 1099; //RMI registry绑定的端口
try {
Registry registry = LocateRegistry.getRegistry(host, port);
Hello stub = (Hello) registry.lookup("Hello");
String response = stub.sayHello();
System.out.println("response: " + response);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}
然后先运行RMIServer.java,再运行 RMIClient.java查看结果,可以看到成功调用sayHello方法:
本例中调用
stub
的sayHello()
方法背后的流程:
RMI client
端通过stub
中包含的 host、port 信息,与远程对象所在的RMI server
建立连接 ,然后序列化调用数据
RMI server
端接收调用请求,将调用转发给远程对象,然后序列化结果,返回给RMI client
RMI client
端接收、反序列化结果
0x04 小结
在远程方法调用过程中,远程对象需要先序列化,从本地 JVM 发送到远程 JVM,然后在远程 JVM 上反序列化,执行完后,将结果序列化,发送回本地JVM,因此这个过程可能会存在反序列化漏洞。
参考文章: