• 本文作者: 
  • |
  • 2020年12月7日
  • |
  • 未分类
  • |

Java反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解

Java反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解

 

前言

Java反序列化漏洞一直都是Java Web漏洞中比较难以理解的点,尤其是碰到了RMI和JNDI种种概念时,就更加的难以理解了。笔者根据网上各类相关文章中的讲解,再结合自己对RMI JRMP以及JNDI等概念的理解,对 RMI客户端、服务端以及rmiregistry之间的关系,和三方之间的多种攻击方式进行了详细的介绍,希望能对各位读者学习Java Web安全有所帮助。

RPC框架原理简介

首先讲这些之前要明白一个概念,所有编程中的高级概念,看似很高级的一些功能什么的,都是建立于最基础的代码之上的,再高级也他离不开JDK。

例如此次涉及到的分布式的概念,就是通过java的socket,序列化,反序列化和反射来实现的。

举例说明 客户端要调用服务端的A对象的A方法,客户端会生成A对象的代理对象,代理对象里通过用Socket与服务端建立联系,然后将A方法以及调用A方法是要传入的参数序列化好通过socket传输给服务端,服务端接受反序列化接受到的数据,然后通过反射调用A对象的A方法并将参数传入,最终将执行结果返回给客户端,给人一种客户端在本地调用了服务端的A对象的A方法的错觉。

RMI流程源码分析

到后来JAVA RMI这块也不例外 但是为了方便更灵活的调用发展成了以下的样子

在客户端(远程方法调用者)和服务端(远程方法提供者)之间又多了一个丙方也就所谓的Registry也就是注册中心。

启动这个注册中心的代码非常简单,如下所示

这个Registry是一个单独的程序 路径位于/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/bin/rmiregistry

2

刚刚所示的启动RMIRegistry的代码,也就是调用了这个rmiregistry可执行程序而已。

简单follow一下代码

很简单 没啥东西 liveRef里面就四个属性

 

this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));这段里面有个参数RegistryImpl::registryFilter这个东西就是jdk1.8.121版本以后添加的registryFilter专门用来校验传递进来的反序列化的类的,不在反序列化白名单内的类就不准进行反序列化操作,具体的方法代码如下

这个白名单先暂且放一放,后面用到了再说。执行完new UnicastServerRef(var2, RegistryImpl::registryFilter)后简单看一下UnicastServerRef对象里的内容

1

setup方法内容

UnicastServerRef.exportObject() 方法内容

很好这样启动rmiregistry的过程就简单分析完毕了,但是此时有一个问题,就是为什么会需要rmiregistry这么一个注册机制?客户端和服务端之间直接通过Socket互相调用不就好了么?就像马士兵老师的代码那样。很明显那个只是简单的讲解原理的用例,实际生产环境中肯定不会这么简单。

首先看下面这个RMI简单的流程图

3

在考虑为什么需要这个rmiregistry之前,先思考一个比较尴尬的问题。就是客户端(远程方法调用方)要想调用服务端(远程放方法服务方)的话,客户端要怎样才能知道服务端用来提供远程方法调用服务的ip地址和端口号?你说直接事先商量好然后写死在代码里面?可是服务方提供的端口号都是随机的啊,总不能我服务端每增加一个新的远程方法提供类就手动指定一个新的端口号吧?

所以现在就很尴尬,陷入了一个死循环,客户端想要调用服务端的方法客户端就需要先知道服务端的地址和对应的端口号,但是客户端又不知道因为没人告诉他。。。所以就相当的头疼。

此时就有了rmiregistry这么一个东西,我们先把rmiregistry称为丙方,功能很简单,服务端每新提供一个远程方法,都会来丙方(rmiregistry)这里注册一下,写明提供该方法远程条用服务的ip地址以及所对应的端口以及别的一些信息。

如下面的代码所示,首先我们如果要写一个提供远程方法调用服务的类,首先先写一个接口并继承Remote接口,

然后写一个类来实现这个接口

最后将这个HelloImpl类注册到也可以说是绑定到rmiregistry也就是丙方中

首先我们先跟一下HelloImpl这个远程对象的实例化过程,首先HelloImpl是UnicastRemoteObject的子类,所以HelloImpl在实例化时会先调用UnicastRemoteObject类的构造方法,其构造方法内容如下

发现其会调用一个exportObject方法,继续跟进该方法

继续跟进UnicastServerRef.exportObject方法,其内部代码如下

其中Util.createProxy()方法返回的结果如下图所示

26

