目录

java RMI反序列化-原理篇

java RMI反序列化-原理篇

前言:RMI 作为后续漏洞中最为基本的利用手段之一,学习的必要性非常之大。本文着重偏向于 RMI 通信原理的理解,如果只懂利用,就太脚本小子了。

这里有个坑点:就是 RMI 当中的攻击手法只在 jdk8u121 之前才可以进行攻击,因为在 8u121 之后,bind rebind unbind 这三个方法只能对 localhost 进行攻击,后续我们会提到。

RMI 是什么?

Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。RMI 使用 JRMP(一种协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。

RMI 的架构

Stub和Skeleton

Stub(存根)和 Skeleton(骨架),当客户端试图调用一个远端对象,实际上会调用客户端本地的一个代理类,也就是 Stub。而在调用服务端的目标类之前,也会经过一个对应的代理类,也就是 Skeleton。它从 Stub 接收远程方法调用并将它们传递给对象。

RMI 实列元素

  • Client:客户端,客户端调用服务端的方法
  • Server:服务端,服务端通过绑定远程对象,这个对象可以封装很多网络操作,也就是 Socket
  • Registry:注册中心,类比成 RMI 的电话薄。提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。

RMI 的实现

这张图非常详细的描述了 RMI 的过程,先是远程的服务端创建并注册远程对象,然后客户端再进行查找的的时候先会去注册中心进行查找,然后注册中心返回服务端远程对象的存根 然后调用远程对象方法时,客户端本地存根和服务端骨架进行通信,然后就是骨架代理进行方法调用并且再服务端进行执行,然后骨架又把结果返回给存根,最后存根把结果给客户端,更详细的图

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240704155117374.png

一个RMI 实列

java. rmi. Remote 接口

java.rmi.Remote 接口用于标识可以从非本地虚拟机调用其方法的接口。任何作为远程对象的对象必须直接或者间接实现。只有那些远程接口(继承 java.rmi.Remote 接口)中指定的方法才可以远程使用。

java.rmi.server.UnicastRemoteObject类

RMI 提供了一些远程对象实现可以继承的便利类,这些类有助于远程对象的创建,其中包括java.rmi.server.UnicastRemoteObject类。这个类构造方法会调用exportObject或调用exportObject静态方法,它会返回远程对象代理类,也就是Stub。如果不继承该类可以手动调用其静态方法 exportObject 来手动 export 对象。

RMI Server

一、编写一个远程接口

远程接口要求:

  • 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
  • 同时需要继承Remote类,也就是需要实现java.rmi.Remote接口
  • 接口的方法需要声明java.rmi.RemoteException报错
  • 服务端实现这个远程接口

定义一个我们期望能够远程调用的接口,这个接口必须扩展 java.rmi.Remote 接口,用来远程调用的对象作为这个接口的实例,也将实现这个接口,为这个接口生成的代理(Stub)也是如此。这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException 异常,

package org.example;  
import java.rmi.Remote;  
import java.rmi.RemoteException;  
  
public interface RMIinter extends Remote {  
    public String hello() throws RemoteException;  
}

二、编写一个实现了这个远程接口的实现类

实现类要求:

  • 实现远程接口
  • 继承UnicastRemoteObject类(具体效果上面有说)
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
  • 注册远程对象
package org.example;  
  
import java.rmi.RemoteException;  
import java.rmi.server.UnicastRemoteObject;  
  
public class RMIobj extends UnicastRemoteObject implements RMIinter {  
  
    protected RMIobj() throws RemoteException {  
        super();  
    }  
  
    public String hello() throws RemoteException {  
        System.out.println("hello()被调用");  
        return "gaoren";  
    }  
}

现在被调用的对象就创建好了,接下来就是如何实现调用了。

RMI Registry

在上面的流程图不难看到还有个Registry 的思想(不再解释),这种思想主要由 java.rmi.registry.Registry 和 java.rmi.Naming 来实现。

1、java.rmi.Naming

这是一个 final 类,提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法,这个类提供的每个方法都有一个 URL 格式的参数,格式如下: //host:port/name

  • host 表示注册表所在的主机
  • port 表示注册表接受调用的端口号,默认为 1099
  • name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字

Naming 提供了查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)、list(列表)用来对注册表进行操作。也就是说,Naming 是一个用来对注册表进行操作的类。而这些方法的具体实现,其实是调用 LocateRegistry.getRegistry 方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的。

2、java.rmi.registry.Registry

