RMI基础

RMI基础

Remote Method Invocation(远程方法调用)

它是 Java 提供的一种用于实现分布式计算的 API

概述

  • RMI以分布式系统为背景,所以我们先说一下分布式系统

    1. 分布式系统的本质是拆分与连接:

      • 拆分:把功能拆成微服务,把数据拆分到不同数据库。
      • 连接:通过 RPC(如 RMI) 进行通信,通过序列化传递数据,通过共识协议达成一致。
    2. 分布式系统就是一组通过网络连接的、互相协作的计算机,但对用户来说,它们就像是一台计算机在提供服务。

    3. 为了在流量大的时候,多台机器可以协作

    4. 分布式系统的“三大基石“

      • CAP 定理

      C (Consistency) 一致性:所有节点在同一时刻看到的数据是一样的。

      A (Availability) 可用性:系统必须时刻能响应请求(不能卡死或报错)。

      P (Partition Tolerance) 分区容错性:如果网络断了(分区了),系统还能不能跑。

      • RPC(远程过程调用)

        分布式系统需要节点间通信(实现让 A 机器调用 B 机器就像调用本地函数一样简单)

      • 共识算法

      多台机器如何达成一致

      意见不统一的解决办法

    image-20260110222608143

  • RMI 依赖的通信协议

    为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。

  • Stub和Skeleton:

    RMI的客户端和服务器并不直接通信,客户与远程对象之间采⽤的代理⽅式进⾏ Socket 通信。为远程对象分别⽣成了客户端代理和服务端代理,
    其中位于客户端的代理类称为Stub即存根(包含服务器 Skeleton 信息),位于服务端的代理类称为 Skeleton即⻣⼲⽹。

  • RMI Registry:

    RMI注册表,默认监听在 1099端⼝上, Client 通过 Name向 RMI Registry 查询,得到这个绑定关系和对应的 Stub。

  • RMI流程图

    1. 首先是服务端创建远程对象,并注册到Registry注册表中

    2. 然后客户端访问服务端的Registry查找注册的远程对象,Registry返回RMI代理所需要的stub,客户端通过这个stub代理调用远程对象的方法,stub去和服务端的skeleton通信

      (注意,这里通信的数据是序列化后的数据,接收方会反序列化这个数据,也就是存在反序列化漏洞。)

    3. skeleton收到序列化数据并反序列化之后,调用远程对象的方法,并返回执行结果给stub,

      (注意,这个执行结果在发给stub之前,也是序列化后的数据,同上,也存在反序列化漏洞,最后stub把结果返回给Client)

    image-20260110223721329

实现

需要两个部分:Server & Client

Server (服务端)

  • 写一个接口类IRemoteObj

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package org.example;
    
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    public interface IRemoteObj extends Remote {
        public String sayHello(String keywords) throws RemoteException;
    
    }
  • 配置接口的实现RemoteObjImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package org.example;
    
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    
    public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
        //UnicastRemoteObject是通用远程对象(绑定到RMI就需要继承这个)
        protected RemoteObjImpl() throws RemoteException {
        }
        @Override
        public String sayHello(String keywords) {
            //转大写
            String unKeywords = keywords.toUpperCase();
            System.out.println(unKeywords);
            return  unKeywords;
        }
    }

    UnicastRemoteObject是通用远程对象(绑定到RMI就需要继承这个)

  • 写RMIServer启动服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package org.example;
    
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class RMIServer {
        public static void main(String[] args) throws RemoteException , AlreadyBoundException {
            IRemoteObj remoteObj = new RemoteObjImpl();//在服务端创建远程对象,实现与客户端通讯
            Registry r= LocateRegistry.createRegistry(1099);//注册中心(1099为固定端口)
            r.bind("remoteObj", remoteObj);//绑定服务,命名为“remoteObj“
        }
    
    }

    服务端创建远程对象之后,能连接到客户端,但是找不到客户端

    客户端只需要知道注册中心的 IP 和端口,所以需要创建这个注册中心作为中介,将服务端挂到注册中心上

Client(客户端)

  • 写一个接口类IRemoteObj

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package org.example;
    
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    public interface IRemoteObj extends Remote {
        public String sayHello(String keywords) throws RemoteException;
    
    }

    客户端必须拥有一份和服务端一模一样的接口文件

    • 类型安全:Java 是强类型语言。客户端需要知道远程对象有哪些方法、参数是什么类型、返回值是什么类型。

    • 生成代理(Stub):当客户端从注册表(Registry)查找服务时,RMI 会在本地动态创建一个实现了 IRemoteObj 的代理对象(Stub)。你的代码其实是和这个接口在交互。

  • 启动RMIClient,进行调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package org.example;
    
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class RMIClient {
        public static void main(String[] args) throws RemoteException, NotBoundException {
            //从端口(1099)获取注册中心
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
            //查找在服务端写的名字”remoteObj“
            IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
            //查到时候,进行调用
            remoteObj.sayHello("hello");
        }
    }

注意:

方法 谁来执行 作用
createRegistry(1099) 服务端 (Server) 创建并启动一个新的注册表服务。就像是在店里拉起一个货架。
getRegistry("IP", 1099) 客户端 (Client) 连接一个已经存在的注册表。就像是顾客走到货架前。

在Server & Client中,对于注册表函数是不一样的
一个是创建,一个是连接

