沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

关于Java中RMI的个人拙见

0x00 前言


懒狗很久都没有动手写过文章了,实在是懒。这次懒狗先是玩了几小时的饥荒,然后挤牙膏似的找了几篇关于Java中的RMI的文章,并且粗略的理解了一下,所以我也不敢保证自己下面写的都是正确的😋。

本篇文章包含以下元素:

  • 什么是RMI
  • RMI的运用

第二天,阿里云的饥荒服务器就嗝屁了,麻了,一天没玩到游戏,更加颓废了。

结果这篇文章,现在才差不多算写完,而且还留下了疑问 —2021/08/01

0x01 什么是RMI


释意

RMI是Remote Method Invocation的缩写,翻译过来的意思就是远程方法调用。在有些文章里会将他与RPC(Remote Procedure Call 远程过程调用)进行对比,这里因为我是个懒狗和菜狗,都还搞不懂什么是RPC,所以这里我就不进行对比了。

作用

远程方法调用,即可以让一个JVM中运行的程序调用另一个JVM中程序的方法,并且这两个JVM可以是分别属于两台不同的主机(也可以相同主机)。

组成

一个RMI往往包含以下三部分:

  • 注册中心 Register
  • 服务端 Server
  • 客户端 Client

通信原理

RMI为了让客户端和服务端更加专注的处理自己的事物,而不用去操心客户端和服务端之间的通信问题,采用了类似代理的模式,并且在客户端和服务端各自有一个代理,客户端的叫Stub(存根) ,服务端的叫Skeleton(骨架),据此,我们给出如下的通信方式:

image-20210710224506479

客户端和服务端各自通过调用自己的代理来与对方通信,并且代理最后也会将取得的结果返回给端,在这里我省略了代理间通信的细节(感兴趣可以自己看参考文章了解细节)。

在JDK 1.2版本(1998)之后,骨架skeleton不再被需要, 由Java的UnicastServerRef#dispatch替代;在JDK 5 (大家常说的1.5)之后,不再需要手动利用rmic命令生成静态Stub,而是会由Java自动地动态生成。这个动态生成也是我们后面JNDI注入的关键

三部分之间的联系

以上我们给出了RMI底层的通信模型,然而客户端是如何获取到自己的Stub的呢?

RMI中采用注册表的方式,服务端会将远程对象注册,然后在注册表中留下相关信息,当客户端需要使用远程代理对象(Stub)时,将在注册表中查找。也就是说注册中心 Register实际上就是用来给服务器提供Stub的。

将这一部分与上面的通信原理图相结合,我们可以得到下面的原理图:

数据传输格式

服务端和客户端的通信,包括服务端、客户端与注册中心的通信都是使用的序列化内容,然后在各自这里进行反序列化。我们之所以能够利用RMI来攻击,就是基于此。

0x02 一个简单的RMI


远程对象接口

要实现远程对象的调用,显然,客户端和服务端需要有一个同样的远程对象接口。这样子,虽然客户端并没有远程对象的具体实现,但是我们依然可以先声明一个相同的类。(😋,听不懂就算了,多看几遍源码就懂了。)

下面是一个远程对象接口的例子:

package edu.myRMI.share;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote {
    String sayHello(String name) throws RemoteException;
}

Java中规定远程对象接口必须继承Remote类,当客户端和服务端不在同一个包时,类必须使用public来声明,且远程接口中的方法必须抛出RemoteException的错误。

远程对象接口实现

远程接口的实现可以显式的继承UnicastRemoteObject类,这便于之后直接注册为远程对象(待会细🔒),这里先给出这样写出来的代码:

package edu.myRMI.server;

import edu.myRMI.share.RemoteInterface;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteImpl extends UnicastRemoteObject implements RemoteInterface{

    protected RemoteImpl() throws RemoteException {
        //看网上的博客说,因为父类的构造器有抛出RemoteException,所以这里也必须写
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        return "hi"+ name +"chenlvtang_is_Fool";
    }
}

之所以上面说显式的继承可以便于直接注册为远程对象,在于UnicastRemoteObject类的构造方法:

protected UnicastRemoteObject() throws RemoteException
{
    this(0);  
}

