深入理解 RMI 之运行逻辑与漏洞原理

深入理解 RMI 之漏洞原理篇

  • 环境是 jdk8u65
  • 本文侧重于理解原理,攻击篇会放到后续一篇中讲。

0x01 前言

RMI 作为后续漏洞中最为基本的利用手段之一,学习的必要性非常之大。本文着重偏向于 RMI 通信原理的理解,如果只懂利用,就太脚本小子了。

这里有个坑点:就是 RMI 当中的攻击手法只在 jdk8u121 之前才可以进行攻击,因为在 8u121 之后,bind rebind unbind 这三个方法只能对 localhost 进行攻击,后续我们会提到。

0x02 RMI 基础

1. RMI 介绍

RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。

RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。

  • 这个协议就像 HTTP 协议一样,规定了客户端和服务端通信要满足的规范。

RMI 包括以下三个部分

image

Server ———— 服务端:服务端通过绑定远程对象,这个对象可以封装很多网络操作,也就是 Socket
Client ———— 客户端:客户端调用服务端的方法

因为有了 C/S 的交互,而且 Socket 是对应端口的,这个端口是动态的,所以这里引进了第三个 RMI 的部分 ———— Registry 部分。

  • Registry ———— 注册端;提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。

2. RMI 的实现

  • 这里最好把服务端与客户端拆分成两个工程来做,会更有助于理解。

先来写服务端 ———— Server

服务端

1. 先编写一个远程接口,其中定义了一个 sayHello() 的方法

public interface RemoteObj extends Remote {  
  
    public String sayHello(String keywords) throws RemoteException;  
}

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

2. 定义该接口的实现类 Impl

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 类,用于生成 Stub(存根)和 Skeleton(骨架)。 这个在后续的通信原理当中会讲到
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable

3. 注册远程对象

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,不写会自动补上,其他端口必须写
  • bind 的绑定这里,只要和客户端去查找的 registry 一致即可。

如此,服务端就写好了

客户端

客户端只需从从注册器中获取远程对象,然后调用方法即可。当然客户端还需要一个远程对象的接口,不然不知道获取回来的对象是什么类型的。

所以在客户端这里,也需要定义一个远程对象的接口:

public interface RemoteObj extends Remote {  
  
    public String sayHello(String keywords) throws RemoteException;  
}

然后编写客户端的代码,获取远程对象,并调用方法

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

这样就能够从远端的服务端中调用 RemoteHelloWorld 对象的sayHello()方法了。

0x03 从 Wireshark 抓包分析 RMI 通信原理

  • 这里文章大部分是引用其他师傅的,我们可以先通过 Wireshark 的抓包心里有个底。

数据端与注册中心(1099 端口)建立通讯

  • 客户端查询需要调用的函数的远程引用,注册中心返回远程引用和提供该服务的服务端 IP 与端口。
image

数据端与注册中心(1099 端口)建立通讯完成后,RMI Server 向远端发送了⼀个 “Call” 消息,远端回复了⼀个 “ReturnData” 消息,然后 RMI Server 端新建了⼀个 TCP 连接,连到远端的 33769 端⼝

image

AC ED 00 05是常见的 Java 反序列化 16 进制特征
注意以上两个关键步骤都是使用序列化语句

客户端新起一个端口与服务端建立 TCP 通讯

客户端发送远程引用给服务端,服务端返回函数唯一标识符,来确认可以被调用

image

同样使用序列化的传输形式

以上两个过程对应的代码是这两句

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);  
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj"); // 查找远程对象

这里会返回一个 Proxy 类型函数,这个 Proxy 类型函数会在我们后续的攻击中用到。

客户端序列化传输调用函数的输入参数至服务端

  • 这一步的同时:服务端返回序列化的执行结果至客户端
image

以上调用通讯过程对应的代码是这一句

remoteObj.sayHello("hello");

可以看出所有的数据流都是使用序列化传输的,那必然在客户端和服务带都存在反序列化的语句。

