java反序列化之RMI专题

引用

教程:RMI - Java™教程

基于Java反序列化RCE - 搞懂RMI、JRMP、JNDI

如何创建java rmi环境

JAVA安全基础(四)– RMI机制

RMI概述

以下是wiki的描述:

1
2
3
Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

根据wiki所说RMI全称为Remote Method Invocation,也就是远程方法调用,通俗点解释,就是跨越jvm,调用一个远程方法。众所周知,一般情况下java方法调用
指的是同一个jvm内方法的调用,而RMI与之恰恰相反。

例如我们使用浏览器对一个http协议实现的接口进行调用,这个接口调用过程我们可以称之为Interface Invocation,而RMI的概念与之非常相似,只不过RMI调用的是一个Java方法,而浏览器调用的是一个http接口。并且Java中封装了RMI的一系列定义。

RMI应用通常由两个独立的程序组成,一个服务器和一个客户端。一个典型的服务器程序创建一些远程对象,使得这些对象的引用可访问,并等待客户端调用这些对象的方法。一个典型的客户端程序在服务器上获取一个或多个远程对象的远程引用,然后调用它们的方法。RMI提供了服务器和客户端之间通信和传递信息的机制。这样的应用有时被称为分布式对象应用

分布式对象应用需要执行以下操作:

  • 定位远程对象。应用程序可以使用各种机制获取对远程对象的引用。例如,应用程序可以使用RMI的简单命名工具RMI注册表来注册其远程对象。另外,应用程序也可以将远程对象引用作为其他远程调用的一部分来传递和返回。
  • 与远程对象通信。远程对象之间的通信细节由RMI处理。对于程序员来说,远程通信看起来与普通的Java方法调用类似。
  • 加载传递的对象的类定义。因为RMI允许对象在服务器和客户端之间传递,它提供了加载对象的类定义以及传输对象数据的机制。

image-20241227152058655

远程接口、对象和方法

像任何其他Java应用程序一样,使用Java RMI构建的分布式应用程序由接口和类组成。接口声明方法。类实现接口中声明的方法,并且可能还声明其他方法。在分布式应用程序中,某些实现可能存在于某些Java虚拟机中,但不存在于其他虚拟机中。可以在Java虚拟机之间调用方法的对象称为远程对象

远程对象必须通过接口来进行通信

通过实现远程接口,对象变成远程对象,具有以下特点:

  • 远程接口扩展接口java.rmi.Remote
  • 接口的每个方法在其throws子句中声明java.rmi.RemoteException,除了任何特定于应用程序的异常。

简单的例子

客户端和服务端的接口需要相同的包名才能序列化反序列化

具体过程可以看看B站up主白日梦组长的讲解

下面是服务端比较主要的文件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
//实例化,接口IRemoteObj、类RemoteObjImpl都是自己写的
//远程对象必须通过接口来进行通信,所以对象remoteObj的类型是接口
IRemoteObj remoteObj = new RemoteObjImpl();
//创建一个RMI注册表,监听端口1099,这个注册表将用于将远程对象绑定到一个名字上,供客户端查找
Registry r = LocateRegistry.createRegistry(1099);
//将 remoteObj 绑定到 RMI 注册表中,并将其命名为 remoteObj。这样,客户端可以通过 remoteObj 名称来查找并访问这个远程对象
r.bind("remoteObj", remoteObj);
}
}

客户端主代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException{
//获取到指定地址 127.0.0.1 和端口 1099 上的 RMI 注册表
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//通过 lookup 方法在 RMI 注册表中查找名为 "RemoteObj" 的远程对象,并将返回的结果强制转换为 IRemoteObj 类型
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("RemoteObj");
//调用sayHello方法
remoteObj.sayHello("hello");
}
}

两个项目成功同时运行后,我们会在服务端处有以下的回显

image-20241227161919385

回显的是大写的hello,这说明成功调用了sayHello的方法

调试RMI流程

基本名词

从RMI设计角度来讲,基本分为三层架构模式来实现RMI,分别为RMI服务端,RMI客户端和RMI注册中心。

客户端:

存根/桩(Stub):远程对象在客户端上的代理;
远程引用层(Remote Reference Layer):解析并执行远程引用协议;
传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。

