java安全-RMI调用逻辑

一、RMI

1.简单了解

**RMI(Remote Method Invocation,远程方法调用)**是 Java 提供的一种核心机制,它允许运行在一个 Java 虚拟机(JVM)上的对象,调用运行在另一个 JVM 上的对象的方法,就像调用本地对象一样

简单来说,它的目的是让分布式 Java 应用之间的通信变得对开发者透明

RMI 的核心运行机制

RMI 的底层通信依赖于网络(通常是 TCP/IP),并且重度依赖 **Java 序列化(Serialization)**机制来在网络中传输对象。它的标准工作流程如下:

  1. RMI Registry(注册中心): 服务端启动时,会将自己提供的远程对象注册到一个叫做 RMI Registry 的地方,并绑定一个名字(类似电话簿)
  2. Stub(客户端存根): 客户端通过名字去注册中心查找,拿到一个 Stub 对象。这个 Stub 就像是服务端对象在客户端的“代理”
  3. 网络传输: 当客户端调用 Stub 的方法时,Stub 会把调用的方法名、参数等信息进行序列化,通过网络发送给服务端
  4. Skeleton(服务端骨架): 服务端接收到网络请求后,Skeleton(在较新的 Java 版本中已弱化为内部机制)会反序列化这些数据,找到真正的服务端对象并执行方法
  5. 返回结果: 执行完毕后,结果再次被序列化,原路返回给客户端的 Stub,最后解包交给客户端代码

2.创建demo

服务端

chuan/rmi/RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 实例化远程对象
RemoteObj remoteObj = new RemoteObjImpl();
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定对象示例到注册中心
registry.bind("remoteObj", remoteObj);

}
}

chuan/rmi/RemoteObj.java

1
2
3
4
5
public interface RemoteObj extends Remote {

public String sayHello(String keywords) throws RemoteException;

}

chuan/rmi/RemoteObjImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {

public RemoteObjImpl() throws RemoteException {
//UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}
@Override
public String sayHello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}

客户端

chuan/rmi/RemoteObj.java

1
2
3
4
public interface RemoteObj extends Remote {

public String sayHello(String keywords) throws RemoteException;
}

chuan/rmi/RMIClient.java

1
2
3
4
5
6
7
8
9
10
11
public class RMIClient {
public static void main(String[] args) throws Exception {

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

RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");

remoteObj.sayHello("hello");

}
}

3.通信原理

第一次 TCP 连接:客户端与 Registry 通信

  1. 客户端发起连接: 客户端连接到 RMI Registry(默认端口 1099)
  2. 发送查询: 客户端发送 Call 消息(调用 lookup 方法),寻找 Name 为 hello 的绑定关系
  3. 返回存根: Registry 找到对应的绑定关系,将一个包含 RMI Server 地址和随机端口(例如你提到的 172.17.88.209:24429)的 Stub(存根)对象序列化后,通过 ReturnData 消息发回给客户端

第二次 TCP 连接:客户端与 RMI Server 通信

  1. 客户端解析存根: 客户端反序列化拿到的 Stub 对象,提取出真正的 RMI Server 的 IP 和端口(172.17.88.209:24429
  2. 客户端发起新连接: 客户端根据提取出的地址,主动与 RMI Server 建立第二次 TCP 连接
  3. 发送调用请求: 在这个新连接中,客户端向服务端发送 Call 消息,告诉服务端:“我要执行 sayHello() 方法,这是我的参数”
  4. 服务端执行并返回: 服务端接收到 Call 消息,反序列化参数,真正执行本地的 sayHello() 方法,最后将结果序列化,通过 ReturnData 返回给客户端

img

二、rmi服务端

1.创建远程服务对象

src/chuan/rmi/RMIServer.java

1
RemoteObj remoteObj = new RemoteObjImpl();

接着调用RemoteObjImpl构造函数

src/chuan/rmi/RemoteObjImpl.java

1
2
3
public RemoteObjImpl() throws RemoteException {
//UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}

由于其继承UnicastRemoteObject类,所以接着调用父类无参构造器

java/rmi/server/UnicastRemoteObject.java

1
2
3
4
5
6
7
8
9
10
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}

protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port); //这里的this,指的是当前实例化的对象chuan/rmi/RemoteObjImpl.java
}

1.1、一层exportObject