总结一下 RMI 的通信原理

实际建⽴了两次 TCP 连接,第一次是去连 1099 端口的;第二次是由服务端发送给客户端的。

在第一次连接当中,是客户端连 Registry 的,在其中寻找 Name 为 hello 的对象,这个对应数据流中的 Call 消息;然后 Registry 返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应数据流中的ReturnData消息。

到了第二次连接,服务端发送给客户端 Call 的消息。客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 172.17.88.209:24429,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是sayHello()

RMI Registry 就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但 RMI Server 可以在上⾯注册⼀个 Name 到对象的绑定关系;RMI Client 通过 Name 向 RMI Registry 查询,得到这个绑定关系,然后再连接 RMI Server;最后,远程⽅法实际上在 RMI Server 上调⽤。

原理图如图

image

那么我们可以确定 RMI 是一个基于序列化的 Java 远程方法调用机制。

0x04 从 IDEA 断点分析 RMI 通信原理

  • RMI 的这个流程是相当复杂的,需要师傅们有一定的耐心看下去。

1. 流程分析总览

首先 RMI 有三部分:

  • RMI Registry
  • RMI Server
  • RMI Client

如果两两通信就是 3+2+1 = 6 个交互流程,还有三个创建的过程,一共是九个过程。

RMI 的工作原理可以大致参考这张图,后续我会一一分析。

image

2. 创建远程服务

> 先行说明,创建远程服务这一块是不存在漏洞的。

断点打在 RMIServer 的创建远程对象这里,如图

image

发布远程对象

开始调试,首先是到远程对象的构造函数RemoteObjImpl,现在我们要把它发布到网络上去,我们要分析的是它如何被发布到网络上去的

RemoteObjImpl这个类是继承于UnicastRemoteObject的,所以先会到父类的构造函数,父类的构造函数这里的 port 传入了 0,它代表一个随机端口。

image

这个过程不同于注册中心的 1099 端口,这是远程服务的。有很多文章在这个地方都交代的不清楚,误导了一些师傅。

远程服务这里如果传入的是 0,它会被发布到网络上的一个随机端口,我们可以继续往下看一看。先 f8 到exportObject(),再 f7 跳进去看。

exportObject()是一个静态函数,它就是主要负责将远程服务发布到网络上,如何更好理解exportObject()的作用呢?我们可以看到RemoteObjImpl这个实现类的构造函数里面,我注销了一句代码

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

如果不继承UnicastRemoteObject这个类的话,我们就需要手动调用这个函数。

我们来看这个静态函数,第一个参数是 obj 对象,第二个参数是new UnicastServerRef(port),第二个参数是用来处理网络请求的。继续往下面跟,去到了UnicastServerRef的构造函数。这里跟的操作先 f7,然后点击UnicastServerRef跟进,这是 IDEA 的小技巧。

image

跟进去之后 UnicastServerRef 的构造函数,我们看到它 new 了一个 LiveRef(port),这个非常重要,它算是一个网络引用的类,跟进看一看。

image

跟进去之后,先是一个构造函数,先跳进 this 看一看

image

跳进 this 后的构造函数如下

public LiveRef(ObjID objID, int port) {  
    this(objID, TCPEndpoint.getLocalEndpoint(port), true);  
}

第一个参数 ID,第三个参数为 true,所以我们重点关注一下第二个参数。

TCPEndpoint 是一个网络请求的类,我们可以去看一下它的构造函数,传参进去一个 IP 与一个端口,也就是说传进去一个 IP 和一个端口,就可以进行网络请求。

image

继续 f7 进到 LiveRef 的构造函数 this 里面

image

这时候我们可以看一下一些赋值,发现 host 和 port 是赋值到了 endpoint 里面,而 endpoint 又是被封装在 LiveRef 里面的,所以记住数据是在 LiveRef 里面即可,并且这一 LiveRef 至始至终只会存在一个。

