RMI

简介

Remote Method Invocation,远程方法调用[目标与RPC相似]

是让某个JVM的对象调用另外一个JVM中的对象,Java独有

java调用Java程序(Java混合编程),分布式部署,多虚拟机JVM运行

多个进程可通过网络互相传递消息进行协作,clint和server

在学习Fastjson jdbcRowSetlmpl时候会用到,还有log4j2

RMI Server

  1. 一个继承了java.rmi.Remote接口,其中定义我们要远程调用的函数,如hello()
  2. 一个实现此接口并且继承于UnicastRemoteObject的实现类,远程对象的实现类必须继承于UnicastRemoteObject,只有继承了才能表示该类是一个远程对象
  3. 一个主类,用来创建Registry,并讲上面的类实例化后绑定到一个地址.
  1. import sun.rmi.server.UnicastRef;
  2. import java.io.IOException;
  3. import java.rmi.Naming;
  4. import java.rmi.Remote;
  5. import java.rmi.RemoteException;
  6. import java.rmi.registry.LocateRegistry;
  7. import java.rmi.server.UnicastRemoteObject;
  8. public class RMIserver {
  9. public interface IRemoteHeloworld extends Remote{
  10. public String hello() throws RemoteException;
  11. }
  12. public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHeloworld{
  13. public RemoteHelloWorld() throws RemoteException{
  14. super();
  15. }
  16. @Override
  17. public String hello() throws RemoteException {
  18. System.out.println("call from");
  19. try {
  20. Runtime.getRuntime().exec("calc");
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. }
  24. return "hello world";
  25. }
  26. }
  27. private void start() throws Exception{
  28. RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld();
  29. //创建并运行registry服务,且端口号为1099
  30. LocateRegistry.createRegistry(1099);
  31. //绑定对象,以Hello名字发布出去
  32. Naming.rebind("rmi://127.0.0.1:1099/Hello",remoteHelloWorld);
  33. }
  34. public static void main(String[] args) {
  35. try {
  36. new RMIserver().start();
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }

RMI Client

  1. 使用Naming.lookup在Registry中寻找名字是Hello的对象,后门使用和在本地使用是一样的了
  1. import java.rmi.Naming;
  2. public class TrainMain {
  3. public static void main(String[] args) throws Exception {
  4. RMIserver.IRemoteHeloworld hello =(RMIserver.IRemoteHeloworld) Naming.lookup("rmi://172.20.10.7:1099/Hello");
  5. String hello1 = hello.hello();
  6. System.out.println(hello1);
  7. }
  8. }

image-20211212094020810.png
image-20211212094049447.png

虽然说执行远程方法的时候,代码是在远程服务器上执行的,但我们还需要知道有哪些方法可以调用

这时候便可以运行接口

理解

client不会直接调用server上的方法,会借助存根(stub)来进行代理访问server;同时骨架(Skeleton)是另一个代理

同时与真实对象一起在服务器上

骨架将接受到的请求交给服务器处理,服务器处理完成之后将结果打包发送至存根,然后存根将结果解包之后的结果发送给客户端

08c1d8b2-08d3-4aa9-961a-69ce2c7dacc1.png

wirshark流量分析

  1. ip.dst ==1.1.1.1
  2. rmi || tcp.port eq 端口号

整个过程进行了两次TCP握手

第一次TCP连接是连接远端172.20.10.7的1099端口,二者沟通后,client向server发送一个”Call”消息,对应客户端连接Registry,并在其中寻找Name是Hello的对象,server回复一个”ReturnData”消息,对应Registry返回一个序列化的数据,这个就是找到Name=Hello的对象

然后新建一个TCP连接,连接server33769端口

客户端反序列化该对象,发现该对象是一个远程地址,地址在192.168.135.142:33769,于是再与这个地址建立TCP连接,在这个新的连接中,才执行真正的远程调用方法[hello()]

在这个ReturnData包中,返回目标192.168.135.142,后面紧跟一个字节\x00\x00\x83\xE9,刚好就是整数33769的网络序列
image-20211212101525661.png

image-20211212143703575.png
image-20211212101222416.png

RMI Registry就像是一个网关,自己不会执行远程方法,但RMI server可以在上面注册一个Name到对象的绑定关系;RMI client通过Name向RMI registry查询,得到这个绑定关系,然后再连接RMI server;最后,远程方法实际上在RMI server上调用

序列化传输

3eafb5fb-23e6-468e-a5dd-43218e6b2b71.png

RMI在数据传输过程中的对象须实现java.io.serializable接口,因为在传输过程中都是进行序列化进行传输并且客户端的serialVersionUID字段要与服务器端保持一致

ac ed就是反序列化的标志

RMI的主要构成

三部分

  1. RMIregistry注册表:服务实例将被注册表注册到特定的名称中
  2. RMI Server
  3. RMI Clint客户端:客户端通过查询注册表来获取对应的名称的对象引用,以及该对象实现的接口

RMI client会远程连接RMI registry(默认端口1099),然后会在registry寻找名字为Test的对象(假设此时客户端要调用test对象中的某个方法),registry会寻找对应名字的远程对象引用,并且序列化后进行返回(数据内容就是远程对象的地址,这里返回的对象就是存根stub),客户端在接受到之后首先会在本机中的classpath进行查找,如果没有找到则说明是远程对象,客户端就会与远程对象进行tcp连接
image-20211212144330106.png

当我们尝试绑定远程服务器上的hello对象的时候,会报错

java对远程访问RMI registry做了限制,只有来源地址是localhost的时候,才能调用rebind.bind.unbind等方法

不过list和lookup方法可以远程调用

list可以列出目标上所有绑定的对象

  1. String[] s =Naming.list("rmi://192.168.135.142:1099");

lookup作用就是获得某个远程对象

目标服务器上要是有危险方法,RMI便可调用

https://github.com/NickstaDB/BaRMIe

存根和骨架

当RMI server启动的时候的端口是被随机分配的,但是我们的rmi registry端口是1099

  1. 客户端通过远程连接registry获取存根,存根中包含了远程对象的定位信息。如socket端口.服务器主机地址等,并实现了远程调用过程中的具体的网络通信细节
  2. 由于存根是客户端的代理类,所以客户端可以调用存根上的方法
  3. 存根远程连接到服务器,提交对应的参数
  4. 骨架收到数据并对其进行反序列化,然后将发送给我们的server
  5. server执行之后将结果打包,传输给client

RMI的危害

利用codebase执行任意代码

曾经Java可以运行在浏览器中,利用Applet,在使用Applet的时候通常需要指定一个codebase属性

比如

  1. <applet code="Helloworld.class" codebase="Applets" width="800" height="600">
  2. </applet>

除了Applet,RMI中也存在远程加载的场景,也会涉及到codebase

codebase是一个地址,告诉JVM去哪搜索类,类似于classpath,但classpath是本地路径,而codebase通常是远程URL,比如http,frp等

若我们指定codebase=http://example.com 然后加载org.vulhub.example.Example类,则JVM 会下载这个文件,并作为Example类的字节码

RMI流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在序列化时,就会去寻找类,如果某一端反序列化时发现一个对象,那么就会去自己的classpath下寻找对应的类,如果在本地没有找到这个类,就会去远程加载codebase中的类

在RMI中我们是可以将codebase随着序列化数据一起传输,服务器在接受到这个数据后就回去classpath和被控制的codebase寻找类,导致任意命令执行

满足条件

  • 安装并配置了securitymanager
  • java版本低于7u21,6u45或者设置了Java.rmi.server.useCodebaseOnly=false