这个接口在 RMI 下有两个实现类,分别是 RegistryImpl_Skel 以及 RegistryImpl_Stub

我们通常使用 LocateRegistry#createRegistry() 方法来创建注册中心

package org.example;  
import java.rmi.Naming;  
import java.rmi.registry.LocateRegistry;  
  
  
public class Registry {  
    public static void main(String args[])throws Exception {  
            LocateRegistry.createRegistry(1099);  
            System.out.println("Server Start");  
        // 创建远程对象  
        RMIinter rmiobj = new RMIobj();  
        // 绑定远程对象  
        Naming.bind("rmi://localhost:1099/Hello", rmiobj);  
    }  
}

RMI Client

客户端进行调用,向注册中心查询相应的Name,调用相应的远程方法

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException {  
  
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);  
  
        System.out.println(Arrays.toString(registry.list()));  
        RMIinter stub = (RMIinter) registry.lookup("Hello");  
        System.out.println(stub.hello());  
  
    }  
}

这里 RMIinter 接口在 Client/Server/Registry 均应该存在,只不过通常 Registry 与 Server 通常在同一端上。

首先需要启动服务端RMI服务,运行服务端代码。然后客户端请求远程方法,也就是运行客户端的代码 服务端

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240720090441795.png

客户端

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240720090428311.png

这样一次简单的远程调用就完成了,不难发现其实方法的执行是在服务端执行的。

源码分析

总共是 6 个互相通信过程+3 个创建过程。

服务注册

创建远程服务

关键代码:

RMIinter rmiobj = new RMIobj();

在这句代码下上断点然后开始调试,在到 UnicastRemoteObject 构造函数之前,会先调用其静态方法进行一些赋值,不过不影响不用管,直接到其构造函数

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727111315889.png

初始化时会调用 exportObject 方法,这里的 obj 是我们要注册的远程对象。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727111452114.png

可以看到这里的 exportObject 方法是个静态函数,所以说如果没有继承 UnicastRemoteObject 类可与进行静态调用其方法。然后其参数 new 了一个 UnicastServerRef 类,跟进一手

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727112111637.png

又 new 了一个 LiveRef 类,继续跟进

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727112214283.png

this 就是调用其构造函数,然后 new 的那个就是个 id,跟进其构造函数,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727112354305.png

三个参数,第二个参数就是处理的网络请求,把端口进行一通处理,继续跟进 this

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727112938946.png

可以看到刚刚的第二个参数就是 ip 和端口嘛,然后出去。

其实这里总的来说就是创建了个 LiveRef 类然后将其赋值给服务端和客户端。

出来后继续跟进 exportObject 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727113459317.png

这里可以看到把刚刚的 liveref 赋值给了 sref,也就是服务端。包含了我们的网络处理信息。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727113613433.png

跟进 sref.exportObject 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727113911894.png

可以看到这里出现了 stub 代理类,通过 createProxy 来创建的,跟进

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727114931265.png

先看下这里的三个参数

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727114949506.png

impClass 就是 stub 代理类,cilentRef 实质上还是之前创建的 ref。

继续看看到一处逻辑

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727115809129.png

最主要的是 stubClassExists(remoteClass),其值为真就调用 if 语句,跟进看看:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727115947810.png

因为这里的 remoteClass 没有 remoteClass_Stub 所以返回 false,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727120328316.png

那么就会进行动态代理的创建,用 RemoteObjectInvocationHandlerUnicastRef对象创建动态代理,最后会返回一个Remote类型的代理类,在调用代理类方法时,就会会调用 RemoteObjectInvocationHandler.invoke 方法,这个后面再议

继续下一步,stub 创建好后其实可以看到里面最主要的还是 ref 部分。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727121346130.png

再往下走,发现其创建了 target 类,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727121641853.png

看其参数,也就是一个总封装,把前面那些对象什么的全部放了进去。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727121928549.png

继续跟进看到调用了 ref.exportObject,然后一直跟进

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727122314956.png

最后到了 TCPTransport.exportObject 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727122505408.png

listen() 里面就是涉及到网络请求的内容的,就是开启一个端口然后等待客户端连接进行操作。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727122913504.png

可以看到就是已经开启端口了(默认的随机端口),然后又调用了父类的 exportObject 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240720225531160.png

Target对象存放进ObjectTable中,ObjectTable 用来管理所有发布的服务实例 Target。