继续跟入this.ref.exportObject(var6),经过一系列的嵌套调用,最终来到了TCPTransport的exportObject方法,该方法内容如下

此处跟进this.listen()方法,

经由以上分析,我们可知每创建一个远程方法对象,程序都会为其创建一个独立的线程,并为其指定一个端口号。

 

在分析完了远程方法提供对象实例化的过程后,也简单跟一下这个getRegistry()bind()方法吧

首先是getRegistry()代码如下

 

关键点在在于后面这几行代码

LocateRegistry.createRegistry()有那么点相似

最关键的在于下面这行

所以说从源码上分析 LocateRegistry.getRegistry()LocateRegistry.createRegistry()最后的返回结果应该是一样的,我们看一下结果

4

果然不出所料返回的同样都是RegistryImpl_Stub对象,只不过LocateRegistry.getRegistry()执行完不会在本地再开一个监听端口罢了。

好了 现在我们有了一个RegistryImpl_Stub对象,我们要用它来将我们的HelloImpl注册到rmiregistry中,用到的是RegistryImpl_Stub.bind()方法。

ok,hold on 我们先来了解一下这个RegistryImpl_Stub首先该类是继承了RemoteStub,并实现了Registry, Remote接口(我们的HelloImpl也实现了这个接口),

该类的方法不多,就下面截图里这么些。没必要全都看,先看bind就行。

5

bind方法详细代码如下

这里需要注意下,这里向rmiregistry发送的是序列化信息,既然一方有序列化的行为那么另一方必然会有反序列化的行为。

到此为止服务端也就是远程方法服务方这边的操作暂且告一段落,因为此时我们的HelloImpl已经注册到了rmiregistry中。

接下来我们返回rmiregistry的代码,来看一看这边的情况。

之前跟踪rmiregistry这边的LocateRegistry.createRegistry()这段代码时有经过这样一行代码

这个Skeleton就是前面流程里面的骨架,当执行完上面这两步的时候,UnicastServerRef的skel属性被赋值为一个RegistryImpl_Skel对象

6

我们来看一下这个RegistryImpl_Skel的相关信息,首先该类实现了Skeleton接口,该类的方法很少,如下图所示

7

其中最关键的方法就是dispatch方法,我们看下在Skeleton接口中对该方法的一个描述

不难理解该方法就是对传入的远程调用信息进行分派调度的。其部分代码如下。

我们来看一个这个binding属性里的详细信息

8

从这里我们明白了rmiregistry的本质就是一个HashMap,所有注册过的远程方法以键值对的形式存放在这里,当客户端来查询时,rmiregistry将对应的键值对中的Proxy返回给客户端,这样客户端就知道了服务端的地址和所对应的端口号,就可以进行通信了。

这其中有一个比较关键的类,在后续的绕过高版本JDK JEP290的白名单是会用到,就是UnicastRef,详观察不难发现该对象中存有rmi服务端的ip地址以及对应远程方法的端口号,该类在客户端、rmiregistry、以及服务端的通信中都起到了非常重要的作用,UnicastRef中有一个newCall方法 具体代码如下。

该方法会在java的DGC(分布式垃圾回收机制)中被调用,DGC则是我们绕过高版本JDK反序列化限制的一个重要的环节

首先客户端的代码

LocateRegistry.getRegistry()没必要再分析一遍了,直接看lookup方法,部分代码如下

这里又提到了Stub我们来看看其反序列化完成后是什么样的吧

9

和之前在rmiregistry中看到的那个HashMap中的值一模一样,这下客户端就知道服务端的地址和端口号了,通过这些信息就可以和服务端进行通信了。

不过在此之前在看一下rmiregistry是怎么处理客户端的查询信息的。

如此这般,这般如此,rmiregistry这块处理客户端的查询信息的部分就简单分析完了。

然后回到客户端这里

贴一下调用链

10

可以看到核心内容都在UnicastRef的Invoke方法, 下面是该方法的部分代码

ok客户端这边的处理过程到此就已经完毕了,接下来跟到服务端看一看。

11

根据调用链信息,先来看UnicastServerRef.dispatch()方法

好了服务端这边也简单的分析完了,我们来总结一下,在这些过程中可以利用的反序列化点。

首先是服务端调用bind方法像rmiregistry注册远程方法的信息时,在执行的过程中,调用了RegistryImpl_Skel.dispatch方法,反序列化服务端传来的数据,此为一个利用点,我们可以修改传递的数据从而达到从服务端对rmiregistry进行反序列化攻击