服务端:

骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值;
远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。

**注册表(Registry):**以URL形式注册远程对象,并向客户端回复对远程对象的引用。

image-20241227164240889

分析创建部分(服务端)

创建过程肯定是没有漏洞的,漏洞产生肯定是在互相调用的过程中的

所以这边就重点关注一下流程,逻辑就行

创建远程对象

image-20241227165111190

开始调试,进入到RemoteObjImpl类的父类UnicastRemoteObject的构造方法中

image-20241227190138610

看到传入的port值为默认值0,实际上就是会把远程对象发布到一个随机端口上

继续往下走,进入到一个重要的静态核心函数,有两个参数:第一个参数是我们的远程对象,第二个是创建了一个新的类UnicastServerRef(服务端引用)

第一个参数肯定是实现逻辑的,所以可以猜测到第二个参数是处理网络请求用的

image-20241227190353453

我们跟进第二个参数看看,可以发现又创建了一个新的类LiveRef,很重要的一个类,我们跟进去

image-20241227190901399

调用了一个构造函数,我们看一下,通过该构造函数我们可以获取到ip,端口等

image-20241227191048936

重点看一下第二个参数,我们看一下它会返回什么东西,返回的是一个类TCPEndpoint

image-20241227191532149

我们点进这个类看以下它的构造函数,也就是说只要给它一个ip,一个端口,他就可以处理后面的网络请求

image-20241227191827238

LiveRef初始化完后,继续往下走,走回到了类UnicastServerRef的构造函数,引用了其父类的构造函数,作用也就是赋值而已

image-20241227190901399

到这里,从始至终我们就只创建了一个liveRef类,继续往下走我们就出来了

image-20241227190353453

我们继续调,这里要点的是步入,这些判断都会成功,ref的值也还是liveRef

image-20241227193239000

进入sref的exportObject函数,作用是创建了代理stub,然后将stub给到注册表,客户端再从注册表拿到stub

image-20241227193514193

看stub的赋值是通过函数createProxy,第一个参数是我们远程对象的类,第二个参数还是我们的ref值

image-20241227194022225

接下来的步骤都是创建动态代理的过程,到下面这边的话动态代理就创建好了

image-20241227194415688

下一步就是创建了一个参数target,这个可以理解为是到目前为止有用的东西的一个总封装,有兴趣的可以跟进去看一下

image-20241227194555701

下一步就是把target给发布了出去,我们看一下它的逻辑,一直点步入,直到

image-20241227195214881

我们点进listen(),就是来开端口的嘛,我们跟进去,这里网络请求会新开一个线程,和真正的代码逻辑的线程是不同的

再新开一个线程的时候,要是端口值为0的话就是随机赋一个值

image-20241227195655843

因此到这里的时候端口就已经是有一个值的了

image-20241227200113183

到目前为止,服务端就已经把远程对象发布出去了,但是发布在了一个随机端口上,所以客户端默认是不知道的

接下来服务端要做的就是记录一下,记录把它发到哪里去了,用的还是exportObject函数
image-20241227200431919

重要的是圈出来的这个静态函数,我们跟进去后继续往下走

image-20241227200644845

objTable.put(oe, target);以及implTable.put(weakImpl, target)的作用就是把target保存到了系统的一个静态表里面

保存好后,这就是服务端整个发布的流程

最后呢就是等待客户端的连接了

image-20241227201218483

创建注册中心+绑定

image-20241228151015372

开始调试,强制步入进到了静态方法createRegistry中,该方法会创建一个RegistryImpl对象,跟进去,我们让它走到这里

image-20241228151803494

if从句是安全检查,不重要,继续往下走会到else从句中创建一个LiveRef对象和UnicastServerRef对象

对象lref的端口值为1099,setup函数和上面创建远程对象过程中所调用的其实是一模一样的

这里第三个参数值为true,进该函数可以看到意思就是创建一个永久的对象,而上面仅仅是创建一个临时对象

image-20241228153031424

和上面有区别的地方在于创建stub,我们进入该函数仔细看一下

image-20241228153623156

会先做判断看这个stub在系统中是否已经存在,进入了if之中说明是存在的