调用无参构造器默认port参数为0,在之后这个值为任意数,也就是默认发布到一个随机的端口

接着调用exportObject方法

java/rmi/server/UnicastRemoteObject.java

1
2
3
4
5
6
public static Remote exportObject(Remote obj, int port)   //这里的传来的obj,指的是当前实例化的对象chuan/rmi/RemoteObjImpl.java

throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

先创建了UnicastServerRef类,构造器如下

sun/rmi/server/UnicastServerRef.java

1
2
3
4
5
6
7
8
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

父类sun/rmi/server/UnicastRef.java
public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

依旧创建一个类LiveRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public LiveRef(int port) {
this((new ObjID()), port);
}

public LiveRef(ObjID objID, int port) {
this(objID, TCPEndpoint.getLocalEndpoint(port), true);
//这里就是获取当前网络请求点
}

public LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal) {
ep = endpoint;
id = objID;
this.isLocal = isLocal;
}

链式创建出LiveRef类具体如下图

image-20260217222029826

接着赋值回UnicastServerRef类的ref属性,最终就是返回一个属性ref为上面创建出来的LiveRef类的UnicastServerRef

image-20260217222819837

image-20260217223314440

1.2、二层exportObject

接着之后return调用本类的同名方法

java/rmi/server/UnicastRemoteObject.java

1
2
3
4
5
6
7
8
9
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}

((UnicastRemoteObject) obj).ref = sref;这里将我们要实例化的RemoteObjImpl转为父类UnicastRemoteObject

这里父类又有很多继承关系

1
2
public class UnicastRemoteObject extends RemoteServer {
public abstract class RemoteServer extends RemoteObject{

RemoteObject中存在ref属性,本质上就是将RemoteObjImplref赋值为我们之前的LiveRef

1.3、三层exportObject

接着调用sref.exportObject,这里的sref便是我们之前提到的UnicastServerRef,此时他的ref属性为LiveRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;

try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

这里的主要逻辑就是创建了stub也就是客户端的代理对象,具体创建过程如下

1
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);

implClass这个是RemoteObjImpl类,getClientRef()获取的就是一个包装ref的对象,forceStubUsefalse

1
2
3
protected RemoteRef getClientRef() {
return new UnicastRef(ref);
}
image-20260217225031210

应该相同,指的是同一个类,我这里只不过是重新启动了调试

之后具体处理流程如图

image-20260217225511532

**方式1:**判断条件为stubClassExists(remoteClass)为真时成立,具体逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

判断本机是否存在类名+_Stub后缀文件,存在即真

**方式2:**反射创建代理对象,handler构造还用LiveRef

1
2
final InvocationHandler handler =
new RemoteObjectInvocationHandler(clientRef);

clientRefref属性为LiveRef

image-20260217230707366

stub创建完成之后

image-20260217231013757

第一步之后用到,不考虑

第二步便是构造一个总的Target对象,implRemoteObjImplthisUnicastServerRefstub为代理对象,ref.getObjID(), permanent为之前定好的

并将其存入发布到LiveRef,返回代理对象

这里创建stub

1
2
3
1. 先在服务端创建一个 Stub
2. 把 Stub 传到 RMI Registry 中
3. 最后让 RMI Client 去获取 Stub

1.4、LiveRef#exportObject

这里的返回又有说法

1
ref.exportObject(target);

ref依旧是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
sun/rmi/transport/LiveRef.java
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

ep为sun/rmi/transport/tcp/TCPEndpoint.java
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

transport为sun/rmi/transport/tcp/TCPTransport.java
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方法详解

image-20260217233319629

同时,这里的newServerSocket方法会处理端口问题

image-20260217233545755

super.exportObject(target);详解

1
super.exportObject(target);

sun/rmi/transport/Transport.java

1
2
3
4
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target); //这里记录
}

sun/rmi/transport/ObjectTable.java

1
2
3
4
static void putTarget(Target target) throws ExportException {
objTable.put(oe, target);
implTable.put(weakImpl, target);
}

意思为将我们每次创建的target放入俩个静态的表objTableimplTable,以方便服务端获取识别

由此创建完毕,将信息返回即可

2.创建注册中心

src/chuan/rmi/RMIServer.java

1
Registry registry = LocateRegistry.createRegistry(1099);

java/rmi/registry/LocateRegistry.java

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

sun/rmi/registry/RegistryImpl.java

