Java反序列化RMI篇-RMI基础

xekOnerR Sleep.. zzzZZzZ

Introduction

RMI , Remote Method Invocation , 远程方法调用;

即在一个 JVM 中 Java 程序调用在另一个远程 JVM中运行的 Java 程序;这个远程JVM可以在本地也可以在不同的实体机上;两者通过网络进行通信。

https://docs.oracle.com/javase/tutorial/rmi/overview.html
JAVA/RMI/attachments/Pasted image 20260225140819.png

由三部分组成:

  • Server - 服务端,通过绑定远程对象,可以进行很多网络操作,也就是socket
  • Client - 客户端,客户端调用服务端的方法
  • Register - 注册端,提供服务注册和服务获取

RMI Implementation

  • Server
    编写一个远程接口,定义一个sayHello方法 - RemoteObj
1
2
3
public interface RemoteObj extends Remote {  
public String sayHello(String keywords) throws RemoteException;
}

此远程接口要求作用域为 public;
继承 Remote 接口;
让其中的接口方法抛出异常

编写该接口的实现类Impl - RemoteObjImpl

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

实现远程接口
继承UnicastRemoteObject类,用于生成 Sub(存根)和 Skeleton(骨架)
抛出错误
继承序列化接口

注册远程对象 - RMIServer.java

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

port 默认为 1099 端口

  • Client
    同样编写一个远程接口,定义一个sayHello方法 - RemoteObj
1
2
3
public interface RemoteObj {  
public String sayHello(String keywords) throws RemoteException;
}

编写client代码,获取远程对象并调用方法 - RMIClient

1
2
3
4
5
6
7
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");
}
}
  • Test
    先启动Server端,然后运行RMIClient,可以发现在RMIServer端已经收到了从client发出的信息,并且成了大写JAVA/RMI/attachments/Pasted image 20260225143549.png

Wireshark RMI Stream Analysis

Analysis

通过 tcp.port == 1099 正则表达式来过滤其他杂包。

  • TCP 三次握手JAVA/RMI/attachments/Pasted image 20260226181107.png

因为是 tcp 协议,链接第一件事肯定是走 tcp 的三次握手协议,这里不做解释了。

  • JRMP 协议握手

    JAVA/RMI/attachments/Pasted image 20260226181558.png
  • JRMI Call

    JAVA/RMI/attachments/Pasted image 20260226181810.png 远程调用包 可以在ASCII 区域查看到调用方法的名字
  • ReturnData

    JAVA/RMI/attachments/Pasted image 20260226182438.png

ReturnData 会返回一个 Stub,也就是存根;里面包含了 ip 地址、端口号等。
接下来客户端会进行解析并对新的端口发起 TCP 链接。

我这里的端口是 00 00 0c e4 ,转换过来就是 3300 port
所以我们继续加正则表达

1
tcp.port == 1099 || tcp.port == 3300
  • TCP 链接JAVA/RMI/attachments/Pasted image 20260226183820.png

接下来会有一个问题,wireshark 会把对3300端口的访问标记为默认协议,所以我们需要去修改一下:

选中这里本应该是 JRMI 协议请求的 2093号, 右键 Decode as…
JAVA/RMI/attachments/Pasted image 20260226183958.png

选择 Current 这一列,选择 RMI 即可。

  • JRMI Call

    JAVA/RMI/attachments/Pasted image 20260226184639.png
  • ReturnData

    JAVA/RMI/attachments/Pasted image 20260226184941.png

Summary

实际上建立了两次 TCP 链接
第一次是链接1099Register,寻找名字为 hello 的对象(Call),然后 Register 返回一个序列化的数据,也就是 name=hell0 (ReturnData);
第二次是链接 Register 分发给客户端的端口。是服务端发送给客户端 Call 的数据;客户端拿到数据后发序列化,链接到新的地址。再一次进行 TCP 协议链接;在此连接中才调用了真正的远程方法,也就是 sayHello

从 IDEA 断点分析 RMI 通信原理