stubClassExists(remoteClass)函数具体做判断的步骤就是检测该文件名加上后缀“_Stub”在系统中是否已经存在

image-20241228153906205

image-20241228154323345

然后就会调用createStub方法,该方法就是简单地创建一个stub对象

和上面的不同就是这里是直接通过forName方法创建出来的,而上面是通过动态代理创建出来的

往下走会到一个if判断,是判断是不是服务端定义好的,是的话就会调用setSkeleton方法

image-20241228155303796

看上面那个流程图的话我们就会知道Skeleton是服务端的代理(客户端是stub),都是处理网络请求的

继续调试,下面的参数target也是一个集成的目前有用的东西

跟着调试到这里,调用了一个静态方法,把target给放了进去

image-20241228155914697

一直跟进去并往下走,会发现把所有的数据都放到了系统的一个表里面

有三个值,一个是默认每次创建的时候都有的,一个是我们之前创建的远程服务对象,还有一个就是我们刚刚创建的注册中心(端口是固定的),两个都会创建一个stub给客户端用

客户端要是想访问服务端就需要stub,想访问注册中心的话也需要拿那个stub来访问

image-20241228160830548

Ok,上述就是创建注册中心的流程

接下来就是绑定的流程,比较简单

image-20241228161458745

到这一步的时候注册中心和远程对象都是已经创建完的

image-20241228161851360

强制步入bind函数,首先是检查,问题不大,都会通过,然后接下来就是绑定

image-20241228162103943

参数bindings其实就是一个哈希表,现在是空的

接下来就是检查表里面是否已经有remoteObj,已存在的话救护抛出已存在的异常,没有的话就put进去

这就是绑定的过程

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

image-20241228164749824

首先是查找注册中心,强制步入->步入,往下调试

image-20241228165018012

创建了一个类LiveRef的对象后再调用了createProxy函数,跟上面创建注册中心的步骤中是一模一样的,在自己本地上上面又起了一个stub,而不是直接拿注册中心的stub来用

然后就出来了,registry为RegistryImpl_Stub,就获取到了注册中心的stub对象

image-20241228165558187

接着进进行下一步:查找远程对象

服务端绑到注册中心上的其实也是一个动态代理,这里的话其实就是获取到那个动态代理

但是这边有个问题就是我们进不到RegistryImpl_Stub.class文件中的lookup函数,这是因为该文件的java版本是1.1,而我们是1.8,所以调试不了,那就直接看吧

image-20241228171252996

第一行就是创建一个连接,该函数传入一个字符串,通过var3.writeObject(var1);把它写入了一个输出流里,也就是序列化进去了

super.ref.invoke(var2);完成网络请求

1
2
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();

之后会获取一个输入流,也就是把返回值获取到了,通过反序列化的方式读取,var23就是读回来的远程对象的代理

我们这里就可以知道客户端获取远程对象代理的过程是通过反序列化来获取的,那么要是有一个恶意的注册中心就可以通过这种方式来攻击客户端

当然除了这一种还有一个地方,就是上面提到的invoke函数

该invoke函数其实是UnicastRef.java文件中的

image-20241228191722937

接着会执行executeCall()函数,在这里打一个断点

image-20241228191822264

该函数有什么特别的地方呢,就是当你的异常是TransportConstants.ExceptionalReturn时,就会通过反序列化来获取流里面的对象

image-20241228191948524

这里如果说你的注册中心返回一个恶意的对象,然后客户端就会在这里进行反序列化,就会导致客户端被攻击

这个点会比我们刚刚说的那个普通的反序列化更加的隐蔽,影响也更广,这是因为只要调用了invoke方法的地方都有可能会被攻击,而stub里面所有处理网络请求的都会调用这个方法

其他函数比如list(),bind等都会调用invoke方法,但是上面的那个直接反序列化并没有出现在所有的函数里面,范围更小

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

也就是在这一步的时候,我们跟进sayHello方法

image-20241228193300130

结果是跑到这里来了,这是为什么呢

这是因为我们获取到的remoteObj是一个远程动态代理,而动态代理无论调用什么方法都会走到调用处理器里面的invoke方法

image-20241228193419898

会走到return invokeRemoteMethod(proxy, method, args);,我们跟进去