image

上述是 LiveRef 创建的过程,然后我们再回到之前出现LiveRef(port)的地方

回到上文那个地方,继续 f7 进入 super 看一看它的父类UnicastRef,这里就证明整个创建远程服务的过程只会存在一个 LiveRef。一路 f7 到一个静态函数exportObject(),我们后续的操作过程都与exportObject()有关,基本都是在调用它,这一段不是很重要,一路 f7 就好了。直到此处出现 Stub

image

这里在我们服务端创建远程服务这一步居然出现了 stub 的创建,其实原理是这个样子的,来结合这张图一起说:

image
  • RMI 先在 Service 的地方,也就是服务端创建一个 Stub,再把 Stub 传到 RMI Registry 中,最后让 RMI Client 去获取 Stub。

接着我们研究 Stub 产生的这一步,先进到 createProxy 这个方法里面

先进行了基本的赋值,然后我们继续 f8 往下看,去到判断的地方。
image

这个判断暂时不用管,后续我们会碰到,那个时候再讲。

再往下走,我们可以看到这是很明显的类加载的地方

image

第一个参数是 AppClassLoader,第二个参数是一个远程接口,第三个参数是调用处理器,调用处理器里面只有一个 ref,它也是和之前我们看到的 ref 是同一个,创建远程服务当中永远只有一个 ref。

此处就把动态代理创建好了,如图 Stub

image

继续 f8,到 Target 这里,Target 这里相当于一个总的封装,将所有用的东西放到 Target 里面,我们可以进去看一看 Target 里面都放了什么。

image

并且这里的几个 ref 都是同一个,通过 ID 就可以查看到它们是同一个。比如比较 disp 和 stub 的。一个是服务端 ,一个是客户端的,ID 是一样的,都是 818

image

一路 f8,回到之前的 Target,下一条语句是ref.exportObject(target),也就是把 target 这个封装好了的对象发布出去。

image

我们跟进去看一下它的发布逻辑是怎么一回事,一路 f7 到这里

image

从这里开始,第一句语句 listen,真正处理网络请求了跟进去。

image

先获取 TCPEndpoint,然后我们继续 f8 往后看,直到server = ep.newServerSocket();这里。

image

它创建了一个新的 socket,已经准备好了,等别人来连接,所以之后在 Thread 里面去做完成连接之后的事儿,这里我挂几张图展示一下运行的逻辑。

image
image

并且这个newServerSocket()方法会给 port 进行赋值,核心语句如图

image

然后回到 listen 去,一路 f8,观察一下整个流程结束之后 Target 里面是增加了 port。

image

发布完成之后的记录

  • 也就是记录一下远程服务被发到哪里去了。

第一个语句target.setExportedTransport(this);是一个简单的赋值,我们就不看了,看下面的ObjectTable.putTarget(target);,跟进去,一路 f8,因为都是一些赋值的语句,直到此处。

image

RMI 这里会把所有的信息保存到两个 table里面,有兴趣的师傅可以跟一下进去看看。

image

我个人理解这段东西有点像日志。

小结一下创建远程服务

从思路来说是不难的,也就是发布远程对象,用exportObject()指定到发布的 IP 与端口,端口的话是一个随机值。至始至终复杂的地方其实都是在赋值,创建类,进行各种各样的封装,实际上并不复杂。

还有一个过程就是发布完成之后的记录,理解的话,类似于日志就可以了,这些记录是保存到静态的 HashMap 当中。

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

3. 创建注册中心 + 绑定

  • 创建注册中心与服务端是独立的,所以谁先谁后无所谓,本质上是一整个东西。

断点打在此处,开始调试

image

创建注册中心

首先会经过一个静态方法 ————createRegistry,继续往下,走到了RegistryImpl这个对象下,f8 进去,会发现新建了一个RegistryImpl对象。这里 122 行,判断 port 是否为注册中心的 port,以及是否开启了 SecurityManager,也就是一系列的安全检查,这部分不是很重要,继续 f8