创建远程服务

  • 该过程不存在漏洞,因为是在本地创建
    分析如何发布远程对象:
1
RemoteObj remoteObj = new RemoteObjImpl();

下断点调试跟进

如果这里步入不了可以尝试强制步入。
Mac快捷键 Fn + Shift + Opt + F7

JAVA/RMI/attachments/Pasted image 20260228102825.png

继续跟进
JAVA/RMI/attachments/Pasted image 20260228102920.png

因为实现类中申明了是继承于父类UnicastRemoteObject的,所以会从父类中寻找构造方法。
这里默认是 port=0,代表随机端口

我们跟进到 exportObject 方法中分析
JAVA/RMI/attachments/Pasted image 20260228104643.png

该方法是一个静态函数,第一个参数为 obj 对象,第二个参数是 new UnicastServerRef(port)

在实现类中有这么一段代码:

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

可以看出来这段代码就是实现网络连接的关键点

跟进第二个参数:
(这里 Mac 可以按 Shift + Fn + F7 来选择参数步入)
JAVA/RMI/attachments/Pasted image 20260228110255.png

(如果这里步入到了 class 文件中,添加 sun 包,详见 cc 1)

这里就算是一个网络引用的类

跟进 LiveRef 方法
JAVA/RMI/attachments/Pasted image 20260228110518.png

继续跟进 this
JAVA/RMI/attachments/Pasted image 20260228110555.png

看到了 TCPEndpoint 类,进入看一下构造方法
JAVA/RMI/attachments/Pasted image 20260228110812.png

传入一个 ip 和一个 port;继续跟进到 this 构造函数中
JAVA/RMI/attachments/Pasted image 20260228110949.png

JAVA/RMI/attachments/Pasted image 20260228111522.png

看到数据是放在 endpoint 中,而 endpoint 又封装于 LiveRef,所以也可以理解为数据存放于 LiveRef,并且 LiveRef 只会存在一个。

以上就是 LiveRef 创建的过程

回到 super(new LiveRef(port)); 处,进入 super 父类中查看:
JAVA/RMI/attachments/Pasted image 20260228111931.png

只是作为赋值,继续 F7 步进
看到出现了 stub
JAVA/RMI/attachments/Pasted image 20260228112633.png

这里解释一下为什么会出现 stub:
JAVA/RMI/attachments/Pasted image 20260228112806.png

RMI 在 Server 中会创建一个 stub,然后 stub 会传到 RMI register 中,最后让 RMI Client 获取 stub。

我们先研究 stub 产生的步骤,跟进 createProxy 方法

JAVA/RMI/attachments/Pasted image 20260228113217.png

此处的判断我们先不研究,后续会学。

后面就是标准的创建动态代理的方法
JAVA/RMI/attachments/Pasted image 20260228113405.png

第一个参数,类加载器为 AppClassLoader,第二个参数是一个远程接口,第三个参数是调用处理器,调用处理器里面只有一个 ref,就是我们之前看到的一路封装的 ref

走到此处就已经把动态代理创建完成了
JAVA/RMI/attachments/Pasted image 20260228113556.png

接下来创建了一个新的 Target,也就是一个总的封装,把所有有用的东西都放在 Target 中
JAVA/RMI/attachments/Pasted image 20260228114142.png

JAVA/RMI/attachments/Pasted image 20260228114511.png

可以看到 ref 和 id 都是相同的。

F8 走到 new Target 的下一条语句,分发 target

1
ref.exportObject(target);
JAVA/RMI/attachments/Pasted image 20260228115447.png

走到这里,是真正处理网络请求的地方。

1
TCPEndpoint ep = getEndpoint();

获取了 TCPEndpoint,继续往下走到 server = ep.newServerSocket();
JAVA/RMI/attachments/Pasted image 20260228115937.png

创建了一个新的 socket,已经准备好了等待别人来连接,

并且会给 port 进行赋值:
JAVA/RMI/attachments/Pasted image 20260228120130.png