走到了一个重载的invoke方法处,我们跟进去

image-20241228193634769

前面一部分的逻辑还是一样的,都是创建连接,然后走到marshalValue方法,作用是序列化值

从下面我们可以知道序列化的是我们传进来的参数

image-20241228193942319

序列化完后就又走到了call.executeCall();,所以嘛就是所有客户端的请求都会调用这个方法

然后往下走要是说有返回的值的话,就会调用unmarshalValue方法

image-20241228194506764

而这个方法会通过反序列化来获取结果

image-20241228194629480

image-20241228194858261

在这个部分里面也是有两个反序列化点的,和刚才客户端请求注册中心是类似的

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

现在来看一下客户端请求注册中心的时候注册中心上面发生的事情,通过注册中心获取获取远程对象的时候注册中心是怎么处理的

从上面的那个流程图我们可以明白在客户端我们一直操作的是stub,所以服务端操作的肯定是skeleton,所以断点肯定是要下在文件RegistryImpl_Skel.class里面

具体怎么走到这个文件里面的可以来走一遍

前面自己跟着白日梦组长的视频走一下,在创建注册表的过程中最后会调用listen()函数

image-20250103212411611

listen函数的作用上面部分也讲过,会开一个网络监听,在函数中会开一个新的线程,跟进去,看该线程的run方法

run方法里面也没有其他东西,所以我们继续跟进去

image-20250103212914522

该方法中又创了一个线程值,所以我们要继续跟进去

image-20250103213200296

走到了这里之后,我们再看看它的run方法,这个方法实际上就是调用run0方法

image-20250103213340439

跟进去,重点看底下的handleMessages方法,该方法就是开始读一些字段,然后根据传过来的字段值做出不同的操作,通过switch-case方法

image-20250103213547497

默认的case值如下

image-20250103213843436

我们跟进serviceCall方法,它会从静态的表里面获取target,之前不是有说过服务端会把创建好的东西都放到target中,再放到静态表里面去,该方法就是这么查找target,所以断点下在这里

image-20250103214158771

开头讲了这步是通过注册中心获取获取远程对象,所以要运行客户端的文件,运行完后服务端会直接跑到我们设的断点这里,说明我们前面走的都是没错的

往下调试,会获取target里面的分发器disp,disp里面有skel

image-20250103215316719

下一步就是会调用disp的dispatch方法,我们跟进该方法中

image-20250103215437083

方法中当skel不是null的时候调用oldDispatch方法,跟进去

image-20250103215702418

该方法中重点就是最后会调用skel的dispatch方法,skel的值是RegistryImpl_Skel,所以就走到了我们这部分开头说的那里了

同样那个文件中的方法也是调试不了的,所以我们只能直接看

image-20250103215923704

我们可以把注册中心理解为一个特殊的服务端,我们在客户端处理的一直是stub,然后走到服务端之后就是处理skeleton

文件RegistryImpl_Skel.class中的dispatch方法主要用的是switch-case逻辑,不同的case值对应的方法是不一样的,其中case2对应的是lookup方法,所以目前来说我们是会调用到case2

image-20250103220856193

之前我们说过客户端代码IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");中的远程对象remoteObj是序列化传过去的,那么注册中心段case2上面的readObject函数就是反序列化读出来的

这里就是一个反序列化点,我们客户端可以通过这里反序列化攻击注册中心

当然只要其他case中有readObject函数的我们都可以进行攻击

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

最后客户端直接在服务端调用远程方法的时候服务端是怎么处理的呢,客户端就是于这部分代码有关系

image-20250104212400761

这里的话前面跟网络相关的逻辑都是一样的

当前请求到的target是动态代理,请求到服务端了,我们再来看一下接下的逻辑

image-20250104212500019

依然会走到disp的dispatch方法,我们跟进去,与前面请求注册中心不一样的是这次方法里面的num值小于0

image-20250104212758304

直接走到这一步,这是因此这次的skel是空的

image-20250104212907451

继续往下走获取到输入流,然后获取到方法,也就是我们定义的远程方法sayHello

image-20250104213106667

继续往下走,重要的在下面

image-20250104213407058