image

再往下走,它创建了一个LiveRef,以及创建了一个新的UnicastServerRef,这段代码就和我们上面讲的创建远程对象是很类似的,我们可以跟进setup看一下。

image

跟进之后发现和之前是一样的,也是先赋值,然后进行exportObject()方法的调用。

我这里贴两张图,第一张是发布远程对象的,第二张是创建注册中心的,师傅们可以对比对比。

image
image
  • 区别在于第三个参数的不同,名为 permanent,第一张是 false,第二张是 true,这代表我们创建注册中心这个对象,是一个永久对象,而之前远程对象是一个临时对象。

f7 进到 exportObject,就和发布远程对象一样,到了创建 Stub 的阶段。

image
  • 那这个 Stub 是怎么创建的呢?诶 ~ 这里就和前面的有大不一样了。我们还是跟进createProxy()中,首先这里要做一个判断。
image

可以跟进stubClassExists进行判断,我们看到这个地方,是判断是否能获取到RegistryImpl_Stub这个类,换句话说,也就是若RegistryImpl_Stub这个类存在,则返回 True,反之 False。我们可以找到RegistryImpl_Stub这个类是存在的。

image
  • 对比发布远程对象那个步骤,创建注册中心是走进到createStub(remoteClass, clientRef);进去的,而发布远程对象则是直接创建动态代理的。
image

执行的这个方法也很简单,就是直接通过反射创建这个对象,里面放的就是 ref

image

相比于之前发布远程对象中的 Stub,是一个动态代理,里面放的是一个 ref。
现在发布远程对象是用 forName 创建的,里面放的也是 ref,是一致的。

image

继续往下,如果是服务端定义好的,就调用setSkeleton()方法,跟进去。然后这里有一个createSkeleton()方法,一看名字就知道是用来创建 Skeleton 的,而 Skeleton 在我们的那幅图中,作为服务端的代理。

image

Skeleton 是用forName()的方式创建的,如图。

image
  • 再往后走,又到了 Target 的地方,Target 部分的作用也与之前一样,用于储存封装的数据
image
  • 所以这一段和前面一样,就迅速跳过了,到如图这个地方。
image

继续走,直到super.exportObject(target);这里,f7 跟进,到里面有一个putTarget()方法,它会把封装的数据放进去。

image

一路 f8,到后面看一下到底放了什么东西进去。

image

查看封装了哪些数据进去

查看 static 中的数据,点开objTable中查看三个 Target,我们逐个分析一下,分析的话主要还是看 ref ~

先点开这个 Target@930 的 value,主要关注几个参数:disp 中的 skel,以及 stub。它们的端口都是 1099,也就是说 1099 注册中心的一些端口数据都有了。这两个 ref 是同一个,可以对比着看一下。

image

先点开这个 Target@1065 的 value,存储里面需要我们关注的有 stub 是$Proxy对象的,如图查看它们的 ref

image

再点开 Target@1063 的 value 的 stub 值,发现它为 DGCImpl_Stub,是分布式垃圾回收的一个对象,它并不是我们刚才创建的。这个东西挺重要的。

image

所以这里就是起了几个远程服务,一个端口是固定了,另外两个端口是不固定的,随机产生的。至于为什么这里有三个 Target 呢?

  • 这个我们在第六点里面会讲到。

绑定

  • 绑定也就是最后一步,bind 操作

断点下在 bind 语句那里。我们开始调试

image

首先检查是否是本地绑定的,有兴趣的师傅们可以跟一下,是都会通过的,我这里就不跟了

image

下一句检查一下 bindings 这里面是否有东西,其实 bindings 就是一个 HashTable。如果里面有数据的话就抛出异常。

image

继续往前走,就是bindings.put(name, obj);,也挺好理解的,就是把 IP 和端口放进去,到此处,绑定过程就结束了hhhh,是最简单的一个过程。