测试

运行之后发现成功转大写

image-20260111153617711

引申的安全问题

  1. 客户端攻击服务端:反序列化漏洞&&服务端攻击客户端:恶意对象回传
  2. 注册中心攻击:类加载&& 动态类加载安全

流程原理

服务端

image-20260111174159604

第一层:注册中心,哈希表对应 名字———远程对象

第二层:服务端,发布远程对象

第三层:客户端,连接注册中心,调用远程对象

Stub和Skeleton是两个代理:服务端创建 Stub/Skeleton —-Stub 进注册中心—- 客户端领走 Stub —-Stub 连 Skeleton实现访问

创建远程服务

调试

加载(初始化阶段:静态代码块 / 静态变量)→ 每次实例化时先执行构造代码块 → 再执行构造函数

实例化对象时会首先触发类加载,在类加载的初始化阶段进行合并静态代码块和静态变量进行赋值操作,接着才会执行构造代码块,构造函数。

(注意静态变量和静态代码块初始化只执行一次)

所以在强制步入的时候,先进的是类加载,类加载结束最终到了构造函数

  • image-20260111174933967

  • 到类加载的静态

    image-20260111191546036

    image-20260111190543319

  • 到了远程对象的构造函数

    image-20260111191710591

先到父类的构造器(UnicastRemoteObject)

在构造函数之前先到父类的构造器

  • 默认存在super,先到他父类的

    image-20260111191907844

  • 接下来是父类的参数port赋值

    image-20260111191950802

    此处的目的是为了将远程服务调用在随机的端口(注意:此处不是1099,1099是注册器的默认端口。而现在是远程服务)

  • 然后到了exportObject这个静态函数

    image-20260111192247215

    目的就是为了导出对象

    因为我们继承了父类UnicastRemoteObject,这个里面包含了导出项目的这个函数exportObject

    未继承,就需要我们自己手写调用exportObject来实现导出

    分析一下exportObject的返回的两个参数吧

    obj是这个对象(RemoteObjImpl),也就是真正实现代码逻辑的

    UnicastServerRef是用来读取端口port,处理网络请求的,所以我们现在进到这个类看看

  • 进到UnicastServerRef

    image-20260111193512789

  • 创建了一个LiveRef的对象

    进到这个里面看看

    image-20260111193626765

    new 的ObjID是个id,没啥看的,那就直接通过this看构造函数

  • LiveRef构造函数

    image-20260111193755135

    分析参数:

    objID:就是前面那个id

    TCPEndpoint.getLocalEndpoint(port):tcp就是给网络协议

    所以可以进到TCPEndpoint看看

  • TCPEndpoint构造函数image-20260111194049006

    得到一个ip和端口就能处理请求

  • 查看同类(LiveRef)的其他构造函数,进到了最终的构造函数,到这里远程对象就创建好了

    image-20260111195023464

    可以看一下参数目前的赋值

    image-20260111195206300

    注意:最重要的函数是LiveRef。 最终的请求处理是在TCPTTransport中

  • 调用UnicastServerRef的父类

    • image-20260111195540900

    • 是UnicastRef

      image-20260111195712452

      在父类 UnicastRef 中赋值,即刚刚创建的 liveRef

    • 从UnicastServerRef出来

      image-20260111195917867

exportObject的构造函数

接下来看exportObject的构造函数

  • image-20260111200823127

    image-20260111200952842

    UnicastServerRef sref是我们刚刚看过的

    sref里面有LiveRef

  • 过完判断之后,走到了另一个类下的exportObject

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public Remote exportObject(Remote impl, Object data,
                               boolean permanent)
        throws RemoteException
    {
        Class<?> implClass = impl.getClass();
        Remote stub;//创建stub
    
        try {
            stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
        } catch (IllegalArgumentException e) {
            throw new ExportException(
                "remote object implements illegal remote interface", e);
        }
        if (stub instanceof RemoteStub) {
            setSkeleton(impl);
        }
    
        Target target =
            new Target(impl, this, stub, ref.getObjID(), permanent);
        ref.exportObject(target);
        hashToMethod_Map = hashToMethod_Maps.get(implClass);
        return stub;
    }

    这个地方创建了stub,后面走到了createProxy,是个代理

  • 进到createProxy

    image-20260111202023230

    image-20260111202016468

    implClass就是远程调用的类

    clientRef分装了ref,也就是LiveRef@786

  • 接下来走到判断

    image-20260111213216519

    stubClassExists(remoteClass)为真,则就往下走到return

  • 到这个函数stubClassExists里面看看

    image-20260111213404710

    后面的get.name+"_Stub"

    也就是说,存在则为真,还得有这个类_Stub

    _Stub是java自带的

    image-20260111213612409

  • 接下来就是创建动态代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    final ClassLoader loader = implClass.getClassLoader();
        final Class<?>[] interfaces = getRemoteInterfaces(implClass);
        final InvocationHandler handler =
            new RemoteObjectInvocationHandler(clientRef);
    
        /* REMIND: private remote interfaces? */
    
        try {
            return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
                public Remote run() {
                    return (Remote) Proxy.newProxyInstance(loader,
                                                           interfaces,
                                                           handler);
                }});
        } catch (IllegalArgumentException e) {
            throw new StubNotFoundException("unable to create proxy", e);
        }
    }

    Proxy.newProxyInstance添加拦截器

    loader类加载器

    interfaces接口

    handler处理器(此处的处理器就是ref,也就是LiveRef)

  • 下一步,创建target

    image-20260111220330349

    可以理解成一个所有东西的封装

    可以进去看一下:

    image-20260111220721261

    image-20260111220737828