接下来就是客户端调用lookup方法向rmiregistry进行远程方法信息查询时, rmiregistry反序列化了客户端传来的数据,这样以来我们就在客户端像rmiregistry查询时来构造恶意的反序列化数据。

然后就是客户端处理rmiregistry返回的数据时,我们已知正常情况下rmiregistry回返回一个实现了Remote的Proxy对象,但是我们也可以利用rmiregistry返回一些恶意的反序列化对象给客户端,从而进行反序列化攻击。

接下来就该客户端和服务端之间的通信了,同理客户端通过rmiregistry返回的那个Proxy对象,也就是所谓的Stub和服务端进行通信,首先服务端接受到数据以后,会对客户端传来的所需要远程方法处理的参数进行反序列化,这里又是一个可以利用的点,因为我们从客户端的角度,这个只要后台不做检验,我们就可控

最后就是服务端处理完成后,将结果返回给客户端,同理,这个范围值从服务端的角度来说,也是可控的,甲乙双方可以进行互相攻击。

所以总结一下有五条攻击思路

服务端——->rmiregistry

客户端——->rmiregistry

rmiregistry——->客户端

客户端——->服务端

服务端——->客户端

客户端攻击服务端

接下来就一个一个来试验一下,这几条攻击思路。

首先客户端(远程方法调用方),对服务端(远程方法服务方)进行反序列化攻击,客户端对服务端进行反序列化的攻击关键在于传递的参数

那我们应该怎么来实现呢?我们来重新写一个远程方法的调用,(此处参考了知道创宇大佬的文章和代码Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上) ,大佬的代码地址https://github.com/longofo/rmi-jndi-ldap-jrmp-jmx-jms)

首先我们先修改一下远程方法服务方的代码,为接口中唯一的一个方法添加参数,是一个Person类型。

看一下这个Person类的具体细节

就是一个简单的pojo类,然后修改HelloImpl代码实现。

然后将接口文件放到Registry项目中,记得包路径要和在服务方的项目中的路径一样否则会爆ClassNotFoundException的错误,Registry项目中的IHello接口中的sayHello方法无需添加参数,因为rmiregistry在返回给客户端Stub时,这个Stub中只有对应的服务端的地址,端口号,以及objID等信息,并没有相关的参数信息。

Registry项目目录结构如下

12

最后客户端这边,就只需要将Person类按照和服务端一样的包路径拷贝过来,在修改下IHello里sayHell方法的参数就ok了

此时一个正常的远程方法调用环境就搭建好了,按理说这种情况下是没有什么反序列化漏洞的,但是如果说服务端的项目中存在一些已知的存在问题的类,例如Apache Common Collection。我们来模拟一下当服务端存在有存在反序列化问题的类时的情况。

这里的Weakness类只是用来模拟一个在反序列化是会进行高危操作的一个类,比起用Apache Common Collection会现显得更加直观。

同样我们客户端如果想要利用这个类来对服务端进行反序列化攻击的话,那么客户端自然也需要存在这个类。所以拷贝一份到客户端,我们之前分析源码的时候看到了,服务端会反序列化客户端传来的需要远程方法处理的参数,这就是我们的攻击点,

我们根据项目的源码可以看到,这里传递的参数类型是一个Person类型,Person这个类型本身是没有问题的,那我们要怎么实现让服务端反序列化Person类时能调用Weakness类呢?

其实很简单,我们只需要将客户端这边的Weakness类修改一下就可以了,我们让Weakness继承PerSon类就可以实现这个效果了,继承了PerSon之后我们的Weakness类就是Person类型的了,这样传递的时候Weakness类就可以被当作Person类来进行传递,表面上传递的是Person类型的参数,可实际上传递的参数确是Weakness类。

看一下客户端这边的实现

可以看成功将Weakness类作为参数进行传递,我们之前说过,服务端在处理客户端传来的远程调用信息时,是会调用UnicastServerRef.dispatch()方法的,会反序列化其中的参数

看一下调用链即可知

13

14

 

至此 我们实在jdk1.7_21的版本下以客户端的身份去成功攻击了服务端。

服务端攻击客户端

分析完了客户端对服务端的攻击,我们来看一下 服务端对客户端的攻击,根据第二章RMI流程源码分析我们看到了,服务端如果想要攻击客户端,那么利用点就存在客户端反序列话服务端的返回值的时候。这时候需要将环境稍微修改一下。

其实很简单,先修改服务端的代码,我们将IHello接口中sayHello方法需要的参数删除,然后将返回值类型由String修改成Person类型。