赋值逻辑如下:如果 listen 为 0,则随机赋一个值
JAVA/RMI/attachments/Pasted image 20260228120456.png

发布完成后 RMI 会把所有的信息都保存在两个 table 中,有点像日志。

  • 小结
    不算难,只是代码逻辑很复杂。
    远程发布对象,用 exportObject() 指定到发布的 ip 和端口,端口为随机值。
    只是赋值和封装的部分很复杂。

发布完成后的记录,只需要知道是保存在静态的 HashMap 中。

这一块就是服务端自己创建远程服务的一个操作,所以这一块是不存在漏洞的。

创建注册中心 + 绑定

下断点:
JAVA/RMI/attachments/Pasted image 20260228124159.png

走到这里:
JAVA/RMI/attachments/Pasted image 20260228124853.png

看到是创建了一个 LiveRef 和一个新的 UnicastServerRef
和上面分析的创建远程对象很相似

跟进这里的 setup 看一下:
JAVA/RMI/attachments/Pasted image 20260228125337.png

和之前的代码一样
JAVA/RMI/attachments/Pasted image 20260228125504.png

看到只是传入的第三个参数有区别,创建远程服务时候是 false,现在创建注册中心的时候是 true。
JAVA/RMI/attachments/Pasted image 20260228125718.png

第三个参数为 permanent ,意为永恒的。
这就代表创建注册中心的时候是一个永久对象,但之前创建远程服务的时候是临时对象

JAVA/RMI/attachments/Pasted image 20260228130050.png

接下来就是和创建远程对象一样了,创建 stub 的阶段。

但是在 stub 创建过程中又不一样了,跟进看一下:

在 if 逻辑中进入 !(ignoreStubClasses || !stubClassExists(remoteClass))) 中的 stubClassExists 查看

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" 这个类

走一下这个 if,发现是可以走进去的,也就是可以找到该类。

对比一下创建远程服务阶段,
创建注册中心是进入 createStub 方法
而创建远程服务是直接创建动态代理
JAVA/RMI/attachments/Pasted image 20260228130754.png

方法内就是通过反射创建这个对象,里面放了 ref
JAVA/RMI/attachments/Pasted image 20260228131124.png

相比于创建远程服务是直接代理创建,现在是 forName 创建,但是里面放的都是 ref,是一样的。

继续往下走
会进入到 setSkeleton 方法中
JAVA/RMI/attachments/Pasted image 20260228131354.png

里面就是 createSkeleton 方法创建 skel

继续走到 Target 方法
JAVA/RMI/attachments/Pasted image 20260228132903.png

这里还是和之前一样,没什么好说的,继续往下走到这里:
JAVA/RMI/attachments/Pasted image 20260228133420.png

调用了一个 putTarget 方法,把封装的数据放进去,跟进看一下都放了什么东西进去

static -> objTable
JAVA/RMI/attachments/Pasted image 20260228135621.png

可以看到有三个表,逐个分析一下:

第一个表中的 stub 是 DGCImpl_Stub,是分布式垃圾回收的一个对象,不是我们刚才创建的但是很重要:
JAVA/RMI/attachments/Pasted image 20260228135733.png

第二个表,最开始创建远程服务的 stub:
JAVA/RMI/attachments/Pasted image 20260228140007.png

第三个表就是我们刚刚创建的:
JAVA/RMI/attachments/Pasted image 20260228140138.png

所以这里就是起了几个远程服务,一个端口是固定的,另外两个是不固定的。为什么会有三个 Target 后面学。

  • 绑定
    绑定,也就是最后一步,bind 操作
1
registry.bind("remoteObj", remoteObj);

下断点然后调试:
checkAccess 方法,就是检测是否在本地绑定。是会通过的。

JAVA/RMI/attachments/Pasted image 20260228140923.png

下一句就是检查 binding 这里是否有内容,bindings 就是一个 hashtable,如果有数据的话会抛出异常。