TCPTransform处理网络请求

  • 发布target

    image-20260111220934623

  • 先调用LiveRef这个包下的exportObjectimage-20260111221301098

  • 再到TCPEndpoint这个包下的exportObject

    image-20260111221510267

  • 最后到TCPTransform的包下的exportObject

    image-20260111221058165

  • listen()可以开放端口

    先看listen()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    private void listen() throws RemoteException {
        assert Thread.holdsLock(this);
        TCPEndpoint ep = getEndpoint();
        int port = ep.getPort();
    
        if (server == null) {
            if (tcpLog.isLoggable(Log.BRIEF)) {
                tcpLog.log(Log.BRIEF,
                    "(port " + port + ") create server socket");
            }
    
            try {
                server = ep.newServerSocket();//创建新的ServerSocket
                /*
                 * Don't retry ServerSocket if creation fails since
                 * "port in use" will cause export to hang if an
                 * RMIFailureHandler is not installed.
                 */
                Thread t = AccessController.doPrivileged(
                    new NewThreadAction(new AcceptLoop(server),
                                        "TCP Accept-" + port, true));
                t.start();
            } catch (java.net.BindException e) {
                throw new ExportException("Port already in use: " + port, e);
            } catch (IOException e) {
                throw new ExportException("Listen failed on port: " + port, e);
            }
    
        } else {
            // otherwise verify security access to existing server socket
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                sm.checkListen(port);
            }
        }
    }
    • server = ep.newServerSocket();———–创建新的ServerSocket等待连接

      在创建新ServerSocket时候,如果port为0,则随机赋值

      image-20260111223143326

      • 创建了一个线程
      1
      2
      3
      4
      Thread t = AccessController.doPrivileged(
                   new NewThreadAction(new AcceptLoop(server),
                                       "TCP Accept-" + port, true));
               t.start();

      又创建了一个线程,为了连接之后的逻辑

  • 进到AcceptLoop看看

    image-20260111222520124

    重写run跑多线程

    新开的这个线程是网络请求的线程,上面那个线程是代码的线程

  • 接下来服务端要记录一下

    image-20260111223747452

    将 target(封装的全部远程对象的信息)put 进两个服务端这边静态的表进行存储

    是两个静态map

到目前来说,服务端将封装好的所有东西都发布到了一个随机的port上

服务端在执行 exportObject 导出远程对象时,会根据远程接口动态生成一个客户端代理 Stub(本质上是一个 Proxy 对象)。这个 Stub 会通过 Registry(注册中心) 传递给客户端。客户端拿到这个‘代理’后,调用上面的方法,Stub 会通过内部的 LiveRef 将请求通过 Socket 发送给服务端,从而间接调用真实对象。

创建注册中心(Registry)

image-20260112215034901

stub

  • 静态方法createRegistry

    image-20260112215453196

    通过RegistryImpl创建port端口

  • 看RegistryImpl

    image-20260112215920258

    走到这一步还是创建了UnicastServerRef的lref和LiveRef

    和上面说的创建远程服务类似

  • 进到setup里面

    image-20260112220150429

    可以看到和创建远程服务一样,都是调用了UnicastServerRef的exportObject

    但是:参数有区别

    • 自定义的创建远程服务(为false)

      image-20260111200823127

      永久的远程对象(为true)

      image-20260112220535240

      也就是说创建注册中心中的远程对象是永远存在的

  • 进到setup里面的exportObject

    image-20260112221001216

    这里的流程就和上面一样了,创建stub,动态代理

  • 进createProxy

    image-20260112221416103

    上面我们直接步过这一步,看的下面的动态代理以及创建线程

  • 这次我们进到createStub

    就是直接forname,进行创建Stub,创建的时候放的是ref

    image-20260205205739036

我们注意到两个创建远程服务和创建注册中心中的stub是不同的,但是本质里面存放的都是ref

Registry 一定使用 JDK 自带的 RegistryImpl_Stub;使用静态 stub
普通远程服务因为 JDK 不知道你的实现类,所以默认没有静态 stub,只能运行时动态生成代理 stub。

创建注册中心(Registry)是forname创建的stub,创建远程服务是动态代理创建的

Skel

服务端的Skeleton和客户端的stub

  • 接下来走createSkeleton

    创建服务端的Skeleton

    image-20260206191207985

    进去看看

    image-20260206191402611

    image-20260206191451762

  • 出来之后就到了target

    image-20260206200319506

    1
    2
    3
    if (stub instanceof RemoteStub) {
           setSkeleton(impl);
       }

    这段代码是为了impl里面的ref放一个skel对象

    image-20260206200446693

  • 创建target

    1
    new Target(impl, this, stub, ref.getObjID(), permanent);

    impl里面包含skel对象

    还放了stub

  • 接下来运行到putTarget

    image-20260206201056846

    创建table

    image-20260206202240960

  • 看table值

    image-20260206202820545

    table:是 RMI 的内部登记簿。

    所有都在远程调用都在table里面:目前是RMImoteObjImpl RegistryImpl DGCImpl

    DGCImpl:分布式垃圾回收(默认创建)