HelloImpl也根据接口的要求进行修改

同客户端攻击服务端时一样,只不过这次变成了服务端这边的Weakness类需要继承Person类了。

然后客户端这边就修改完毕。

然后我们来修改rmiregistry这边的代码,同样先修改IHello接口,然后我们需要将Person类拷贝到rmiregistry这边,不过一般在生产环境中,rmiregistry和服务端一般都是在同一台机器统一个项目文件里,所以服务端可以访问的类rmiregistry同样也可以。

紧接着就是就该客户端这边的代码,同理Weakness类不再继承Person

如此一来就可以实现通过服务端去攻击客户端

根据之前的分析客户端在远程方法的调用过程中会在UnicastRef.invoke方法中对服务端返回的数据进行反序列化,看一下调用链

15

如此一来服务端通过RMI攻击客户端的方式也就清晰了。

 

服务端攻击客户端 2

上一小节讲述的服务端攻击客户端的方式是通过返回值来进行操作的,这样的话利用面比较狭窄,那么有没有一种特别通用的利用方式呢?让客户端在lookup一个远程方法的时候能直接造成RCE,事实证明是有的。

这里就要讲到一个特别的类javax.naming.Reference,下面是该类的官方注释

* Reference provides a way of recording address information about
* objects which themselves are not directly bound to the naming/directory system.
*
* A Reference consists of an ordered list of addresses and class information
* about the object being referenced.
* Each address in the list identifies a communications endpoint
* for the same conceptual object. The “communications endpoint”
* is information that indicates how to contact the object. It could
* be, for example, a network address, a location in memory on the
* local machine, another process on the same machine, etc.
* The order of the addresses in the list may be of significance
* to object factories that interpret the reference.
*
* Multiple addresses may arise for
* various reasons, such as replication or the object offering interfaces
* over more than one communication mechanism. The addresses are indexed
* starting with zero.
*
* A Reference also contains information to assist in creating an instance
* of the object to which this Reference refers. It contains the class name
* of that object, and the class name and location of the factory to be used
* to create the object.
* The class factory location is a space-separated list of URLs representing
* the class path used to load the factory. When the factory class (or
* any class or resource upon which it depends) needs to be loaded,
* each URL is used (in order) to attempt to load the class.
*
* A Reference instance is not synchronized against concurrent access by multiple
* threads. Threads that need to access a single Reference concurrently should
* synchronize amongst themselves and provide the necessary locking.
*
* @author Rosanna Lee
* @author Scott Seligman
*
* @see RefAddr
* @see StringRefAddr
* @see BinaryRefAddr
* @since 1.3
*/

简单解释下该类的作用就是记录一个远程对象的位置,然后服务端将实例化好的Reference类通过bind方法注册到rmiregistry上,然后客户端通过rmiregistry返回的Stub信息找到服务端并调用该Reference对象,Reference对象通过URLClassloader将记录在Reference对象中的Class从远程地址上加载到本地,从而触发恶意类中的静态代码块,导致RCE

我们使用JDK 7u21作为环境来进行该利用方式的深入分析

首先看下服务端的代码

可以看到在实例化Reference对象的时候会传递三个参数进去,这三个参数分别是

className

包含此引用所引用的对象的类的全限定名。(ps: 就是恶意类的类名或者全限定类名,经过测试该参数不是必须,为空也行,关键在于第二个参数 也就是classFactory)

classFactory

包含用于创建此引用所引用的对象的实例的工厂类的名称。初始化为零。(ps: 第二个参数很重要 一定要写恶意类的全限定类名)

classFactoryLocation

包含工厂类的位置。初始化为零。(ps: 也就是恶意类存放的远程地址)

接下来就来跟入源码看一看

实例化Reference期间就只进行以上这些操作

实例化ReferenceWrapper的时候同样只进行了简单的赋值操作

接下来就是通过调用bind方法来将ReferenceWrapper对象注册到rmiregistry中。客户端bind Reference过程结束接下来看rmiregistry这边

这里呢因为 jdk7u21 和 jdk 8u20两个版本在调试的时候无法在RegistryImpl_Skeldispatch方法上拦截断点所以 暂时采用jdk 8u221版本来进行演示

同绑定一个正常的远程对像的差别不大只不过绑定一个正常的远程对象的时候,rmiregistry反序列化服务端传递来的结果是这样的

16

而绑定Reference的时候rmiregistry反序列化服务端传递来的结果是这样的

17

可以看到最终注册完成后,二者的区别

18

