RMI基础
前言
避坑:RMI 攻击手法只能在 jdk8u121 之前可以使用,因为在 8u121 之后 bind、rebind、unbind 这三个方法只能对 localhost 进行攻击
环境搭建
还是和之前学习一样,用的 jdk8u65
RMI基础
RMI概念
官方文档:https://docs.oracle.com/javase/tutorial/rmi/overview.html
RMI(Remote Method Invocation,远程方法调用),在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信,所以 RMI 需要一个 Server 服务端和一个 Client 客户端
RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写
RMI组成

RMI Server:
RMI 的服务端,服务端通常绑定远程对象,这个对象封装了很多的网络操作(Socket通信、序列化等),对外提供访问方法接口,可以被 Client 远程访问
RMI Client:
RMI 的客户端,客户端可以访问调用 Server 端的远程对象,并调用其方法
RMI Registry:
RMI 的注册端,提供服务注册和服务获取,因为 Client 要去访问 Server 肯定是去访问它的端口,但是这个端口肯定是动态的,Client不知道是哪个端口,这就需要 Registry 端,Server 端可以向 Registry 端注册服务(服务端的地址、端口等信息),Client 端可以从 Registry 端获取远程对象的信息(地址、端口等)
Web server:
当 Client 想去调用 Server 上的一个对象,但是 Server 没有这个对象时,RMI 提供一个大胆的方法:允许 Server 进行远程远程类加载,这明显就很不安全,所以在 jdk121 之后就对其进行了限制,在 jdk9 之后默认禁止了远程类加载
Server 和 Client 两端想要能通信的话,就要实现同一个接口java.rmi.Remote,并且要抛出异常

RMI实现
用 Java 代码来写一个 demo 实现一下 RMI,这里把 Server 端和 Client 端分别写在不同的项目中来进行模拟 RMI
服务端
新建个 RMIServer 项目

先写一个远程接口
RemoteObj,其中定义sayHello()方法:1
2
3
4
5
6
7import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}- 作用域是
public - 要继承
Remote接口 - 让其中的接口方法抛出异常
- 作用域是
然后是接口的实现类
RemoteObjImpl:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
public RemoteObjImpl() throws RemoteException {
// 如果不能继承 UnicastRemoteObject 就需要手工导出
// UnicastRemoteObject.exportObject(this, 0);
}
@Override
public String sayHello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}重写的
syaHello()方法,实现转大写的方法,并打印出结果- 实现远程接口
RemoteObj,重写其中的方法 - 继承
UnicastRemoteObject类,用于生成 Stub(存根)和 Skeleton(骨架) - 构造函数要抛出
RemoteException错误 - 使用的对象都需要可以序列化的
- 实现远程接口
然后是要注册远程对象的
RMIServer:1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
// 创建远程对象
RemoteObj remoteObj = new RemoteObjImpl();
// 创建RMI注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定远程对象
registry.bind("RemoteObj", remoteObj);
}
}- port默认是1099
服务端就写好了
客户端
然后是写 Client 端的,另外在新建一个 RMIClient 项目

首先是要定义一个和 Server 端一样的远程对象的接口
RemoteObj:1
2
3
4
5
6
7import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}然后就是客户端
RMIClient的代码了,获取远程对象,并调用方法:1
2
3
4
5
6
7
8
9
10
11
12
13import 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);
RemoteObj remoteObj = (RemoteObj) registry.lookup("RemoteObj");
// 调用远程方法
remoteObj.sayHello("Suzen");
}
}
模拟测试
先运行 Server 端的代码

然后是运行 Client 端的

这里就是掉用一下sayHello()方法,没有打印出来,所以啥都没有,再去看 Server 端的,可以看到 Server 端的sayHello()方法成功被 Client 端远程调用了

其实这整个过程中都是通过序列化和反序列化实现的
从Wireshark抓包分析RMI通信原理
准备工作
打开Wireshark,捕获 Adapter for loopback traffic capture(因为这是本地的)

然后设置捕获过滤器筛选一下流量:
1 | |

然后先运行一下 Server 端,然后再运行 Client 端,就可以在 Wireshark 里捕获到流量了

分析流量包
建立连接与RMI协议协商
首先是 TCP 三次握手建立起连接

1 | |
然后接下来每一个 RMI 包后会跟一个 TCP 包,这个 TCP 包是一个确认包
然后是 RMI 协议协商

1 | |
客户端与注册中心交互