1
2
3
4
5
public RegistryImpl(int port)
throws RemoteException{
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}

这里和创建远程服务时逻辑相同,不过这里先利用1099默认端口创建了LiveRef,将其作为参数传入UnicastServerRef,并且将其属性ref设置为LiveRef

接着将创建出来的UnicastServerRef作为参数传入RegistryImpl#setup方法

sun/rmi/registry/RegistryImpl.java

1
2
3
4
5
6
7
8
9
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

这里就是调用了 UnicastServerRef 的 exportObject 方法,本质上就是创建远程对象时的第二层 exportObject 方法

不同的是,传入的permanent参数这回为true

image-20260218121142780

接着进入创建远程服务对象的第三层exportObject逻辑

2.1、创建stub

创建stub并返回

image-20260218121820391

注册中心与远程服务端对象主要区别就是在创建stub的过程

image-20260218122400668

stubClassExists具体逻辑如下

image-20260218122451448

本地是有这个静态文件的,所以判断为true,也就走到了sun/rmi/server/Util.java#createStub

image-20260218122537502

sun/rmi/server/Util.java

1
2
3
4
5
6
7
8
9
10
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
String stubname = remoteClass.getName() + "_Stub";

try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });

这里代理stub本质上也就是创建了一个参数ref为我们的关键对象LiveRef的类

2.2、创建Skel

image-20260218123817696

接着

sun/rmi/server/UnicastServerRef#setSkeleton

1
2
3
4
5
6
7
8
9
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);
}
}
}
image-20260218124056449

这一步,将sun/rmi/server/UnicastServerRef.javaskel字段创建并且赋值

此时

1
2
3
4
5
UnicastServerRef
ref -> UnicastServerRef
ref -> LiveRef
skel -> RegistryImpl_Skel
stub -> null

2.3、创建target

与服务端不同的是,此时impl中存在属性skel

image-20260218125528784

2.4、加入静态表

sun/rmi/server/UnicastServerRef#exportObject

1
ref.exportObject(target);

sun/rmi/transport/LiveRef#exportObject

1
2
3
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

sun/rmi/transport/tcp/TCPEndpoint.java

1
2
3
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

sun/rmi/transport/tcp/TCPTransport.java

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

sun/rmi/transport/Transport.java

1
2
3
4
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

image-20260218131137473

可以在最终的objTable静态列表完成终极对比

3.注册中心绑定

chuan/rmi/RMIServer.java

1
registry.bind("remoteObj", remoteObj);

sun/rmi/registry/RegistryImpl.java

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

checkAccess("Registry.bind");

必要安全检查

1
2
3
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);

检查是否绑定过此远程对象

1
bindings.put(name, obj);

本质上就是向静态hash表写入我们的代理对象

image-20260218132247002

三、rmi客户端

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

1.1、获取RegistryImpl_Stub

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

调用

java/rmi/registry/LocateRegistry.java

1
2
3
4
5
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}

调用

java/rmi/registry/LocateRegistry.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

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);
}

最后通过Util.createProxy这个方法创建了一个和注册中心创建的一样的stub

image-20260218201349757

1.2、获取远程对象

1
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");

调用

sun\rmi\registry\RegistryImpl_Stub.classlookup方法

一共有三个注意点

image-20260218223648155
  • 第一个点是将字符串序列化传入

  • 第三个点是指,反序列化获取读取到的的动态代理对象

  • 第二个方法调用

sun/rmi/server/UnicastRef.java

1
2
3
4
5
6
7
8
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");