protected UnicastRemoteObject(int port) throws RemoteException
{
    this.port = port;
    exportObject((Remote) this, port);
}

它的第一个无参构造方法中的this(0)其实就是调用了下面的有参构造方法,并传参为0。而exportObject方法,我们只需要知道,它会帮我们将类导出为远程对象服务(Exports the remote object to make it available to receive incoming calls, using the particular supplied port.),能够接收调用(很浅显的说法,如果有兴趣,可以自己细究,其实它有好几种功能, 并且也有函数多态)。所以当我们直接继承UnicastRemoteObject类时,待会实例化的时候,就会自动帮我调用exportObject方法,从而更方便的导出为远程对象服务。

因为Java不支持多继承,所以有时候我们想让远程对象继承别的类的时候,就不得不放弃直接继承UnicastRemoteObject,这时候我们可以在之后手动的调用exportObject方法,具体写法将在下面服务端给出。

上面的UnicastRemoteObject继承了RemoteServer,而RemoteServer继承了RemoteObject,其实现了接口Remote, java.io.Serializable,这也符合上面我们所说的客户端、服务端与注册中心间是通过序列化进行通信;然而这里我有个疑问,当我们的远程对象类没有显式的继承UnicastRemoteObject时,为什么还可以用于通信呢?或许是我目前对这三者通信的理解有什么盲点,又或者可能是我对Java中的序列化了解的不够完善。所以我这里就先写下疑问,等以后再来探讨。– 2021/07/31

关于这个问题,答案其实是:Java并不是把这个远程对象类序列化后发送过去给客户端调用,而是在收到调用请求后,先调用方法,然后把方法的返回值进行序列化。所以,只要我们方法中的返回值是“基本类型”或者是能够进行序列化的类即可,这里我们的返回值就是基本类型中的String – 2021/08/07

服务端

在写服务端代码时,我们可以选择将启动注册中心的代码写入其中:

package edu.myRMI.server;

import edu.myRMI.share.RemoteInterface;

import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class myServer {
    public static void main(String[] args) throws RemoteException, MalformedURLException {
        //实例化一个远程对象,注意这里的RemoteImpl是有继承UnicastRemoteObject的
        RemoteInterface myRemote = new RemoteImpl();
        //将注册中心创建在1099端口,可以再加一个参数IP
        Registry myRegistry = LocateRegistry.createRegistry(1099);
        //向注册中心中注册Stub,第一个参数为名字,要求客户端服务端相同
        myRegistry.rebind("myRMI", myRemote);
    }
}

上面我们说到过,远程对象接口的实现类如果有直接继承UnicastRemoteObject,则在实例化的时候,就会自动调用exportObject,导出为远程对象服务。如果没有继承,那我们可以使用下面的代码手动导出:

RemoteInterface myRemote = new RemoteImpl();
UnicastRemoteObject.exportObject(myRemote, 0);

这里也可以不用代码启动注册中心,可以选择使用命令启动:

rmiregistry 5566 //可以指定端口创建,未指定端口时,默认在1099端口创建

或者让其在后台运行start rmiregistry,这将单独创建一个运行窗口

客户端

package edu.myRMI.client;

import edu.myRMI.share.RemoteInterface;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class myClient {
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
        //连接到注册中心,这里因为是本地创建的,所以IP写的是localhost
        Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
        //像注册中心查询服务,这里也就体现了为什么要和服务端共用一个接口
        RemoteInterface myRemote = (RemoteInterface) myRegistry.lookup("myRMI");
        String result = myRemote.sayHello("chenlvtang");
        System.out.println(result);
    }
}

启动

分别启动客户端和服务端,查看调用结果

image-20210801173027844

image-20210801173008476

0x03 参考文章


java RMI原理详解 - yehx - 博客园 (cnblogs.com)

深入理解JNDI注入与Java反序列化漏洞利用 – KINGX

Java 安全-RMI-学习总结 - 知乎 (zhihu.com)

JAVA RMI 反序列化知识详解 (seebug.org)

Java RMI应用程序 - Java RMI远程方法调用教程™ (yiibai.com)

RMI远程调用 - 廖雪峰的官方网站 (liaoxuefeng.com)