1 | |
注意看1093这个包:客户端(40315端口)发送 “Call” 消息,查找要调用的远程函数RemoteObj

然后看1095这个包:注册中心(1099端口)回复一个 “ReturnData” 消息,带有 RMI Server 的 IP 地址和端口

Server 端的端口是40311

ac ed 00 05是常见的 Java 反序列化的 16 进制特征,这上面这里的两个步骤都使用了序列化语句
客户端另起一个端口与服务端交互
可以把tcp.port == 1099过滤给删去查看
从1097包到1109包就是客户端与服务端进行的交互,客户端发送远程引用给服务端,服务端返回函数唯一标识符,来确认可以被调用,可以看到也是使用了序列化进行传输

对应的代码是:

1 | |
这里会返回一个 Proxy 类型函数,这个 Proxy 类型函数会在我们后续的攻击中用到
客户端通过序列化将调用函数的输入参数传输给服务端
之前的包1110到包1115就是保持 TCP 连接活跃,防止超时断开
然后看包1116:

也是使用的序列化进行的传输
对应的代码:
1 | |
所以所有的数据都是通过序列化进行传输的,那么在客户端喝服务端肯定是存在反序列化的语句
总结
整个过程一共建立了两次 TCP 连接:
- 第一次是客户端去连注册中心:客户端会传入
Name为RemoteObj的对象,对应数据流中的 “Call” 消息,然后注册中心返回一个序列化的数据,就是找到了Name=RemoteObj的对象,对应数据流中的 “ReturnData” 消息 - 第二次是服务端发送给客户端:服务端发送给客户端 Call 的消息。客户端反序列化该对象,发现该对象是⼀个远程对象,地址在40311端口,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是
sayHello()
其实就是,服务端创建远程对象,bind 远程对象到注册中心上时,会给一个 Name,然后客户端带着 Name 在注册中心上取得这个远程对象,建立连接,从而能够远程调用
原理图如图
那么我们可以确定 RMI 是一个基于序列化的 Java 远程方法调用机制
从代码分析RMI通信原理
这个部分有挺多挺复杂的,要有耐心的调,我也是断断续续的坚持过了一遍
流程分析总览
RMI 有三个部分:
- RMI Registry
- RMI Server
- RMI Client
两两通信就是 2 * 3 = 6 个交互流程,再加上 3 个创建过程,一共是 9 个过程
RMI 的工作原理可以大致参考这张图
1. 创建远程服务
注:创建远程服务这部分是不存在漏洞的
在 RMIServer 创建远程对象这里打断点:

发布远程对象
开始调试,首先是要实例化一个RemoteObjImpl对象,所以会到RemoteObjImpl类中的构造函数(前面的RemoteServer就直接跳过了)

RemoteObjImpl类是继承的UnicastRemoteObject,所以会先到父类UnicastRemoteObject的构造函数:

上面图中是传入的参数是 0,继续往下到下图是赋值给的port参数(相当于传了一个默认值),它代表发布远程服务是到了一个随机端口,是不同于注册中心的 1099 端口的

在exportObject()这里下个断点,F9 恢复程序走到这里

然后 F7 走进这个方法里

exportObject()是个静态函数,顾名思义,这个方法的作用就是将远程服务发布到网络上,这就是一个核心的函数
之前在写RemoteObjImpl类的代码的时候,有一句注释的地方:
1 | |
就是如果RemoteObjImpl类没有继承UnicastRemoteObject类的话,就需要在构造函数中手动去调用UnicastRemoteObject.exportObject()方法
1 | |
看一下这个静态函数:
- 第一个参数是
obj,就是传入的远程对象 - 第二个参数是
new UnicastServerRef(port),是用来处理网络请求的,这里传入了一个port
这里跟进UnicastServerRef()中

可以看到这里new了一个LiveRef(port),这是一个网络引用类,继续跟进看

到了LiveRef的构造函数这里,继续跟进到this里,又到了另一个构造函数:

这个里面又有一个this(),第一个参数是 ID,第三个参数是ture,重点看第二个参数TCPEndpoint.getLocalEndpoint(port)
TCPEndpoint是一个网络请求的类,跟进看它的构造函数

要传入的参数就是一个 IP 和一个 端口,然后就可以进行网络请求了
回到LiveRef这里,跟进去看this


看赋值,host和port是赋值到endpoint里,endpoint是被封装在LiveRef里
所以记住数据是在LiveRef里面即可,并且这一LiveRef至始至终只会存在一个