image

小结一下创建注册中心 + 绑定

  • 总结一下比较简单,注册中心这里其实和发布远程对象很类似,不过多了一个持久的对象,这个持久的对象就成为了注册中心。

绑定的话就更简单了,一句话形容一下就是hashTable.put(IP, port)

3. 客户端请求,客户端调用注册中心

  • 这一部分是存在漏洞的点,原因很简单,因为前文我们在 Wireshark 的抓包里头说到:”RMI 是一个基于序列化的 Java 远程方法调用机制”,这里有一些个有问题的反序列化 ~
  • 且听我娓娓道来

客户端的请求分为三个阶段,获取注册中心,查找对象,

获取注册中心

这一块不存在漏洞,我们可以调试看一下,很简单。

断点的话,三句代码都先下断点,接着开始调试。

image

进到getRegistry()方法里面,继续往下走,这里调试部分大家可以自己看一下,都不难的,无非是一些赋值与判断,大致流程其实和之前是很像的,有new LiveRef的操作,有Util.createProxy()的操作,感兴趣的师傅们可以跟进去看一下,是一样的流程。也是通过forName的方式创建的。

image

就和之前一样,新建了一个 Ref,然后把该封装的都封装到 Ref 里面进去。这里封装的是127.0.01:1099的,这里我们就获取到了注册中心的 Stub,下一步就是去查找远程对象。

查找远程对象

  • 这里调试的话,因为对应的 Java 编译过的 class 文件是 1.1 的版本,无法进行打断点,所以会直接跳到其他地方去,比如此处。
image

代码是可以按照正常的逻辑走的,就是打不了断点,问题不大,我们主要分析一下代码运行的逻辑。

先看我们变量里面多了一个param_1="remoteObj",这个东西就是传参的 String var1,这个 var1 最后是作为序列化的数据传进去的。注册中心后续会通过反序列化读取。

image

接着下一步,我们看到super.ref.invoke(var2);,super 就是父类,也就是我们之前说的UnicastRef这个类。这里的invoke()方法是类似于激活的方法,invoke()方法里面会调用call.executeCall(),它是真正处理网络请求的方法,也就是客户端的网络请求都是通过这个方法实现的。

image
  • 这个方法后续再细讲,先看整个代码运行的逻辑。

我们的逻辑现在是从invoke()—>call.executeCall()—>out.getDGCAckHandler(),到out.getDGCAckHandler()这个地方的时候,是 try 打头的,这里它有一个异常存在潜在攻击的可能性,如图,中间省略了部分代码。

image

我们先看一下in这个变量是什么

image

不难理解,in 就是数据流里面的东西。这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 ———— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞。这里的漏洞相比于其他漏洞更为隐蔽。

  • 也就是说,只要调用invoke(),就会导致漏洞。RMI 在设计之初就并未考虑到这个问题,导致客户端都是易受攻击的。

上述就是注册中心与客户端进行交互时会产生的攻击。

我们这里继续 f8,看一下到最后一步的时候获取到了什么数据。简单来说就是获取到了 RemoteObj 这个动态代理,其中包含一个 ref。

image

4. 客户端请求,客户端请求服务端

存在漏洞

这里就是客户端请求的第三句代码 ————remoteObj.sayHello("hello");的运行逻辑。

这里如果 Debug 有问题的话,可以先在RemoteObjectInvocationHandler类下的invoke()方法的 if 判断里面打个断点,这样才能走进去。调试开始

image

下面是一堆 if 的判断,都是关于抛出异常的,这里就不再细看了,直接跳过。直到尾部这个地方,我们跟进去看一下。

image

跟进到此处,ref.invoke(),这是一个重载的方法,跟进到重载的invoke()方法里面。这个重载的invoke方法作用是创建了一个连接,和之前也比较类似。我们可以看一下它具体的逻辑实现。

image

继续往里走,在循环里面有一个marshalValue()方法。

image