普通的远程对象是以一个Proxy对象的形式存在,Reference则是以ReferenceWrapper_Stub对象的形式存在

接下来来看客户端调用Reference这个远程对象的过程,客户端的代码演示环境为jdk 8u20

首先看下客户端的代码

其实jndi的InitialContext().lookup() 底层和rmi自己的LocateRegistry.getRegistry().lookup()一样都是调用了RegistryImpl_Stub.lookup()方法但是jndi在此基础上又做了自己的封装,例如在处理rmiregistry返回的ReferenceWrapper_stub对象时,二者的处理方式就不相同。

rmi无法处理ReferenceWrapper_stub对象,而jndi在接收了rmiregistry返回的ReferenceWrapper_stub对象后,结束当前lookup方法,在其上一层的lookup方法中也就是RegistryContext.lookup()方法里会对返回的ReferenceWrapper_stub进行处理

来观察下RegistryContext.lookup()方法的具体内容

接下来再跟进decodeObject()方法之后

NamingManager.getObjectInstance()方法就是处理Reference对像并导致RCE的关键了

根据观察NamingManager.getObjectInstance()方法的内部实现,关键代码在于这一段 factory = getObjectFactoryFromReference(ref, f);

跟进getObjectFactoryFromReference方法,

可以看到真正负责从远程地址加载恶意类的是第二次的helper.loadClass(factoryName, codebase)

该方法的具体实现如下

这就是服务端攻击客户端的另一种方式,虽然本质上还是有rmi去访问rmiregistry获取的Reference对象,但是由于JNDI对rmi进行了又一次的封装导致两者对Reference对象的处理不一样,所以客户端只有在使用JNDI提供的方法去访问rmiregistry获取的Reference对象时才会触发RCE。

这个方法看上去好像很通用,在jdk 8u121版本之前确实如此,但是在jdk 8u121版本以及之后的版本中,此方法默认情况下就不再可用了,因为从jdk 8u121版本开始 增加了对com.sun.jndi.rmi.object.trustURLCodebase的值的校验,而该值默认为false,所以默认情况下想要通过Reference对象来远程加载恶意类的想法是行不通了,

我们来看一下jdk 8u121版本究竟为了防止远程加载恶意类做了哪些改动

首先在还没有通过rmi去到rmiregistry获取Reference对象之前,在RegistryContext这个类被加载的时候就执行了以下的静态代码

可以看到这里获取了com.sun.jndi.rmi.object.trustURLCodebase默认值为false

然后当执行进decodeObject()方法,并且准备执行NamingManager.getObjectInstance()方法之前多了以下判断

就是判断了com.sun.jndi.rmi.object.trustURLCodebase的值,由于该值为false所以就会跑出异常中止执行

想要jdk 8u121版本能够正常远程加载就去要加上以下代码

这样就能又正常的RCE了

 

但是在JDK 8u191及以后的版本中客户端lookup以前加上上面的代码之后,从新执行会发现不报错了,但是仍然无法RCE,这是为什么呢,我们继续跟着源码往下看

在通过了RegistryContext中对com.sun.jndi.rmi.object.trustURLCodebase的判断并执行了NamingManager.getObjectInstance()方法之后,一路正常执行来到了关键的实例化URLClassloader并远程加载恶意类的最后一步,然后你就会发现这里变了

我们来看下这个trustURLCodebase的值究竟是怎么获取的

这次获取的是一个名称为TRUST_URL_CODEBASE_PROPERTY的属性值,也就是说我们需要将该值也设置为true才行

也就是说 在jdk 8u191及其以后的版本中如果想让 JNDI Reference rmi攻击向量成功RCE的话 目标服务器就必须在lookup之前加上以上两行代码

由此可见在jdk 8u191及其以后的版本中通过这种方式来进行RCE攻击几乎不可能实现了。

 

服务端攻击客户端 3

在上一小节中通过使用JNDI 的Reference rmi攻击向量进行RCE攻击,根据网络上大佬们提供的思路,除了使用rmi攻击向量以外还可以使用JNDI Ldap向量来进行攻击

话不多说直接上源码,首先先看下Ldap服务端源码

以上的代码呢就是在本地起了一个ldap服务监听1389端口,并向其中添加了一条可被查询的条目。单起一个ldap服务肯定是不够的,既然是ldap RCE攻击向量,那就肯定要添加一些东西让 客户端在通过JNDI查询该Ldap的条目之后转而去指定的服务器上加载恶意类。

所以需要向该条目中添加一些属性,根据知道创宇404实验室的Longofo大佬的文章