这就是LiveRef创建的过程
然后回到LiveRef(port)那里,跟进到super()中

这里就表明整个创建远程服务的过程中只会存在一个 LiveRef

继续往后走回到exportObject()方法这里,进入到前面的这个exportObject中


这里面就是会先检查传入的对象obj是不是UnicastRemoteObject的子类,如果是就会将obj这个对象强转为UnicastRemoteObject类型,然后将传进来的第二个参数sref复制给这个对象中的ref字段,这里的sref就是刚才new UnicastServerRef,也就是那个唯一的 LiveRef

到这个函数的最后又调用一次exportObject()方法

跟进到里面,注意看下面这里创建了一个 stub,是用来存储客户端代理,这里是通过Util.createProxy创建动态代理作为stub
Stub(存根) 是分布式系统中一个重要的概念,特别是在RPC(远程过程调用)和RMI(远程方法调用)中
Stub 是客户端代理对象,它充当远程对象的本地代表,当客户端调用远程方法时,实际上是在调用 Stub 对象的方法。

结合下面这个图来理解

就是 RMI 会现在 Server 端创建一个 Stub,然后会将 Stub 传到 RMI Registry 中,就是注册一个服务,最后让 RMI Client 从注册中心中获取 Stub,因为 RMI Client 的运行需要服务端的 Stub
继续去看 Stub 是如何产生的,跟进到createProxy()方法中

implClass就是远程对象的实现类,clientRef就是前面的 LiveRef

继续往下到这里,很明显是存在类加载的

这里是用Proxy.newProxyInstance创建动态代理对象,看一下这三个参数

loader:就是AppClassLoaderinterfaces:远程接口handler:调用处理器,在进行实例化的时候传入的是clientRef,也就是前面的 LiveRef
最后回到exportObject()里,这个 Stub 就创建好了

继续往后到target这是,是一个总封装,将所有东西放在里面

进入到Target里看看,这里的disp和stub分别是服务端和客户端的,这俩的 ref 是同一个,因为ID一样都是912,可以通过ID看出来

出来Target后就是调用exportObject()方法将target发布出去

跟进看其中的发布逻辑



最终来到上面这里,到synchronized()方法,首先是第一个调用listen()方法,这是真正处理网络请求,跟进看

前面这里就是获取 TCPEndpoint,还有监听的端口

到上面这里就是创建一个 socket,等着别人来连接
运行逻辑:
如果被连接就会到这个
run()方法里去执行executeAcceptLoop()方法
然后就是对连接的一些处理
回到newServerSocket()这里,在这个方法的最后,如果端口为0,就会给端口随机赋一个值

最终回到listen中,整个流程最终就是令target中增加了一个随机分配的端口

发布完成后的记录
就是记录远程服务被发到哪里去了
接着上面的继续往下到这里,使用exportObject()将target发布出去

跟进看,这里会先调用target.setExportedTransport()

继续跟进是一个赋值操作

然后回头再去看exportObject()里的另一个ObjectTable.putTarget(target);

前面也都是一些赋值语句,直接到下面这里

1 | |

就是将 Target 存到两个表里,保存到静态的 HashMap 中,就是存储对象信息,类似于日志
小结
整个过程就是一直在使用exportObject()方法发布远程对象到指定的 IP 和端口,这里的端口是一个随机值,很多地方就是在进行赋值、封装,到最后就是会有一个类似于日志的记录操作,记录会被保存到静态的 HashMap 中
2. 创建注册中心并绑定
创建注册中心与服务端是独立的,所以谁先谁后无所谓,本质上是一整个东西
下面这里打断点

创建注册中心
首先是来到createRegistry()这个静态方法

这里实例化一个RegistryImpl()对象,传入的prot为1099
然后继续往下走,直接到RegistryImpl()这里

这里新建一个RegistryImpl对象

然后到122行这里就是检查传入的 port 是不是注册中心的 port 以及是否开启了 SecurityManager

继续往下到136行,这里创建一个 LiveRef 对象,137行就是创建的一个UnicastServerRef对象

跟进到setup方法中,先进行赋值,然后调用exportObject()
这里和前面的发布远程对象进行一下对比,下面这个图是前面发布远程对象时的

这里对比出来的区别就是permanent的值不一样:
- 前面创建远程对象时是
false,表明是一个临时对象 - 现在创建注册中心是
true,表明是一个永久对象
回来setup,继续进入到exportObject()