在上面有提到客户端传入的参数都是序列化传进的,然后在服务端这边就会反序列化得到

最后获取到参数值,就是我们传过来的hello

image-20250104213620889

在这里进行真正的方法远程调用,然后就结束了

image-20250104213740960

后面还有就是返回值,会把它给序列化,然后到客户端再反序列化

image-20250104213937963

客户端请求服务端-dgc

dgc是rmi的分布式垃圾回收的一个模块,不需要知道具体的,只要按照和上面的一样分析一样就要可以了

它的位置在创建静态表那里,也就是文件ObjectTable.java中,在这里打上断点

image-20250104215313272

其中的dgcLog是一个静态变量,然后在调用一个类的静态变量的时候会先完成它的初始化的

初始化的时候实际上时会调用到它的静态代码块的,会走到run函数中,打个断点

image-20250104215850062

开始调试,可以看到下面创建stub的方法和创建注册中心是类似的,自己可以跟一下

image-20250104220405717

这边我们往下走就可以知道是创建了一个DGCImpl_Stub类,它的功能和注册中心是一样的,注册中心那个端口是用来注册服务,它这个端口是用来远程回收服务,并且这个端口是随机的

image-20250104220537689

创建完了之后就把它集合到target中,然后put进去

image-20250104220826185

上面就是dgc的创建过程

dgc调用的时候会走到disp里面,具体的流程和注册中心是一样的,这里就不分析了

image-20250104221401866

我们重点看一下它的功能

我们首先看一下客户端DGCImpl_Stub里面的方法,有两个方法clean和dirty,其实就是一个比较干净的清除和一个比较弱的清除

我们不需要过分关注,重点看一下哪里是有风险的

image-20250104222141558

dirty方法中也有这行代码super.ref.invoke(var5);,该函数函数其实是UnicastRef.java文件中的

clean方法中也有

具体可以看标题《客户端请求注册中心-客户端》

所以就是说所有的客户端stub都是可以被反序列化攻击的

dirty方法中还有一处,它是会从服务端获取到一个东西然后反序列化出来,获取到的是Lease类的东西

image-20250104222305469

综上dgc客户端可是可以受到反序列化攻击的

接下来我们再来看一下服务端DGCImpl_Skel

这里有一个方法dispatch,通过switch-case来调用dirty和clean方法:case=0调用clean,case=1调用dirty

服务端一样看一下有没有反序列化点,如下同样是有的

image-20250104222802935

所以服务端也是会被攻击的

和注册中心一样,无论是客户端还是服务端都会被攻击

但是dgc有一个特点就是它是自动生成的,也就是只要创建了远程对象就肯定有一个dgc服务

攻击远程对象的话是需要知道参数类型的,但是dgc的话就不需要

JDK高版本绕过

随着版本的更新,jdk对上述我们所提到的反序列化的点都做了一些防御

8u121

思路

在RegistryImpl.java文件中定义了一个函数,在输入流处多加了判断

image-20250106205556866

圈出来的代码是说只有你输入的类型是这几种类或者是继承了这几种类的话才允许你反序列化

在DGCImpl.java文件中限制就更加严重了

image-20250106205953266

只有圈出的这几类才允许反序列化

现在的话就是这两条路都是行不通的了,之前还有说过一条路是远程对象直接反序列化

但这条路的话是需要你知道远程对象的具体参数类型才可以,下面图片中箭头所指的地方

image-20250106210513929

还是有限制的,再往回看看感觉还是只能从注册中心那边下手,也就是看看RegistryImpl.java文件中有没有一些可以绕过限制的

在限制的输入类型中有一个类是UnicastRef,这个类在客户端请求注册中心的时候会走到,可以去回顾一下

这样的话客户端还是会受到反序列化的攻击

现在有一个思路,就是说让服务端发起一个客户端请求,实际上就会在服务端上面导致一个反序列化攻击

这里重点就是上面提到的RegistryImpl.java文件中的invoke方法,查找一下它的用法

调用invoke方法实际上就是在stub里面,一个是RegistryImpl_Stub,还有一个是DGCImpl_Stub

想要调用invoke方法,就必须得有一个stub,想要创建一个sub,就必须调用createProxy函数,所以我们看一下哪些地方会用到createProxy函数