绑定

image-20260206203411205

  • 先检查

    image-20260206203556390

  • 再绑定

    image-20260206203616326

    逻辑是:若有就报异常(已经绑定),没有就接下来bindings.put(name, obj);(将name put到hashtable里面)

    此处这个name就是RMIServer命名,我命名为remoteObj

客户端

服务端创建完成之后,客户端调用

  1. 向注册中心获取远程对象的代理
  2. 通过获取的代理向服务端做远程调用

image-20260206204619120

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

  • 给ip和端口获取注册中心

    image-20260206205130204

    传入ip和端口到liveRef,然后封装一下

    调用 Util.createProxy和前面说的RegistryImpl_Stub一摸一样

    我们本来的想法是通过序列化反序列化把注册中心的stub给到客户端

    现在是利用再次创建一个一样参数的stub实现

  • 查找获取远程对象(获取服务端绑定到注册中心上的stub(动态代理))

    到了RegistryImpl_Stub.class的lookup

    image-20260213163521036

    内部逻辑:

    • 传入字符串(我们传入的名称remoteObj)

    • 写到输出流中 进行 序列化

    • 然后进行激活

      image-20260213163554338

      image-20260213163656825

    • 调用call.executeCall();

      这个是真正处理网络请求的

      image-20260213163837784

      此处

      image-20260213164526049

      这个地方也有readobject,存在异常从而利用反序列化 获取异常流

    • 接下来注册中心进行 反序列化

      image-20260213163942830

      也就是说,得到的var20就是远程对象的动态代理

    两个反序列化点:

    1. 客户端通过注册中心获取服务端远程对象的过程 利用的反序列化

    (恶意注册中心可以直接利用反序列化攻击客户端)

    1. call.executeCall()对于 存在的异常 利用反序列化 获取异常流,可以将使用恶意流

    (也叫JRMP协议)

    根据这两个恶意点可以看出,invoke是每一个都会调用(不只是lookup)所以都会有的call.executeCall()来处理网络请求,存在反序列化的恶意点

  • 最后获取对象remoteObj显示端口

    image-20260213165632754

    接下来从客户端连接服务端

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

  • 动态代理先到调用处理器里面的invoke方法

    image-20260213172515973

  • 到invokeRemoteMethod

    image-20260213172558015

  • 进去之后发现有一个ref.invoke(

    image-20260213172743486

    但是这是重载的方法

  • 进invoke看逻辑

    有一个marshalValue

    image-20260213185942580

    他的内部是writeObject序列化

    image-20260213190145058

    而序列化的参数就是我们传入的hello

    image-20260213190232136

  • 后面调用call.executeCall();

    image-20260213190314506

    也就是说:

    1
    2
    3
    4
    5
    stub 方法
    InvocationHandler.invoke
    UnicastRef.invoke
    call.executeCall() 
    只要触发远程方法调用,不论 stub 是静态还是动态代理,最终都会进入 UnicastRef.invoke 并执行 call.executeCall,从而把 payload 发往服务端。
  • 接下来如果有返回值的话

    调用unmarshalValue

    image-20260213191014739

    内部:

    image-20260213191049501

    相当于是把传入的值通过反序列化返回

两个反序列化点:

  1. JRMP协议(call.executeCall)
  2. 对应返回值(unmarshalValue)

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

(注册中心是一个特殊的服务端,看的是skel)

从服务端创建的过程之后,在服务端下断点调试

打断点时考虑 之前客户端代理是 stub,所以这里操作的是注册中心的代理 skel,分析RegistryImpl_Skel 类(注册中心处理客户端交互的逻辑就在这里)

  • 下面看是怎么走到 RegistryImpl_Skel 类:

    1. 发起网络请求的时候到了listen()

    2. 创建完毕好之后,放到target,在走listen()

    3. 开启新线程,里面的run方法

      里面有executeAcceptLoop(),到这个里面

      image-20260213204712659

      又创建了一个线程

    4. 进ConnectionHandler,看这个线程的run方法,里面调用run0()

    5. 调用handleMessages

      image-20260213204914083

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      void handleMessages(Connection conn, boolean persistent) {
              int port = getEndpoint().getPort();
      
              try {
                  DataInputStream in = new DataInputStream(conn.getInputStream());
                  do {
                      int op = in.read();     // transport op
                      if (op == -1) {
                          if (tcpLog.isLoggable(Log.BRIEF)) {
                              tcpLog.log(Log.BRIEF, "(port " +
                                  port + ") connection closed");
                          }
                          break;
                      }
      
                      if (tcpLog.isLoggable(Log.BRIEF)) {
                          tcpLog.log(Log.BRIEF, "(port " + port +
                              ") op = " + op);
                      }
      
                      switch (op) {
                      case TransportConstants.Call:
                          // service incoming RMI call
                          RemoteCall call = new StreamRemoteCall(conn);
                          if (serviceCall(call) == false)//默认
                              return;
                          break;
      
                      case TransportConstants.Ping:
                          // send ack for ping
                          DataOutputStream out =
                              new DataOutputStream(conn.getOutputStream());
                          out.writeByte(TransportConstants.PingAck);
                          conn.releaseOutputStream();
                          break;
      
                      case TransportConstants.DGCAck:
                          DGCAckHandler.received(UID.read(in));
                          break;
      
                      default:
                          throw new IOException("unknown transport op " + op);
                      }
                  } while (persistent);
      
              } catch (IOException e) {
                  // exception during processing causes connection to close (below)
                  if (tcpLog.isLoggable(Log.BRIEF)) {
                      tcpLog.log(Log.BRIEF, "(port " + port +
                          ") exception: ", e);
                  }
              } finally {
                  try {
                      conn.close();
                  } catch (IOException ex) {
                      // eat exception
                  }
              }
          }

      handleMessages 就是根据字段进入到不同 case,这里面默认进到第一个调用 serviceCall(call)

      • 进serviceCall

        获取Target,(serviceCall 里面 getTarget,从静态表中获取记录的 target(存放着所有的信息))

        image-20260213205227991

      • 接下来就是获取disp,从 target 中获取 skel 放到分发器 disp 里面,调用disp.dispatch(impl, call);

      • 进dispatch

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        public void dispatch(Remote obj, RemoteCall call) throws IOException {
                // positive operation number in 1.1 stubs;
                // negative version number in 1.2 stubs and beyond...
                int num;
                long op;
        
                try {
                    // read remote call header
                    ObjectInput in;
                    try {
                        in = call.getInputStream();
                        num = in.readInt();
                        if (num >= 0) {
                            if (skel != null) {
                                oldDispatch(obj, call, num);
                                return;
                            } else {
                                throw new UnmarshalException(
                                    "skeleton class not found but required " +
                                    "for client version");
                            }
                        }
                        op = in.readLong();
                    } catch (Exception readEx) {
                        throw new UnmarshalException("error unmarshalling call header",
                                                     readEx);
                    }
        
                    /*
                     * Since only system classes (with null class loaders) will be on
                     * the execution stack during parameter unmarshalling for the 1.2
                     * stub protocol, tell the MarshalInputStream not to bother trying
                     * to resolve classes using its superclasses's default method of
                     * consulting the first non-null class loader on the stack.
                     */
                    MarshalInputStream marshalStream = (MarshalInputStream) in;
                    marshalStream.skipDefaultResolveClass();
        
                    Method method = hashToMethod_Map.get(op);
                    if (method == null) {
                        throw new UnmarshalException("unrecognized method hash: " +
                            "method not supported by remote object");
                    }
        
                    // if calls are being logged, write out object id and operation
                    logCall(obj, method);
        
                    // unmarshal parameters
                    Class<?>[] types = method.getParameterTypes();
                    Object[] params = new Object[types.length];
        
                    try {
                        unmarshalCustomCallData(in);
                        for (int i = 0; i < types.length; i++) {
                            params[i] = unmarshalValue(types[i], in);
                        }
                    } catch (java.io.IOException e) {
                        throw new UnmarshalException(
                            "error unmarshalling arguments", e);
                    } catch (ClassNotFoundException e) {
                        throw new UnmarshalException(
                            "error unmarshalling arguments", e);
                    } finally {
                        call.releaseInputStream();
                    }
        
                    // make upcall on remote object
                    Object result;
                    try {
                        result = method.invoke(obj, params);
                    } catch (InvocationTargetException e) {
                        throw e.getTargetException();
                    }
        
                    // marshal return value
                    try {
                        ObjectOutput out = call.getResultStream(true);
                        Class<?> rtype = method.getReturnType();
                        if (rtype != void.class) {
                            marshalValue(rtype, result, out);
                        }
                    } catch (IOException ex) {
                        throw new MarshalException("error marshalling return", ex);
                        /*
                         * This throw is problematic because when it is caught below,
                         * we attempt to marshal it back to the client, but at this
                         * point, a "normal return" has already been indicated,
                         * so marshalling an exception will corrupt the stream.
                         * This was the case with skeletons as well; there is no
                         * immediately obvious solution without a protocol change.
                         */
                    }
                } catch (Throwable e) {
                    logCallException(e);
        
                    ObjectOutput out = call.getResultStream(false);
                    if (e instanceof Error) {
                        e = new ServerError(
                            "Error occurred in server thread", (Error) e);
                    } else if (e instanceof RemoteException) {
                        e = new ServerException(
                            "RemoteException occurred in server thread",
                            (Exception) e);
                    }
                    if (suppressStackTraces) {
                        clearStackTraces(e);
                    }
                    out.writeObject(e);
                } finally {
                    call.releaseInputStream(); // in case skeleton doesn't
                    call.releaseOutputStream();
                }
            }

        然后 serviceCall 方法中调用 disp.dispatch(impl, call)

        (在里面 skel 不为 null 进入 if 调 oldDispatch, 传的第三个参数 num 为 2)

      • 进oldDispatch调skel.dispatch

        image-20260213211113765

  • 查看函数dispatch的逻辑(在RegistryImpl_Skel)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
            if (var4 != 4905912898345647071L) {
                throw new SkeletonMismatchException("interface hash mismatch");
            } else {
                RegistryImpl var6 = (RegistryImpl)var1;
                switch (var3) {
                    case 0:
                        String var100;
                        Remote var103;
                        try {
                            ObjectInput var105 = var2.getInputStream();
                            var100 = (String)var105.readObject();
                            var103 = (Remote)var105.readObject();
                        } catch (IOException var94) {
                            throw new UnmarshalException("error unmarshalling arguments", var94);
                        } catch (ClassNotFoundException var95) {
                            throw new UnmarshalException("error unmarshalling arguments", var95);
                        } finally {
                            var2.releaseInputStream();
                        }
    
                        var6.bind(var100, var103);
    
                        try {
                            var2.getResultStream(true);
                            break;
                        } catch (IOException var93) {
                            throw new MarshalException("error marshalling return", var93);
                        }
                    case 1:
                        var2.releaseInputStream();
                        String[] var99 = var6.list();
    
                        try {
                            ObjectOutput var102 = var2.getResultStream(true);
                            var102.writeObject(var99);
                            break;
                        } catch (IOException var92) {
                            throw new MarshalException("error marshalling return", var92);
                        }
                    case 2:
                        String var98;
                        try {
                            ObjectInput var104 = var2.getInputStream();
                            var98 = (String)var104.readObject();
                        } catch (IOException var89) {
                            throw new UnmarshalException("error unmarshalling arguments", var89);
                        } catch (ClassNotFoundException var90) {
                            throw new UnmarshalException("error unmarshalling arguments", var90);
                        } finally {
                            var2.releaseInputStream();
                        }
    
                        Remote var101 = var6.lookup(var98);
    
                        try {
                            ObjectOutput var9 = var2.getResultStream(true);
                            var9.writeObject(var101);
                            break;
                        } catch (IOException var88) {
                            throw new MarshalException("error marshalling return", var88);
                        }
                    case 3:
                        Remote var8;
                        String var97;
                        try {
                            ObjectInput var11 = var2.getInputStream();
                            var97 = (String)var11.readObject();
                            var8 = (Remote)var11.readObject();
                        } catch (IOException var85) {
                            throw new UnmarshalException("error unmarshalling arguments", var85);
                        } catch (ClassNotFoundException var86) {
                            throw new UnmarshalException("error unmarshalling arguments", var86);
                        } finally {
                            var2.releaseInputStream();
                        }
    
                        var6.rebind(var97, var8);
    
                        try {
                            var2.getResultStream(true);
                            break;
                        } catch (IOException var84) {
                            throw new MarshalException("error marshalling return", var84);
                        }
                    case 4:
                        String var7;
                        try {
                            ObjectInput var10 = var2.getInputStream();
                            var7 = (String)var10.readObject();
                        } catch (IOException var81) {
                            throw new UnmarshalException("error unmarshalling arguments", var81);
                        } catch (ClassNotFoundException var82) {
                            throw new UnmarshalException("error unmarshalling arguments", var82);
                        } finally {
                            var2.releaseInputStream();
                        }
    
                        var6.unbind(var7);
    
                        try {
                            var2.getResultStream(true);
                            break;
                        } catch (IOException var80) {
                            throw new MarshalException("error marshalling return", var80);
                        }
                    default:
                        throw new UnmarshalException("invalid method number");
                }
    
            }
        }

    可以看到不同的case对应的不同的方法

  • 目前我们用到的是case2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    case 2:
                        String var98;
                        try {
                            ObjectInput var104 = var2.getInputStream();
                            var98 = (String)var104.readObject();
                        } catch (IOException var89) {
                            throw new UnmarshalException("error unmarshalling arguments", var89);
                        } catch (ClassNotFoundException var90) {
                            throw new UnmarshalException("error unmarshalling arguments", var90);
                        } finally {
                            var2.releaseInputStream();
                        }
    
                        Remote var101 = var6.lookup(var98);
    
                        try {
                            ObjectOutput var9 = var2.getResultStream(true);
                            var9.writeObject(var101);
                            break;
                        } catch (IOException var88) {
                            throw new MarshalException("error marshalling return", var88);
                        }
    1. 上面提到服务端的查找的名字remoteObj传入注册中心利用的序列化

      此处通过反序列化读取

    2. 其他case还有很多