继续走就是 put 了,很好理解,就是把名字和远程对象 put 进去。
JAVA/RMI/attachments/Pasted image 20260228141144.png

  • 小结
    其实依旧挺简单的,依旧是代码逻辑非常复杂。
    和创建远程对象很类似,只不过多了一个持久对象,这个对象就是注册中心。

绑定的话也很简单,就判断判断然后 put 进去了。

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

  • 客户端请求部分是存在漏洞的,上面在 wireshark 抓包的时候分析过了,RMI 是一个基于序列化的 java 远程方法调用机制,有一些有问题的反序列化。

客户端的请求分为三个部分,获取注册中心,查找对象,调用方法。

  • 获取注册中心
    这一部分不存在漏洞。

三行代码都下断点后开始调试

会进入 getRegister 方法中
JAVA/RMI/attachments/Pasted image 20260228145435.png

可以看到是先创建了一个 liveRef 对象,把 ip、port 传入;然后进行封装。

这里的 createProxy 流程是和前面的创建 stub 一样的,新建了一个 ref,把该封装的东西都封装到 Ref 中。

就获取到了注册中心的 Ref,是在服务端给的参数,本地重新创建的 stub。

JAVA/RMI/attachments/Pasted image 20260228145902.png

获得到注册中心的 stub 对象,接下来要去查找远程对象。

  • 查找远程对象
    这里对应的是第二个断点处
    如果直接调进去的话,会发现是调试不了的,反编译出来是 class 文件JAVA/RMI/attachments/Pasted image 20260228150820.png

原因应该是版本的问题

但是代码依旧是可以正常运行,不妨碍我们分析。

这里的 var1 就是我们传入的参数 remoteObj,写进了一个输出流,序列化进去。
那注册中心肯定会调用反序列化操作进行读取。
JAVA/RMI/attachments/Pasted image 20260228151602.png

下面会走到

1
super.ref.invoke(var2);

调用父类的 invoke 方法,也就是
JAVA/RMI/attachments/Pasted image 20260228151732.png

继续调用了方法,跟进跟进,进入到 executeCall 方法中

JAVA/RMI/attachments/Pasted image 20260228151934.png

该方法是真正调用网络请求的方法,暂时先不学,后面再讲。
先继续看整个代码的逻辑

在此处下一个断点:
JAVA/RMI/attachments/Pasted image 20260228152220.png

运行到此处后先分析一下逻辑 :

1
2
3
invoke()
call.executeCall()
out.getDGCAckHandler()

该函数有一个处理异常的方法
JAVA/RMI/attachments/Pasted image 20260228152507.png

这里的 in 实际上就是数据流里面的东西
JAVA/RMI/attachments/Pasted image 20260228152727.png

本意应该就是想在报错的时候把一整个信息都拿出来,这样会更加清晰一点
但是这里就会有一个问题:

这里的 readObject 会进行反序列化操作,如果注册中心返回一个恶意的对象,客户端进行反序列化的话
这就会导致反序列化漏洞的产生

也就是说只要 stub 中调用网络请求,就会调用 invoke 方法,就会收到攻击。

继续运行到下一个断点,看一下获取到了什么数据

JAVA/RMI/attachments/Pasted image 20260228153409.png

简单来说就是获取到了 RemoteObj 这个动态代理,其中包含一个 ref。

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

此处存在漏洞。

1
remoteObj.sayHello("hello");

的 Debug

F7步入,无法步入的 Windows: Shift + Alt + F7 强制步入。
JAVA/RMI/attachments/Pasted image 20260301135930.png

走到了 invoke 方法中,该方法是动态代理调用方法时候自动调用的。
一堆 if 都是抛出异常的

我们跟进进最后一行语句

1
return invokeRemoteMethod(proxy, method, args);
JAVA/RMI/attachments/Pasted image 20260301140135.png

看到还会调用一次重载的 invoke 方法

重载的方法调用了链接
JAVA/RMI/attachments/Pasted image 20260301140654.png

接着往下走

