浅谈RMI


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分布式应用程序。服务器调用RMI Registry来将一个名称与一个远程对象关联(或绑定)。客户端在服务器的RMI Registry中通过名称查找远程对象,然后调用其方法。图示还显示了RMI系统在需要时使用现有的Web服务器从服务器到客户端以及从客户端到服务器加载类定义的过程。

RMI serverRMI 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异常。

java.rmi.Remote源码

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方法:

运行成功

本例中调用 stubsayHello() 方法背后的流程:

  1. RMI client 端通过 stub 中包含的 host、port 信息,与远程对象所在的 RMI server 建立连接 ,然后序列化调用数据

  2. RMI server 端接收调用请求,将调用转发给远程对象,然后序列化结果,返回给 RMI client

  3. RMI client 端接收、反序列化结果

0x04 小结

在远程方法调用过程中,远程对象需要先序列化,从本地 JVM 发送到远程 JVM,然后在远程 JVM 上反序列化,执行完后,将结果序列化,发送回本地JVM,因此这个过程可能会存在反序列化漏洞。

参考文章:

RMI Overview

Getting Started Using Java RMI

Remote Method Invocation (RMI)


文章作者: anch0r
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 anch0r !
  目录