这里是向之前创建好的ldap索引中添加一些属性,客户端在向服务端查询该条索引,服务端返回查询结果,客户端根据服务端的返回结果然后去指定位置查找并加载恶意类,这就是ldap攻击向量一次RCE攻击的流程。

这里我们就要具体关注下JNDI客户端是如何在访问Ldap服务的时候被RCE的

首先客户端代码

lookup函数开始一直往下执行,执行到LdapCtx.c_lookup方法时,发送查询信息到服务端并解析服务端的返回数据

关键点在于var3 = Obj.decodeObject((Attributes)var4)这行代码解析完成后所返回的结果,如下图所示。

14

然后在DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4)这行代码中根据Reference中的信息 实例化URLClassloader去远程加载恶意类。

这种方法一直到jdk 8u191之前的版本都是可用的,但是在之后的版本中同 JNDI rmi Reference一样,添加了对com.sun.jndi.ldap.object.trustURLCodebase属性的校验,该值默认为false

 

服务端攻击rmiregistry

接下来我们就要讲通过服务端来攻击rmiregistry了,和客户端服务端互相攻击的方式比起来相对复杂那么一些,确切的说是通过伪造一个服务端的形式,因为之前说这rmiregistry通常都和真正的服务端出在同一个主机,同一个项目上,根据我们之前对RMI流程的分析,服务端在通过bind方法向rmiregistry绑定远程方法信息时,rmiregistry会反序列化服务端传来的数据,在rmiregistry方处理服务端传来的数据时会调用RegistryImpl_Skel的dispatch方法,其中会反序列化服务端传来的两个信息,一个是远程方法提供服务的注册名,另一个是封装有远程方法提供服务方信息的Proxy对象。

第一个String类型的数据反序列化我们没有利用的思路,因为String是一个final类型,没办法继承和实现,我们入手的点就只能是下面的那个 var80 = (Remote)var9.readObject();之前分析RMI流程代码时有一个点没有提到,就是bind方法在序列化一个远程对象时会将转化成一个proxy对象然后再进行序列化操作并传输给rmiregistry,序列化的proxy对像默认是实现Remot接口并封装RemoteObjectInvocationHandler的,但是如果传递的远程对象本身就是Proxy则不会进行任何转化直接传递,由MarshalOutputStream对象的replaceObject方法来实现具体操作,代码如下。

那么这样以来,似乎攻击的思路就突然清晰了,我们只需要找一个rmiregistry中可以利用的Gadget然后,ysoserial中的RMIRegistryExploit就是针对使用了版本低于JDK8u121的rmiregistry进行反序列化攻击的一个工具。

此次的测试环境是jdk1.7_21,采用CommonCollection2作为payload来进行尝试和分析。由于CommonCollection2封装的过程中用到了

所以在rmiregistry这边将commons-collections4引入

然后展示一下服务端这边最终封装完后的一个Proxy,服务端将这个Proxy序列化后 传递给rmiregistry,然后rmiregistry反序列化该数据从而出发漏洞执行命令

25

最终的调用链简化一下,如下所示

具体的反序列化过程就不做分析了

但是要注意一点就是jdk 8u121版本以后,在rmiregistry创建时不是有这么一段代码么 this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter)); 传入了RegistryImpl::registryFilter作为参数,所以在rmiregistry这边反序列化服务端传递来的Proxy对象时,是会进行对象的白名单校验的,只有以下对象才能进行反序列化

但是我们在构造恶意类的时候使用的是CommonCollection2,registryFilter在反序列化完最外面的proxy对象后第二要要反序列化的就是AnnotationInvocationHandler,而AnnotationInvocationHandler根本就不在上面的白名单里所以自然会抛出异常

这个白名单过滤机制也就是所谓的 JEP290, 就是可以通过实现ObjectInputFilter这么一个函数式接口的方式来自定义自己想要过滤的类,在使用了该机制以后,ysoserial中所有的gadget几乎都不可用了,需要想办法绕过这个白名单才行。

 

总结

在以上的讲解中,我们分析了 RMI客户端,服务端以及rmiregistry之间的关系,也对三方之间的多种攻击方式进行了详细的介绍,希望大家在看完文章后可以自己在跟随文章的步骤,手动调试一下这个过程,这样可以加深大家对RMI,JRMP,以及JNDI的理解。

参考链接

https://xz.aliyun.com/t/7079

https://xz.aliyun.com/t/7264

https://paper.seebug.org/1091/

Written by