循环中有一个 marshalValue 方法
JAVA/RMI/attachments/Pasted image 20260301140823.png

它会序列化一个值,实际上也就是我们传入进来的 hello
JAVA/RMI/attachments/Pasted image 20260301140849.png

下面就会调用 executeCall 方法
JAVA/RMI/attachments/Pasted image 20260301140945.png

继续往下走,看到了 unmarshalValue 方法
JAVA/RMI/attachments/Pasted image 20260301141019.png

步过后同样看到了存在攻击的点
JAVA/RMI/attachments/Pasted image 20260301141156.png

这个数据会被 return

1
Object returnValue = unmarshalValue(rtype, in);
JAVA/RMI/attachments/Pasted image 20260301141323.png
  • 小结
    存在两个可能的攻击点
    executeCall()
    unmarshalValue()

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

之前研究的是客户端请求数据中心中,客户端的代码执行逻辑
现在分析一下注册中心的执行逻辑

因为在客户端中,操作的是 stub,服务端操作的是 Skel
Skel 创建之后应该是被封装在 Target 中的

断点下在 Server 处的处理 Target 的地方:
JAVA/RMI/attachments/Pasted image 20260301144509.png

Target 内容:
JAVA/RMI/attachments/Pasted image 20260301144853.png

接着走,把 stub 的数据放入 disp
JAVA/RMI/attachments/Pasted image 20260301144959.png

然后调用 disp 的 dispatch 方法

我们手动跳进去看一下

进入到分支
JAVA/RMI/attachments/Pasted image 20260301145242.png

跟进后发现
JAVA/RMI/attachments/Pasted image 20260301145307.png

走到这里,下面就是 skel.dispatch() 的过程了

数据中心代码中的几个方法:
list (case1)
bind (case0)
rebind (case3)
unbind (case4)
lookup (case2)

分析代码可以发现,bind、rebind、unbind、lookup 中都有 readObject 方法
JAVA/RMI/attachments/Pasted image 20260301145819.png

如果向控制中心传入构造好的序列化内容

就会触发反序列化攻击

  • 小结
    客户端请求数据中心 - 数据中心的操作

主要就是处理 Target,进行 Skel 的生成和处理

漏洞点在 dispatch 方法,存在反序列化的入口类,可以配合 cc 链进行攻击利用。

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

和上面一样,打两个断点
JAVA/RMI/attachments/Pasted image 20260301150444.png

因为是研究服务端最后的运行逻辑,所以需要到最后一次的代码执行逻辑。

按 2x2 次 F9,直到 Stub 为动态代理为止
JAVA/RMI/attachments/Pasted image 20260301151751.png

在此状态下进入 dispatch 方法
JAVA/RMI/attachments/Pasted image 20260301151929.png

可以看到 skel 为 null,并没有走入 if 语句。

往下走,获取到输入流以及 method
Method 就是我们之前写的 sayHello() 方法。
JAVA/RMI/attachments/Pasted image 20260301152114.png

继续往下走就是上面分析的 unmarshalValue() 方法。
这里和我们之前说的一样,是存在漏洞的。
JAVA/RMI/attachments/Pasted image 20260301152154.png

这里的流程相同,也就是我们的 hello 传参传进去,序列化读进去,反序列化读出来,和之前是一致

总结

太底层代码实现了,看的脑袋昏

下一篇开始学习 rmi 攻击

但是不学原理又不行

参考资料

https://www.bilibili.com/video/BV1L3411a7ax?spm_id_from=333.788.videopod.episodes&vd_source=d51dbb41ef00391c5c021ee533eafd8e&p=8

https://drun1baby.top/

  • Title: Java反序列化RMI篇-RMI基础
  • Author: xekOnerR
  • Created at : 2026-03-01 15:31:14
  • Updated at : 2026-03-01 15:41:56
  • Link: https://xekoner.xyz/2026/03/01/Java反序列化RMI篇-RMI基础/
  • License: This work is licensed under CC BY-NC-SA 4.0.