它会序列化一个值,这个值其实就是我们传进的参数hello,它的逻辑如图。判断一堆类型,之后再进行序列化。

image

继续往前走,我们看到一个注释// unmarshal return,后面接的是call.executeCall(),之前我们也看到了这个方法,也就是说只要 RMI 处理网络请求,就一定会执行到这个方法,这里是存在危险的,原理上面已经代码跟过一遍了 ~

所以我们直接往后看。

  • 这里有一个unmarshalValueSee的方法,因为现在我们传进去的类型是 String,不符合上面的一系列类型,这里会进行反序列化的操作,把这个数据读回来,这里是存在入口类的攻击点的。
image
image

这个数据会被读回来

image

关于客户端一系列主动请求的小结

  • 先说说存在攻击的点吧,在注册中心 –> 服务端这里,查找远程对象的时候是存在攻击的。

具体表现形式是服务端打客户端,入口类在call.executeCall(),里面抛出异常的时候会进行反序列化。
这里可以利用 URLClassLoader 来打,具体的攻击在后续文章会写。

在服务端 —> 客户端这里,也是存在攻击的,一共是两个点:一个是call.executeCall(),另一个点是unmarshalValueSee这里。

  • 再总结一下代码的流程

分为三步走,先获取注册中心,再查找远程对象,查找远程对象这里获取到了一个 ref,最后客户端发出请求,与服务端建立连接,进行通信。

5. 客户端发起请求,注册中心如何处理

先说说断点怎么打,因为客户端那里,我们操作的是 Stub,服务端这边操作的是 Skel。在有了 Skel 之后应当是存在 Target 里面的,所以我们的断点打到处理 Target 的地方。

断点位置如图

image

先点 Server 的 Debug,再跑 Client 就可以了,成功的打断点如图

image

往下走,我们先看一看 Target 里面包含了什么

image

里面包含一个 stub,stub 中是一个 ref,这个 ref 对应的是 1099 端口。

image

再往下走final Dispatcher disp = target.getDispatcher();是将skel的值放到 disp 里面。

image

继续往下走,它会调用 disp 的 dispatch 方法,我们跳进去看一下disp.dispatch()

image

继续走,我们目前的skel不为 null,会到oldDispatch()这里,跟进。

image

下面就是skel.dispatch()的过程了,这里才是重点,这里就是很多师傅文章里面会提到的 客户端打注册中心的攻击方式。

image
  • 先介绍一下这段源码吧,很长,基本都是在做 case 的工作。

我们与注册中心进行交互可以使用如下几种方式:

  • list
  • bind
  • rebind
  • unbind
  • lookup

这几种方法位于RegistryImpl_Skel#dispatch中,也就是我们现在 dispatch 这个方法的地方。

如果存在对传入的对象调用readObject方法,则可以利用,dispatch里面对应关系如下:

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

只要中间是有反序列化就是可以攻击的,而且我们是从客户端打到注册中心,这其实是黑客们最喜欢的攻击方式。我们来看一看谁可以攻击。

image
image
image
image

也就是除了 list 都可以。

小结一下客户端发起请求,注册中心做了什么

简单,注册中心就是处理 Target,进行 Skel 的生成与处理。

漏洞点是在 dispatch 这里,存在反序列化的入口类。这里可以结合 CC 链子打的。

6. 客户端发起请求,服务端做了什么

  • 这个流程是比较简单的,同第四点一样,此处得到的 Skel 是动态代理$Proxy0这个类的,之前我们提到过其实是封装了三个 Target 的,这就是其中之一。

这里的断点位置打两个,如图:

image

也就是当前请求到的是服务端的 Target,我们开始调试。

  • 调试这里有一点小坑,打完两个断点之后,我们得到的第一个 Target 中的 Stub 是 DGCImpl 的,我们要的不是这个,前文我们提到过,这个类是用来处理内存垃圾的。

动态代理的 stub