这里就和发布远程对象一样,到了创建 Stub 的地方

不过创建的过程有些不同,跟进createProxy()方法中,直接来到下面这里一个判断

跟进到stubClassExists()中

是判断是否能获取到 RegistryImpl_Stub 这个类

最终是可以获取到的,返回的true

然后就可以直接走进createStub()中,而前面发布远程对象是要到下面创建动态代理

跟进到createStub()中,这里是直接通过反射来创建对象,传入的就是ref
继续往后,回到exportObject()

如果服务端定义好 stub,就会继续调用setSkeleton()方法,跟进去

这里有个createSkeleton()方法,顾名思义就是创建Skeleton的,在这个图里Skeleton就是服务端的代理

继续跟进到createSkeleton()方法中,这里skelcl是用Class.forName()来创建的

继续往后走,回到了exportObject()方法中,接下来就是到了创建 Target 了,这部分和前面一样,是用来封装数据的

直接跟进到下面的exportObject()里,这里和前面一样




直接跳到上面这里,然后跟进这个exportObject()方法

第一个setExportedTransport()方法是一个赋值操作,就直接跳过了,直接继续跟进到ObjectTable.putTarget(),这个方法会将封装的数据放进去
查看封装了哪些数据
点开static中的objTable,查看里面的三个Target:
- Target@859
- Target@855
- Target@895

一个个看,先看最后一个 Target@895 的 value,这个是刚刚创建的,这里的 stub 是RegistryImpl_Stub的一个对象,主要是看ref,这里的端口都是 1099,也就是说 1099 注册中心的一些端口数据都有了,这俩ref是同一个

然后看第一个 Target@859 的 value,这个主要看 stub 是$Proxy对象的

最后再看第二个 Target@855 的 value,这里注意 stub 是 DGCImpl_Stub,是一个分布式垃圾回收的对象,也挺重要的

这里起来三个远程服务,其中的 1099 端口是固定的,另外两个端口是随机产生的
绑定
最后一步绑定,就是bind操作,断点下在下面这里,然后就可以直接接着上面的操作 F9 恢复程序到这个断点处了

来到bind()方法,传入的两个参数分别是名字RemoteObj和远程对象remoteObj,首先调用checkAccess()

跟进到checkAccess()方法中,这个方法就是检查是不是本地绑定的,就是一种安全检查,防止未授权访问,getClientHost()是用来获取ClientHost,可以直接跳过这个方法

继续回到bind()方法,下面到synchronized()方法,用来检查bindings里有没有东西,如果有的话就会抛出异常,这里的bindings是一个 HashTable 对象

这里的bindings肯定是啥都没有的,继续往下走,到调用bindings.put()方法,刚才看了bindings是一个 HashTable 对象,这里就是将name和远程对象 put 进去,也就是绑定了
小结
注册中心的流程有很多地方和远程对象类似,不过注册中心是一个持久的对象
服务端的整体过程就是:
- 创建远程对象
- 创建注册中心
- 绑定远程对象
3. 客户端请求注册中心-客户端
从注册中心获取远程对象的代理
RMIServer就直接运行

RMIClient的话就三句代码都下断点,开始调试

首先来到getRegistry()方法,传入的参数就是注册中心的 IP 和 port,这里再调用另一个getRegistry()方法

继续跟进,前面部分的就是一些 IP 和 port 的检查和赋值

继续往后,下面的流程和之前的很像,就是一些new LiveRef,还有新建了一个ref,进行了一些封装的操作,然后也调用了createProxy()方法,这里的用的是false,是一个临时对象

跟进到createProxy()方法,这里最终可以走到createStub()这里,然后通过反射来创建对象

通常可能会觉得获取远程对象是通过序列化和反序列化来获取,但实际上就是客户端在本地新建一个对象,注册中心传入参数,最终客户端想要获取的就是注册中心的 Stub 对象
通过远程代理对服务端进行远程调用
继续往下看lookup方法

跟进到lookup方法里,将想要的远程对象的名字RemoteObj传进去,这里因为这个版本的问题,打不了断点,会直接进入到newCall方法里,问题不大,看一下逻辑就行

这里发现多了一个param_1参数,这就是传入的var1,也就是名字RemoteObj,这里是调用的writeObject(),也就是通过序列化传进去的,到了注册中心就会肯定会通过反序列化进行读取