反序列化点:

  1. 反序列化查找 注册中心中的名字
  2. 其他case中也存在readobject,对应方法进行触发

客户端请求服务端

客户端在服务端调用远程方法,服务端的处理

  • 跟前面一样都是先 进oldDispatch调skel.dispatch

    image-20260218221222985

  • 看dispatch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public void dispatch(Remote obj, RemoteCall call) throws IOException {
        // positive operation number in 1.1 stubs;
        // negative version number in 1.2 stubs and beyond...
        int num;
        long op;
    
        try {
            // read remote call header
            ObjectInput in;
            try {
                in = call.getInputStream();
                num = in.readInt();
                if (num >= 0) {
                    if (skel != null) {
                        oldDispatch(obj, call, num);
                        return;
                    } else {
                        throw new UnmarshalException(
                            "skeleton class not found but required " +
                            "for client version");
                    }
                }
                op = in.readLong();
            } catch (Exception readEx) {
                throw new UnmarshalException("error unmarshalling call header",
                                             readEx);
            }

    注意,此处skel是空的 直接就到了 op = in.readLong();

  • 再后面就是获取远程方法sayHello

    image-20260218225814340

  • 将传入的参数反序列化

    image-20260218230026747

    unmarshalValue内部:

    image-20260213191049501

  • 然后远程调用就结束了

    接下来是对于返回值的 序列化传值,反序列化接收

两个反序列化点:

  1. UnicastServerRef.dispath 中 unmarshalValue 反序列化客户端传的参数造成攻击
  2. 对于返回值的 序列化传值,反序列化接收也可造成攻击

dgc(分布式垃圾回收)

这个在服务端创建注册中心 提到

客户端请求服务端

  • 接服务端创建注册中心提到的putTarget

    image-20260218231001985

    也说到过,我们理论上是有两个Target,但是又多了一个,他的stub是DGCImpl

    在创建RMImoteObjImpl之前,先创建了dgc的代理

  • 先创建

    image-20260218234203371

    由于dgcLog是一个静态元素、

    image-20260218234938285

    访问类的静态变量时,如果类尚未初始化,JVM 会初始化整个类,执行所有静态代码块和静态变量初始化。

  • 初始化,调用静态代码块

    image-20260218235132109

    接下来的代码类似创建注册中心的stub

    • 进createProxy

      image-20260218235937757

    • 进createStub

      image-20260219000038029

      和创建注册中心的stub套路就一样了

      有没有 xxxxx_stub? 有的兄弟有的:image-20260219000239975

      接下来的就和注册中心的一样了,就是端口不一样(注册服务 远程回收服务)

  • 然后就是disp进行调用

    从 target 中获取 skel 放到分发器 disp 里面,调用disp.dispatch(,进oldDispatch调skel.dispatch

  • 接下来就是看看DGCImpl_Stub和DGCImpl_skel的区别

    • DGCImpl_Stub

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      //
      // Source code recreated from a .class file by IntelliJ IDEA
      // (powered by FernFlower decompiler)
      //
      
      package sun.rmi.transport;
      
      import java.io.IOException;
      import java.io.ObjectInput;
      import java.io.ObjectOutput;
      import java.rmi.MarshalException;
      import java.rmi.RemoteException;
      import java.rmi.UnexpectedException;
      import java.rmi.UnmarshalException;
      import java.rmi.dgc.DGC;
      import java.rmi.dgc.Lease;
      import java.rmi.dgc.VMID;
      import java.rmi.server.ObjID;
      import java.rmi.server.Operation;
      import java.rmi.server.RemoteRef;
      import java.rmi.server.RemoteStub;
      import java.rmi.server.UID;
      import java.util.ArrayList;
      import sun.misc.ObjectInputFilter;
      import sun.misc.ObjectInputFilter.Status;
      import sun.rmi.transport.tcp.TCPConnection;
      
      public final class DGCImpl_Stub extends RemoteStub implements DGC {
          private static final Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")};
          private static final long interfaceHash = -669196253586618813L;
          private static int DGCCLIENT_MAX_DEPTH = 6;
          private static int DGCCLIENT_MAX_ARRAY_SIZE = 10000;
      
          public DGCImpl_Stub() {
          }
      
          public DGCImpl_Stub(RemoteRef var1) {
              super(var1);
          }
      
          public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
              try {
                  StreamRemoteCall var6 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, -669196253586618813L);
                  var6.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
      
                  try {
                      ObjectOutput var7 = var6.getOutputStream();
                      var7.writeObject(var1);
                      var7.writeLong(var2);
                      var7.writeObject(var4);
                      var7.writeBoolean(var5);
                  } catch (IOException var8) {
                      throw new MarshalException("error marshalling arguments", var8);
                  }
      
                  this.ref.invoke(var6);
                  this.ref.done(var6);
              } catch (RuntimeException var9) {
                  throw var9;
              } catch (RemoteException var10) {
                  throw var10;
              } catch (Exception var11) {
                  throw new UnexpectedException("undeclared checked exception", var11);
              }
          }
      
          public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
              try {
                  StreamRemoteCall var5 = (StreamRemoteCall)this.ref.newCall(this, operations, 1, -669196253586618813L);
                  var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
      
                  try {
                      ObjectOutput var6 = var5.getOutputStream();
                      var6.writeObject(var1);
                      var6.writeLong(var2);
                      var6.writeObject(var4);
                  } catch (IOException var16) {
                      throw new MarshalException("error marshalling arguments", var16);
                  }
      
                  this.ref.invoke(var5);
                  Connection var7 = var5.getConnection();
      
                  Lease var22;
                  try {
                      ObjectInput var8 = var5.getInputStream();
                      var22 = (Lease)var8.readObject();
                  } catch (IOException | ClassNotFoundException | ClassCastException var17) {
                      if (var7 instanceof TCPConnection) {
                          ((TCPConnection)var7).getChannel().free(var7, false);
                      }
      
                      var5.discardPendingRefs();
                      throw new UnmarshalException("error unmarshalling return", var17);
                  } finally {
                      this.ref.done(var5);
                  }
      
                  return var22;
              } catch (RuntimeException var19) {
                  throw var19;
              } catch (RemoteException var20) {
                  throw var20;
              } catch (Exception var21) {
                  throw new UnexpectedException("undeclared checked exception", var21);
              }
          }
      
          private static ObjectInputFilter.Status leaseFilter(ObjectInputFilter.FilterInfo var0) {
              if (var0.depth() > (long)DGCCLIENT_MAX_DEPTH) {
                  return Status.REJECTED;
              } else {
                  Class var1 = var0.serialClass();
                  if (var1 == null) {
                      return Status.UNDECIDED;
                  } else {
                      while(var1.isArray()) {
                          if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGCCLIENT_MAX_ARRAY_SIZE) {
                              return Status.REJECTED;
                          }
      
                          var1 = var1.getComponentType();
                      }
      
                      if (var1.isPrimitive()) {
                          return Status.ALLOWED;
                      } else {
                          return var1 != UID.class && var1 != VMID.class && var1 != Lease.class && (!Throwable.class.isAssignableFrom(var1) || var1.getClassLoader() != Object.class.getClassLoader()) && var1 != StackTraceElement.class && var1 != ArrayList.class && var1 != Object.class && !var1.getName().equals("java.util.Collections$UnmodifiableList") && !var1.getName().equals("java.util.Collections$UnmodifiableCollection") && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList") ? Status.REJECTED : Status.ALLOWED;
                      }
                  }
              }
          }
      }

      两个反序列化点:

      1. 调用invoke里面的call.executeCall()(JRMP协议)image-20260219001342316

        所有客户端stub都会被反序列化攻击

      2. 反序列化之后, 转化为Lease类型image-20260219001800110

    • DGCImpl_skel

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      //
      // Source code recreated from a .class file by IntelliJ IDEA
      // (powered by FernFlower decompiler)
      //
      
      package sun.rmi.transport;
      
      import java.io.IOException;
      import java.io.ObjectInput;
      import java.io.ObjectOutput;
      import java.rmi.MarshalException;
      import java.rmi.Remote;
      import java.rmi.UnmarshalException;
      import java.rmi.dgc.Lease;
      import java.rmi.dgc.VMID;
      import java.rmi.server.ObjID;
      import java.rmi.server.Operation;
      import java.rmi.server.RemoteCall;
      import java.rmi.server.Skeleton;
      import java.rmi.server.SkeletonMismatchException;
      
      public final class DGCImpl_Skel implements Skeleton {
          private static final Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")};
          private static final long interfaceHash = -669196253586618813L;
      
          public Operation[] getOperations() {
              return (Operation[])operations.clone();
          }
      
          public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
              if (var4 != -669196253586618813L) {
                  throw new SkeletonMismatchException("interface hash mismatch");
              } else {
                  DGCImpl var6 = (DGCImpl)var1;
                  StreamRemoteCall var7 = (StreamRemoteCall)var2;
                  switch (var3) {
                      case 0:
                          ObjID[] var34;
                          long var35;
                          VMID var36;
                          boolean var38;
                          try {
                              ObjectInput var39 = var7.getInputStream();
                              var34 = (ObjID[])var39.readObject();
                              var35 = var39.readLong();
                              var36 = (VMID)var39.readObject();
                              var38 = var39.readBoolean();
                          } catch (IOException | ClassNotFoundException | ClassCastException var32) {
                              var7.discardPendingRefs();
                              throw new UnmarshalException("error unmarshalling arguments", var32);
                          } finally {
                              var7.releaseInputStream();
                          }
      
                          var6.clean(var34, var35, var36, var38);
      
                          try {
                              var7.getResultStream(true);
                              break;
                          } catch (IOException var31) {
                              throw new MarshalException("error marshalling return", var31);
                          }
                      case 1:
                          ObjID[] var8;
                          long var9;
                          Lease var11;
                          try {
                              ObjectInput var12 = var7.getInputStream();
                              var8 = (ObjID[])var12.readObject();
                              var9 = var12.readLong();
                              var11 = (Lease)var12.readObject();
                          } catch (IOException | ClassNotFoundException | ClassCastException var29) {
                              var7.discardPendingRefs();
                              throw new UnmarshalException("error unmarshalling arguments", var29);
                          } finally {
                              var7.releaseInputStream();
                          }
      
                          Lease var37 = var6.dirty(var8, var9, var11);
      
                          try {
                              ObjectOutput var13 = var7.getResultStream(true);
                              var13.writeObject(var37);
                              break;
                          } catch (IOException var28) {
                              throw new MarshalException("error marshalling return", var28);
                          }
                      default:
                          throw new UnmarshalException("invalid method number");
                  }
      
              }
          }
      }

      两个反序列化点:

      1. case0

      image-20260219002145777

      1. case1image-20260219002155781

攻击汇总

  1. 攻击客户端:

    • 客户端查询远程对象时:注册中心攻击客户端 RegistryImpl_Stub.lookup(xxx)
    • 客户端反序列化服务端返回的方法结果:服务端攻击客户端 UnicastRef.invoke() (JRMP协议)
    • DGC 服务端攻击客户端 DGCImpl_Stub.dirty()
    • 客户端发起网络请求后反序列化服务端返回的恶意对象:call.executeCall() (JRMP协议)
  2. 攻击注册中心:

    • 客户端获取远程对象时:客户端攻击注册中心 RegistryImpl_Skel.dispatch(xxx)
  3. 攻击服务端:

    • 客户端传递方法参数时:客户端攻击服务端 UnicastServerRef.dispatch(xxx)

    • DGC 客户端 攻击服务端 DGCImpl_Skel.dispatch()


RMI基础
http://example.com/2026/03/08/RMI基础/
作者
Piggy Sprint
发布于
2026年3月8日
许可协议