call.executeCall(); //这里

} catch (RemoteException e) {
/*

接着调用

sun/rmi/transport/StreamRemoteCall.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void executeCall() throws Exception {
byte returnType;

// read result header
DGCAckHandler ackHandler = null;
try {
if (out != null) {
ackHandler = out.getDGCAckHandler();
}
releaseOutputStream();
DataInputStream rd = new DataInputStream(conn.getInputStream());
byte op = rd.readByte();
if (op != TransportConstants.Return) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"transport return code invalid: " + op);
}
throw new UnmarshalException("Transport return code invalid");
}

这一块的代码是主要的通信加载方法,同时在代码下方逻辑处依旧会有问题

image-20260218224154721

而且这个点更为隐蔽,甚至更加危险通用,只要是存在 invoke 方法,就有可能调用这里

方法最终获取到stub代理对象

image-20260218225057668

2.请求服务端-客户端

1
remoteObj.sayHello("hello");

remoteObj是一个动态代理类,所以会走到invoke方法

java/rmi/server/RemoteObjectInvocationHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object invoke(Object proxy, Method method, Object[] args)

if (method.getDeclaringClass() == Object.class) {

return invokeObjectMethod(proxy, method, args);

} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
!allowFinalizeInvocation) {
return null; // ignore
} else {
return invokeRemoteMethod(proxy, method, args);
}
}

接着调用invokeRemoteMethod

image-20260218225922350

进一步调用invoke,此时refsun/rmi/server/UnicastRef.java

这个invoke中逻辑较多

image-20260218230143124

image-20260218230230354

这里通过判断,如果有返回值的话,会将返回值反序列化

image-20260218230446710

这里这个 executeCall 方法,就是处理通常提到的 JRMP 协议

image-20260218230746082

3.请求注册中心-注册中心

接第一层,创建注册中心时调用

1
ref.exportObject(target);

这里最终会调用到sun/rmi/transport/tcp/TCPTransport.javalisten方法

listen方法会开启socket监听,并且创建一个新的线程

image-20260219110513967

进一步查看线程处理情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AcceptLoop(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
}

public void run() {
try {
executeAcceptLoop();
} finally {
try {
serverSocket.close();
} catch (IOException e) {
}
}
}

启动线程时,是启动线程的run方法,这里继续深入

sun/rmi/transport/tcp/TCPTransport#executeAcceptLoop

image-20260219110930160

sun/rmi/transport/tcp/TCPTransport$ConnectionHandler#run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void run() {
Thread t = Thread.currentThread();
String name = t.getName();
try {
t.setName("RMI TCP Connection(" +
connectionCount.incrementAndGet() +
")-" + remoteHost);
AccessController.doPrivileged((PrivilegedAction<Void>)() -> {

run0();//这里

return null;
}, NOPERMS_ACC);
} finally {
t.setName(name);
}
}

接着调用sun/rmi/transport/tcp/TCPTransport$ConnectionHandler#run0

image-20260219111542512

此方法接着调用handleMessages

image-20260219111803039

3.1、具体的serviceCall方法

默认的TransportConstants会调用serviceCall

image-20260219114548323

接着sun/rmi/server/UnicastServerRef#dispatch

image-20260219114853747

因为是注册中心,当然对应的sekl不为空,调用oldDispatch方法

image-20260219115905800

D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\sun\rmi\registry\RegistryImpl_Skel.class

image-20260219121056969

这里主要用到了 case2 就是将传入的参数反序列化,因此这里便是一个反序列化漏洞点

4.请求服务端-服务端

前方网络依旧相同,这里到了serviceCall出现分歧

这里获取的target是我们的代理类

image-20260219122333442

进一步的,后面调用的dispatch方法,也会产生分歧

image-20260219122806492

image-20260219122934572

image-20260219123055897

image-20260219123449671

5.dgc垃圾回收机制

5.1、创建

服务端创建出target对象,将其放入静态表之前时,会进行dgc target加载

就是在第四层listen方法结束之后,将target放入静态表前exportObject加载时处理

image-20260219213924944
1
2
3
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}

这里是调用DGCImpl类的静态变量dgcLog,调用静态变量时会默认加载类的static代码块

image-20260219215606198

这里静态代码块会利用 Util.createProxy 动态加载 sun.rmi.transport.DGCImpl_Stub 这个类

image-20260219220047294

并将其DGCImplUnicastServerRef属性stub设置为其

image-20260219220337586

最后在静态表中将最终的 target 对象放入

5.2、调用

客户端D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\sun\rmi\transport\DGCImpl_Stub.class

方法dirty

image-20260219221000827

方法clean

image-20260219221124933

所有的客户端stub都会被JRMP攻击

服务端D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\sun\rmi\transport\DGCImpl_Skel.class

image-20260219221419408 image-20260219221453529

随着 JDK 安全机制的不断完善(特别是 8u121 把 useCodebaseOnly 默认设为 true,以及后续 8u191 对 JNDI trustURLCodebase 的严格限制),早期那种直接利用 RMI 组件互相投毒、依赖远程加载 codebase 的攻击手法,在现代环境下的门槛已经变得极高

现在 RMI 在安全攻防里的生态位,已经完全变成了**”最佳辅助”**