继续往下调用super.ref.invoke(var2);,这里的super就是UnicastRef类

这里手动找一下,在invoke()方法中会调用call.executeCall(),这是真正处理网络请求的方法,客户端的网络请求都是通过这个方法实现的

这里可以下断点,直接断到这里看

跟进去直接看到下面这245行,这里有个异常处理的地方,调用了in.readObject()方法,会进行反序列化,这就可以被 JRMP 攻击

看一下这个in是什么

不难理解,in 就是数据流里面的东西。这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 ———— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞。这里的漏洞相比于其他漏洞更为隐蔽
所以这里是第一个漏洞点,更加隐蔽的,也更容易被利用,因为所有的stub里,调用网络请求都会调用到这个方法
回到lookup()这里,继续往下看

进行网络连接后,这里会获取到返回值,然后回进行一次反序列化,和前面一样,如果注册中心返回一个恶意的对象,这里进行反序列化,就会导致漏洞,这是第二个漏洞点
小结
这部分就是客户端去连接注册中心,客户端想要从注册中心获取到 Stub 对象,会先在本地创建一个对象,然后通过注册中心传参建立起连接来完成,这部分不是序列化和反序列化完成的
然后是客户端获取服务端的远程对象,这部分是通过序列化和反序列化来完成的,并且存在两个可能的漏洞点
4. 客户端请求服务端-客户端
然后就到了最后一句代码这里

继续走会来到RemoteObjectInvocationHandler.invoke方法
remoteObj是一个动态代理,动态代理无论调用什么方法,都会先走到调用处理器的invoke方法

前面一堆if判断,都是关于抛出异常的,直接跳到最后这里

跟进invokeRemoteMethod()方法,下面这里有个重载方法ref.invoke()

跟进这个invoke方法,直接跳到下面这个for循环里,调用的marshalValue()方法

跟进看,这里先判断一堆类型,因为传入的是String类型,所以都不满足,直接到最后的else里,就会对value进行一次序列化的操作,这里的value就是传入的参数Suzen

出来再回到invoke方法中,再往后又看到了call.executeCall();,所以说客户端只要处理网络请求,就一定会执行这个方法,和上面一样,这里是存在漏洞点的,这里就是第一个漏洞点

继续往后看,来到unmarshalValue()方法,注意这里传入的是String类型的

跟进这个方法看,看起来和前面marshalValue()的样子差不多,因为传入的是String类型的,不满足前面一系列if判断,所以就直接执行到最后else的readObject,对服务端传回来的数据流in进行反序列化的操作,将值读回来,这里就是第二个漏洞点


小结
这也还是有两个漏洞点,第一个地方就是call.executeCall();,客户端只要处理网络请求,就一定会执行这个方法,也就一定会存在漏洞,也就是 JRMP 攻击,第二个地方就是在unmarshalValueSee()方法里,服务端返回参数时进行反序列化攻击
5. 客户端请求注册中心-注册中心
回头看一下这个图,想一下如何调试,断点应该下在哪

刚才是针对的客户端进行的调试分析,操作的都是 Stub,到了服务端这里就要换成 Skel,在前面的分析中可知,Skel 是被封装在 Target 中的,所以把断点打在 RMIServer 的处理 Target 的地方,也就是Transport类中的 173 行,注意这里是打在服务端的

然后 Debug 运行服务端的

再直接运行客户端的就可以断到了

先看一下target中有什么

target里的stub里是个ref,里面的端口是1099,也就是注册中心的

继续往下走到 180 行,就是将skel的值放到disp中

再继续往下走到 200 行这里,会调用disp.dispatch(),跟进看看

此时的skel不为null,可以走到oldDispatch()这里,继续跟进

直接来到410行这里的skel.dispatch(),继续跟进到了RegistryImpl_Skel,这里也是不能下断点调试的

和注册中心进行交互的方法有:list、bind、rebind、unbind、lookup
这里面有很多的case,对应的方法:
- 0 => bind
- 1 => list
- 2 => lookup
- 3 => rebind
- 4 => unbind
下面这几个都是带有readObject()可以反序列化,也就是可以攻击的




小结
当客户端请求注册中心的时候,注册中心就是处理 Target,还有生成了 Skel 并进行处理,漏洞点在skel.dispatch()里,有几个反序列化的入口类
6. 客户端请求服务端-服务端
断点还是打在 Target 这里

然后服务端调试,客户端运行,这里有个注意点