直接说结论就是我们要重点关注下面这个地方的方法调用

image-20250107152712243

它是在DGCClient里面的一个内部类EndpointEntry的构造函数中调用的

现在的刘晨是已经固定的,服务端代码都是已经写死的了,那么我们想改变代码的流程,实际上就是需要一个反序列化的点触发创建stub,然后再调用invoke

为什么需要反序列化呢,这是因为在一个已经跑起来的程序里面想改变它的逻辑,非常的困难,它的那种动态的特性只能通过反序列化来实现

所以现在我们就要一这里作为入口,想办法创建一个类EndpointEntry,然后生成一个dgc

下一步我们要让dgc发起一个客户端请求,要么是dirty,要么是clean

服务端上创建类EndpointEntry

查找该类的用法,只有一个lookup方法

image-20250107154453331

查找lookup用法

image-20250107154607614

继续往上查找registerRefs用法,最终的目的是查找到一个反序列化点

这个时候有两条路

image-20250107154802314

底下那个read方法是在else中调用registerRefs方法

image-20250107154925812

条件是当输入流不是ConnectionInputStream的时候才会调用,但是在整个反序列化流程里面输入流都是ConnectionInputStream

所以这条路断了,只剩下上面一条

image-20250107155140703

继续往上查找调用

image-20250107155544089

查找releaseInputStream()方法的调用

image-20250107160713654

最后就是找到了在各个Skel文件中都会调用这个方法,无论是DGCImpl_Skel或是其他的

我们Skel是在服务端的,我们在服务端上找到了一个地方能够让它产生dgc服务

就比如我们看一下注册中心的,比方说我们让它调用了一个方法list()

image-20250107161116341

然后它就会调用releaseInputStream()方法,我们跟进去,其实就是相当于上面查找的流程反过来

查到下面这里,要当incomingRefTable不为空的时候才会继续往下走

image-20250107161353844

只要调用了这个方法,最后就会创建一个dgc

这条链中需要我们处理的只有一个if判断,但是我们正常使用的话incomingRefTable就是空的

所以我们现在就要找不让它为空的办法,也就是找给它赋值的地方,查找incomingRefTable的用法

也就只有这一个赋值的地方,调用了一个put方法

image-20250107162304530

首先的话我们需要调用saveRef方法,查找一下

image-20250107162435762

上面讲过在反序列化流程中只能进入if内,所以它就会直接调用到saveRef方法

现在我们再查找一下read方法会在哪里调用

image-20250107162713505

readExternalf方法实际上是java原生反序列化的另一种,就是与readObject类似但不完全一致,所以在反序列化的时候有这个方法也会调用

那到现在我们就已经完成了我们创建dgc的逻辑

我们怎么做呢

首先我们先序列化一个UnicastRef对象(因为readExternal方法在UnicastRef.java文件中),在里面保存一个ref

前面有说UnicastRef是RegistryImpl.java文件中的白名单对象,所以传过去是可以正常反序列化的

在UnicastRef里面它的反序列化是会调用上面的readExternalf方法,而这个方法又会调用read方法,read方法会调用saveRef方法,而saveRef方法会给incomingRefTable赋值,使其不为空

这一部分就结束了

总结:传入UnicastRef后,它的反序列化流程走完后内存中那个表不为空,接下来我们就正常走反序列化流程

也就是到RegistryImpl_Skel中,到它的releaseInputSream()的时候

image-20250107163942706

然后我们就继续往下走到

image-20250107161353844

到这里后这个表就不为空了

所以说UnicastRef的反序列化只是为了给表赋值让它走进if里面,真正的触发攻击是在正常的调用流程里面

dgc发起一个客户端请求

所以现在就可以得到一个dgc对象,但是目前还没有办法发起一个请求

无碍,构造方法继续往下看会发现有提起一个新的线程

image-20250107164844437

走进去,这个方法最后调用了makeDIrtyCall方法

image-20250107165007540

该方法里面就自己调用了dgc的dirty方法,也就成功发送了一个客户端请求

image-20250107165302282

也就是说从构造dgc到调用dirty方法都是jdk里面写好了的,但默认通过判断都是走不进去的