这里要摁两下一下 f9,直至有 Proxy 动态代理的 stub 为止,如图:

image

在这种情况下,我们到dispatch()方法下,跟进。

image

这里的 skel 为 null,所以不会执行 oldDispatch 方法,如图

image

继续往下走,获取到输入流,以及 Method,Method 就是我们之前写的sayHello()方法。

image

继续往下走,重点部分来了 ———— 循环当中的unmarshalValue()方法,这里和我们之前说的一样,是存在漏洞的。

image

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

DGC 的 stub

三个 Target 当中的一个,如图

image

断点需要下在ObjectTable类的putTarget()方法里面。并且将前面两个断点去掉,直接调试即可。

image
  • 首先我们去看一看 DGC 的运行原理是什么

还是比较简单的,将 Target 放到一个静态表里面,这里静态表就是我在第三点说的,ObjectTable 里面封装了三个 Target。

image

然后这里我们会发现,放进去的是 Proxy 这个动态代理的 Target 而非 DGC 的 Target。

这个 DGC 的 Target 挺奇妙的,是已经被封装到了 static 里面,我们去看 static 里面,发现它已经被封装进去了。

image
  • 那它到底是怎么创建的呢?我们一步步看。

在 DGC 这个类在调用静态变量的时候,就会完成类的初始化。

类的初始化是由 DGCImpl 这个类完成的,我们跟到 DGCImpl 中去看,发现里面有一个 static 方法,作用是 class initializer

image
  • 我们可以在创建对象的地方打个断点。

后续的过程,首先是 new 了很多对象,这些其实都是 Target 的一堆属性,不过这是封装之前的。

后续的部分,createProxy()方法这里,和注册中心创建远程服务的特别像。

image

createProxy()方法进去,会看到一个createStub()方法,跟进去。

image

这里和注册中心创建远程服务一样,尝试是否可以获取到这一个类 ————DGCImpl_Stub

image

这一个 DGCImpl_Stub 的服务至此已经被创建完毕了,它也是类似于创建远程服务一样,但是它做的业务不一样。注册中心的远程服务是用于注册的,这个是用于内存回收的,且端口随机。

image

setSkeleton()这个过程就是在 disp 里面创建skel,和之前是一样的。

调用过程是与第 3、4 点讲的一样的,这里就不重复了。

我们重点关注一下 DGC 的 Stub 里面有漏洞的地方。

DGCImpl_Stub这个类下,它有两个方法,一个是 clean,另外一个是 dirty。clean 就是”强”清除内存,dirty 就是”弱”清除内存。

这里调用了readObject()方法,存在反序列化的入口类。

image

同样在DGCImpl_Skel这个类下也存在反序列化的漏洞,如图。

image

小结一下 DGC 的过程

  • 是自动创建的一个过程,用于清理内存。

漏洞点在客户端与服务端都存在,存在于SkelStub当中。这也就是所谓的 JRMP 绕过

0x05 总结

  • 如果是漏洞利用的话,单纯攻击 RMI 意义是不大的,不论是 codespace 的那种利用,难度很高,还是说三者互相打这种,意义都不是很大,因为在 jdk8u121 之后都基本修复完毕了。

RMI 多数的利用还是在后续的 fastjson,strust2 这种类型的攻击组合拳比较多,希望这篇文章能对正在学习 RMI 的师傅们提供一点帮助。

具体的攻击 payload 可以看我另外一篇文章

0x06 参考资料

https://www.bilibili.com/video/BV1L3411a7ax?p=10&vd_source=a4eba559e280bf2f1aec770f740d0645
https://johnfrod.top/%e5%ae%89%e5%85%a8/rmi%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96/

本文作者:Drunkbaby, 转载请注明来自FreeBuf.COM

主题测试文章,只做测试使用。发布者:1869,转转请注明出处:https://community.anqiangkj.com/archives/21631

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年8月3日 下午9:51
下一篇 2022年8月3日 下午9:53

相关推荐