熟悉的名字,在CVE学习中,其中Apache solr 的log4j漏洞和weblogic远程代码执行都提到了RMI,作用如名, RMI(remote method invocation)即远程方法调用。
Java安全[RMI(1)]
RMI的目标其实和RPC类似,是让某个Java虚拟机上的对象调用另一个Java虚拟机上的方法,只不过RMI是Java中独有的一种机制。
RPC(Remote Procedure Call)是远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。在分布式计算中,RPC允许运行于一台计算机的程序调用另一个地址空间的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程。RPC是一种
CS模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
既然是远程调用,那么肯定是存在谁调用谁的关系,这就构成了RMI Server和RMI Client,在Server中实现远程调用的函数和接口,而Client需要知道想要调用方法的接口,然后访问执行即可。
RMIServer
⼀个RMI Server分为三部分:
- ⼀个继承了
java.rmi.Remote的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的hello()- ⼀个实现了此接⼝的类
- ⼀个主类,⽤来创建
Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server了。
先编写一个RMI Server
1 | package RMI_1; |
分析一下代码,
- 接口定义
先定义一个名为IRemoteHelloWorld的接口,它扩展了Remote接口,这是RMI中的一个标记接口。接口声明了一个hello()方法,该方法可以抛出RemoteException异常,这个接口定义了一个远程方法,客户端可以通过RMI调用它。
1 | public interface IRemoteHelloWorld extends Remote { |
- 远程对象的实现
这里定义了一个名为RemoteHelloWorld类,实现了IRemoteHelloWorld接口并继承UnicastRemoteObject类。这个类的构造函数调用父类,也就是UnicastRemoteObject类的构造函数,用于创建一个远程对象。RemoteHelloWorld还实现了hello()方法,该方法奖打印一条消息call from,并返回Hello World给调用该方法的对象(如,客户端服务器)。
1 | public class RemoteHelloWorld extends UnicastRemoteObject implements |
- 启动RMI服务器方法定义和执行
start() 方法用于启动RMI服务器。
- 创建一个
RemoteHelloWorld实例h,这个实例将充当远程对象。 - 通过
LocateRegistry.createRegistry(1099)创建了一个RMI注册表,并指定它监听在1099端口上。 - 使用
Naming.rebind()将远程对象h绑定到了rmi://127.0.0.1:1099/Hello这个名称下。
1 | public class RMIServer { |
- main方法
创建RMIServer的实例,并调用start()方法启动RMIServer。
RMIClient
RMI客户端相比于RMI服务端的代码就简单一点,只需要访问调用服务端的远程方法即可
1 | package RMI_1; |
这里展示的只是本地进行调用RMIServer,要实现真正的RMI,客户端需要服务端提供接口实现方式,如上面代码所示的IRemoteHelloWorld,一般不会把接口和实现都包含在一个类中,而是分开多个文件,这里是为了方便。
客户端只需要接口打包成jar,这样就能知道RMI可以调用的方法有哪些,并知道服务端的ip和端口即可,然后使⽤ Naming.lookup 在Registry中寻找到名字是Hello的对象,后⾯的使⽤就和在本地使⽤⼀样了。
jar打包命令,这就实现了把接口打包的结果
最后在IDEA中将包加到项目结构里即可。
RMI流量分析
用wireshark抓包看看RMI的通讯数据原理
为了方便直观看出服务端和客户端,于是用虚拟机跑服务端,主机跑客户端,这里就不会两个ip都是一样的了
服务端ip:
192.168.169.131客户端ip:
192.168.126.1(实际ip为10.19.16.44,但是由于主机是通过虚拟机网卡访问的所以,抓虚拟机网卡的流量包时,网卡ip就是主机的)
这里将IRemoteHelloWorld独立为一个文件,所以客户端代码有点不一样,服务端删不删IRemoteHelloWorld都一样
1 | package RMI_1; |
抓个流量包看看,
可以看到整体的过程中发生了两次tcp握手[灰色部分],也就是在实际情况下构成了两次tcp连接。
第一次是从客户端的19581端口访问服务器的1099端口,第二次是客户端的19584端口访问55947端口
其实第一次握手很容易理解,因为我们的客户端设置的就是访问服务端的1099端口,但是为什么后面会莫名其妙访问服务端的55947端口呢
在流量包的JRMI Return Data中,也就是服务端向客户端发送的流量中可以看到,在最后的服务端ip后面的一个字节\x00\x00\xda\x8b
通过进制转化可以看到,这个字节正好是55947的对应的网络序列,这也就是为什么客户端会向服务器端的55947端口进行tcp握手。
但其实这段数据中,从\xAC\xED开始后,后面的所有数据都属于Java序列化的内容,其中的ip和端口只是这个对象的一部分。
其实可以简单总结一下RMI的流程,
首先客户端访问连接Registry,并在其中寻找Name名为Hello的对象,这个过程对应数据包中的JRMI,Call。
而后Registry向客户端发送一串反序列化字符串,代表找到了Name=Hello的对象,这个过程对应数据包中的JRMI,ReturnData。
客户端反序列化JRMI,ReturnData,发现该对象是一个远程对象,地址是192.168.169.131:55947,于是再与这个地址建立TCP连接,在这个新的连接中,才可以执行真正远程方法调用,也就是hello()。
可以从下图直观的认识到RMI中各个元素的关系。
(底下是RMI,单词写错了应该是invocation)
可以从先从RMI Server开始看,服务端先到RMI Registry上注册了一个Name的对象绑定关系;【如下代码,将RemoteHelloWorld类实例化,然后将其绑定到Hello这个Name上,这就是绑定,然后告诉Registry,这个对象能通过访问给定的名称进行访问】
1 | RemoteHelloWorld h = new RemoteHelloWorld(); |
RMI Registry相当于一个网关,它本身虽然绑定了远程需要调用的对象,但是它自己是不会执行远程方法的。
而后是RMI Client,当它知道Name后,会向RMI Registry发送查询请求【如下代码,客户端这里用服务器给的对应的调用的接口IRemoteHelloWorld,创建对象hello,然后向RMI Registry发送想要调用的注册名字,RMI Registry使用这个信息来查找并返回相应的远程对象引用】
1 | IRemoteHelloWorld hello = (IRemoteHelloWorld) Naming.lookup("rmi://192.168.169.131:1099/Hello"); |
得到远程方法的绑定关系,然后通过这个绑定关系再次连接RMI Server;
1 | String ret = hello.hello("Ttoc"); |
这里hello就是获得了RemoteHelloWorld类的远程对象引用,然后用hello.hello("Ttoc")进行调用这个类中的hello方法。而这个方法的调用会通过网络发送到服务器端,服务器端会执行对应方法,并将结果返回客户端。这个过程中RMI框架起到的作用就是处理网络通信,序列化,反序列化等细节,使得客户端和服务器之间的通信就像是在本地方法执行一样,但是本质还是在服务器端进行的执行。
]/image-20230913101103425.png)
]/image-20230913103004923.png)
]/image-20230917111005900.png)
]/image-20230917110908280.png)
]/image-20230913132813979.png)
]/image-20230914162413296.png)
]/image-20230914163214005.png)
]/image-20230914163037413.png)
]/image-20230914164341663.png)
]/image-20230917102629249.png)