其实总的来说涉及到的大多是网络通信的东西,就是把创建的 ref 赋值给服务端然后创建个 stub,在把前面那些对象全部封装进 target,然后在 TCPTransport 中对网络通信进行处理发布服务,最后把 target 放进 ObjectTable 中。

创建注册中心

LocateRegistry.createRegistry(1099);

跟进 createRegistry 函数,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727210718200.png

new 了一个 RegistryImpl 对象,继续跟进,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727210955198.png

看到这里和上面远程对象注册很像,都 new 了个 liveref 对象,这里就不在跟进了和上面是一样的。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727211223405.png

继续走又创建了个 UnicastServerRef 对象,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727211319743.png assets/java%20RMI反序列化-原理篇/image-20240727211538936.png

就是个赋值,跟进 setup 方法里面,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727211718988.png

调用了 uref.exportObject 方法,回顾上面的远程对象注册,是调用的 sref.exportObject

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727113911894.png

其实都是 UnicastServerRefexportObject 方法,这里的 this 是 RegestryImpl 对象

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727212404575.png

继续跟进

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240726203951695.png

又是创建 stub,不过稍有区别了,跟进

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727212805943.png

这里的 RometeClassRegestryImpl 对象,由于存在 RegestryImpl_Stub ,所以会返回 true,执行 if 语句。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727213121649.png

执行的 createStub 方法其实也就是创建了个 RegestryImpl_Stub 实列化对象,

最后回到 exportObject 方法,stub 代理类也就创建好了,对比和上面远程对象注册中的 stub 确实不一样,上面的 stub 是动态代理创建的。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727213602804.png

然后又因为满足下面的 if 条件会执行 setSkeleton 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727213919288.png

assets/java%20RMI反序列化-原理篇/image-20240727214042983.png

看到又是创建 skle 代理,其实和 stub 代理创建差别不大,实列化了 RegestryImpl_Skel 对象

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727214407141.png

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727214609192.png

最后又是创建了个 target 对象

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727214719765.png

也是把刚刚那些对象进行一个总封装,不过比起上面的远程对象注册多了个远程对象 Impl 中多了 skel 对象,并且 stub 对象也不一样了。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727214826127.png

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727215014442.png

最后还是调用 putTarget 方法将其添加进 objecttable 中,可以看到多了一个 hashmap,11 是远程对象注册添加的,至于那个 2 是 DGC ,后面再说。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727215848487.png

创建注册中心与创建远程服务的大部分流程相同,差异在:

  • 远程服务对象使用动态代理,invoke 方法最终调用 UnicastRef 的 invoke 方法,注册中心使用 RegistryImpl_Stub,同时还创建了 RegistryImpl_Skel
  • 远程对象默认随机端口,注册中心默认是 1099(当然也可以指定)

服务端绑定

关键代码,

Naming.bind("rmi://localhost:1099/Hello", rmiobj);

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727221605128.png

先是把两个参数进行处理,然后只把 name 被 obj 传入 registry.bind 中。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727221648201.png

调用了 newCall 方法,(这里我这个类是 class 文件,没有源码无法正常调试,随便调调好了)

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727222211350.png

继续跟进,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727222129967.png

看到熟悉的 ref 了,总之这个方法就是建立一个连接,然后继续看到会对其进行序列化,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727222456951.png

然后执行了UnicastRef.invoke方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727222856664.png

跟进后在方法 executeCall ,在该方法中又对连接对象行了反序列化。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727222948440.png

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727223236410.png

这个应该属于远程绑定,一般服务端和注册中心在一端可以直接执行如下命令进行绑定

java.rmi.registry.Registry r = LocateRegistry.createRegistry(1099);  
r.bind("Hello", rmiobj);

这个 bind 就太好分析了,就不多说了。

服务发现

服务发现,就是获取注册中心并对其进行操作的过程,这里面包含 Server 端和 Client 端两种。如果是在 Server 端,我们希望在注册中心上绑定(bind)我们的服务,如果是 Client 端,我们希望在注册中心遍历(list)、查找(lookup)和调用服务。

相应代码:

RMIinter stub = (RMIinter) registry.lookup("Hello");

调用 lookup 方法,通过对应的 RMI_NAME,获取远程对象接口

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727085341352.png

同样是个建立个连接,然后对 remoteCall 进行序列化。

又通过 UnicastRef.invoke 方法传输这个 remoteCall,通过反序列化来获取注册远程对象时创建的代理类 stub。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727090213121.png

最后return这个对象。

整个服务调用三个地方过程