刚开始断到这里的时候是和刚才上面一样的,此时的target里的stub是RegistryImpl_stub的,这个分析过了,直接 F9 跳过

跳过一次后又回到这里,此时的target里的stub是DGCImpl_Stub的,这个类是用来处理内存垃圾的,也不是我们想要的,所以再 F9 跳过一次

这个时候target里的stub就是想要的$Proxy动态代理的了

还是来到这个dispatch方法,跟进去

这里的num=-1,skel=null,所以就不会到oldDispatch()

继续往下来到上图这里,获取输入流和 Method,这里的 Method 就是要调用的远程方法,也就是写的satHello()方法

继续往下走,又来到了unmarshalValue()方法

和上面一样,传入的type是String类型的,所以会走到else里的in.readObject(),存在漏洞点
这里就是通过反序列化获取到客户端传过来的参数值

再往下到这里就是进行远程调用方法了

然后到下面marshalValue()方法里进行序列化,传回给客户端再进行反序列化

小结
客户端将调用远程方法的参数值传给服务端,服务端会通过反序列化来获取到参数值,这里就存在漏洞点,然后调用的方法执行完后会将返回值进行序列化并传回到客户端,客户端通过反序列化获取到返回值,客户端比服务端多了一个 JRMP 攻击
7. DGC的stub
DGC之前提到过,就是分布式垃圾回收,前面分析知道会有三个 Target,其中有一个是 DGC的,来分析一下这个是怎么创建出来的

断点下在ObjectTable.java中的putTarget()里,可以直接下在 174 行,之前的断点可以删去了,直接调试服务端即可

有一个点,第一次来到这里的target的stub是Proxy的

所以要按一次 F9 才行

这里的dgcLog是一个静态变量,当调用一个静态变量是会完成一个类的初始化,初始化的时候实际上会调用它的静态代码块,跟进到DGCImpl类里,找到这个静态代码块,在创建对象这里打一个断点

这里一直断不到,搞了半天进来发现是源码和字节码不匹配

又重新搞了一个 sun 包
https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

重新调,这里按一次 F9 就可以断到这里了

继续往下会到createProxy()这个方法,

跟进后是可以走到createStub()这里的,这就和前面注册中心创建 stub 一样


此时 DGCImpl_Stub 的服务就创建好了,类似于注册中心创建远程服务一样,但是注册中心是用来注册的,这里是用来内存回收的,并且这里的端口是随机的

最后就是回到putTarget()方法里,put进表里,这和前面的都一样

主要来看一下漏洞点,在DGCImpl_stub中有clean()和dirty()两个方法,clean就是 ”强” 清除内存,dirty就是 ”弱” 清除内存

在dirty()里有调用readObject的地方能反序列化,这里是一个漏洞点,会被 JRMP 攻击

还有在DGCImpl_Skel类里的dispatch()方法里的两个case也有漏洞点


总的来说 DGC 是自动创建用于清理内存的,在服务端和客户端都存在漏洞点,在Skel和Stub里,也就是 JRMP 攻击
总结
创建远程服务:
服务端通过exportObject()方法发布远程对象到随机端口,创建 Stub 作为客户端代理,并将服务信息记录在静态 HashMap 中
创建注册中心并绑定:
注册中心在 1099 端口启动,创建RegistryImpl_Skel和RegistryImpl_Stub,服务端将远程对象通过bind()方法注册到注册中心的 HashTable 中
客户端请求注册中心-客户端:
客户端通过getRegistry()本地创建Registry_Stub代理,通过lookup()方法序列化服务名请求注册中心,反序列化获取远程对象 Stub 引用
客户端请求服务端-客户端:
客户端通过获取的远程Stub调用方法,通过marshalValue()序列化参数,经JRMP协议传输,在executeCall()和unmarshalValue()处存在反序列化漏洞点
客户端请求注册中心-注册中心:
注册中心通过RegistryImpl_Skel.dispatch()处理客户端请求,在lookup、bind等操作的readObject()处存在反序列化入口,可被 JRMP 攻击利用
客户端请求服务端-服务端:
服务端 Skeleton 通过dispatch()解析方法调用,在unmarshalValue()中反序列化客户端参数,执行远程方法后序列化返回值返回客户端
DGC的stub:
系统自动创建DGCImpl_Stub处理分布式垃圾回收,在dirty()、clean()方法和 Skeleton 的dispatch()中均存在反序列化漏洞点