上面 Client 拿到 Registry 端返回的动态代理对象并且反序列化后,对其进行调用,这看起来是本地进行调用,但实际上是动态代理的 RemoteObjectInvocationHandler 委托 RemoteRef 的 invoke 方法进行远程通信,由于这个动态代理类中保存了真正 Server 端对此项服务监听的端口,因此 Client 端直接与 Server 端进行通信。

客户端

话不多说,直接看

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727224816386.png

stub 是个动态代理类,在其调用 hello() 方法的时候会直接调用到 handler 的 invoke 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727225001037.png

最后调用到了invokeRemoteMethod 函数

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727225133934.png

跟进,这里 ref 是 UnicastRef,会调用其 invoke 方法。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727225234929.png

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727225600259.png

marshalValue() 就是进行序列化,是对传入的参数进行序列化,只是这里调用的 hello 方法是个无参方法。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727225649500.png

然后继续看见调用了executeCall 函数。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727225854412.png

刚刚上面服务注册不难看出里面可以进行反序列化,这里不在深入了,继续看这个 invoke 方法逻辑,发现如果方法有返回值还会调用 unmarshalValue 方法进行反序列化,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727230115700.png

但是由于这里返回值是 string 型不符合条件会直接返回。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240727230339750.png

到此客户端的方法调用就结束了。

这里又两个反序列化的点,第一个就是直接的 stub 进行反序列化,第二个就是返回值进行反序列化。

注册中心

接下来继续看注册中心,要从 listen 哪里开始跟进,总之就是发布网络后处理一些请求的 JRMP 协议内容,最后调用到了 serviceCall 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728001052266.png

可以对 objectTable 进行了个获取,看看 target 里是什么

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728001207440.png

就是注册中心的 stub 嘛,然后继续看发现其分发器 disp 里有 skel 代理类

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728001420384.png

在该函数最下面调用了 disp.dispatch 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728001926785.png

然后继续看,skel 不是 null 满足条件执行 if 语句,调用 oldDispatch 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728002040385.png

在这个方法里面最后调用到了 skle 的 dispatch 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728002326968.png

跟踪进入

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728002416856.png

由于这个类是 class 文件,就简单分析一下吧,有很多 case,然后这里客户端调用的是 lookup 方法是 2

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728005250937.png

就是查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。

服务端

最后在看服务端是怎么处理的。

前面是差不多的就是在进行 skle 的条件判断的时候会是 false

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728004228271.png

不会执行 if 语句,继续向下走,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728003950323.png

发现其会把在客户端进行序列化的参数进行反序列化(我这里方法没有参数无法进行调试)。

最后进行方法调用。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728004504264.png

然后再把返回值进行序列化

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728004553675.png

至此就完美闭环了。

总结

RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:

  1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI 客户端反序列化 RMI 远程方法调用结果。

DGC

Distributed Garbage Collection,分布式垃圾回收

当 RMI 服务器返回一个对象到其客户端(远程方法的调用方)时,其跟踪远程对象在客户机中的使用。当再没有更多的对客户机上远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。

在前面远程对象注册时调用 put 时发现,在还没有 put 进行封装 target 时,里面已经存在一个 target 了。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728010247769.png

可以看见其 stub 是 DGCImpl_Stub 类,那这个是怎么创建的呢?可以看见在执行 putTarget 方法时有这么一串代码

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728010529480.png

因为这里的 dgclog 是个静态变量

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728010707360.png

在调用静态变量时会完成类的初始化,最后会创建代理类

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728011009610.png

从上面的 target 不难看出这里的 stub 创建更像注册中心中 stub 的创建,因为 DCGImpl 存在 DCGImpl_Stub,所以相同的还会创建 DGCImpl_Skel 对象。

Java提供了java.rmi.dgc.DGC接口,这个接口继承了Remote接口,定义了dirty和clean方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728011559527.png

看到 dirty 方法调用了 UnicastRef.invoke 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728011819683.png

剩下的就是里面反序列化了。然后再看服务端的 DCGImpl_Skel 中

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240728012104950.png

case1 或 2 就是对应的不同方法嘛,也是存在反序列化的。

参考:https://su18.org/post/rmi-attack/#2-攻击-registry-端
参考:https://nivi4.notion.site/Java-RMI-8eae42201b154ecc89455a480bcfc164
参考:https://xz.aliyun.com/t/9053?time__1311=n4%2BxnD0DuAiti%3DGkD9D0x05Sb%2BDOSYKaNTNaTek4D#toc-1