作者:kejaly@白帽汇安全研究院
校对:r4v3zn@白帽汇安全研究院

前言

在研究高版本 JDK 反序列化漏洞的时候,往往会涉及到 JEP 290 规范。但是网上公开针对 JEP 290 规范原理研究的资料并不是很多,这就导致在研究高版本 java 反序列化的时候有些无能为力,所以最近对 JEP 290 规范好好的研究的一番,输出这篇文章,希望和大家一起交流学习。

简介

官方描述:Filter Incoming Serialization Data,即过滤传入的序列化数据。

image-20210818095109225

主要内容有:

  • Provide a flexible mechanism to narrow the classes that can be deserialized from any class available to an application down to a context-appropriate set of classes.【提供了一个灵活的机制,将可以反序列化的类从应用程序类缩小到适合上下文的类集(也就是说提供一个限制反序列化的类的机制,黑白名单方式)。】
  • Provide metrics to the filter for graph size and complexity during deserialization to validate normal graph behaviors.(限制反序列化深度和复杂度)
  • Provide a mechanism for RMI-exported objects to validate the classes expected in invocations.【为 RMI 导出的对象设置了验证机制。( 比如对于 RegistryImpl , DGCImpl 类内置了默认的白名单过滤)】
  • The filter mechanism must not require subclassing or modification to existing subclasses of ObjectInputStream.
  • Define a global filter that can be configured by properties or a configuration file.(提供一个全局过滤器,可以从属性或者配置文件中配置)

JEP 290 在 JDK 9 中加入,但在 JDK 6,7,8 一些高版本中也添加了:

Java? SE Development Kit 8, Update 121 (JDK 8u121)

Java? SE Development Kit 7, Update 131 (JDK 7u131)

Java? SE Development Kit 6, Update 141 (JDK 6u141)

官方文档:https://openjdk.java.net/jeps/290

JEP 290 核心类

JEP 290 涉及的核心类有: ObjectInputStream 类,ObjectInputFilter 接口,Config 静态类以及 Global 静态类。其中 Config 类是 ObjectInputFilter接口的内部类,Global 类又是Config类的内部类。

ObjectInputStream 类

JEP 290 进行过滤的具体实现方法是在 ObjectInputStream 类中增加了一个serialFilter属性和一个 filterChcek 函数,两者搭配来实现过滤的。

构造函数

有两个构造函数,我们需要关注的是在这两个构造函数中都会赋值 serialFilter 字段为 ObjectInputFilter.Config.getSerialFilter():

image-20210827185257711

image-20210827185310172

ObjectInputFilter.Config.getSerialFilter() 返回 ObjectInputFilter#Config 静态类中的 serialFilter静态字段

image-20210827185402971

image-20210827185418278

serialFilter 属性

image-20210818104237263

serialFilter 属性是一个 ObjectInputFilter 接口类型,这个接口声明了一个 checkInput 方法(关于 ObjectInputFilter 后面会更细致的讲解)。

image-20210818104509197

filterCheck 函数

image-20210818104147822

filterCheck 函数逻辑可以分三步。

第一步,先会判断 serialFilter 属性值是否为空,只有不为空,才会进行后续的过滤操作。

第二步,将我们需要检查的 class ,以及 arryLength等信息封装成一个FilterValues对象,

image-20210818105043290

传入到 serialFilter.checkInput 方法中,返回值为 ObjectInputFilter.Status 类型。

image-20210818105252696

最后一步,判断 status 的值,如果 statusnull 或者是 REJECTED 就会抛出异常。

ObjectInputStream 总结

到这里可以知道,serialFilter 属性就可以认为是 JEP 290 中的"过滤器"。过滤的具体逻辑写到 serialFiltercheckInput 方法中,配置过滤器其实就是设置 ObjectInputStream 对象的 serialFilter属性。并且在 ObjectInputStream 构造函数中会赋值 serialFilterObjectInputFilter#Config 静态类的 serialFilter 静态字段。

ObjectInputFilter 接口

JEP 290 中实现过滤的一个最基础的接口,想理解 JEP 290 ,必须要了解这个接口。

在低于 JDK 9 的时候的全限定名是 sun.misc.ObjectInputFIlterJDK 9 及以上是 java.io.ObjectInputFilter

另外低于 JDK 9 的时候,是 getInternalObjectInputFiltersetInternalObjectInputFilterJDK 9 以及以上是 getObjectInputFiltersetObjectInputFIlter

先来看一下 ObjectInputFilter接口的结构:

image-20210818101549350

有一个 checkInput 函数,一个静态类 Config ,一个 FilterInfo 接口,一个 Status 枚举类。

函数式接口

@FunctionalInterface 注解表明, ObjectInputFilter 是一个函数式接口。对于不了解函数式接口的同学,可以参考:https://www.runoob.com/java/java8-functional-interfaces.html 以及 https://www.jianshu.com/p/40f833bf2c48https://juejin.cn/post/6844903892166148110

在这里我们其实只需要关心函数式接口怎么赋值,函数式接口的赋值可以是: lambda 表达式或者是方法引用,当然也可以赋值一个实现了这个接口的对象。

lambda 赋值:

image-20210809172607772

使用函数引用赋值,比如 RMI 中 RegistryImpl 使用的就是函数引用赋值:

image-20210809172831645

image-20210809172815971

Config 静态类

Config 静态类是 ObjcectInputFilter 接口的一个内部静态类。

image-20210818120140858

Config#configuredFilter 静态字段

configuredFilter 是一个静态字段,所以调用 Config 类的时候就会触发 configuredFilter 字段的赋值。

image-20210818120619722

可以看到会拿到 jdk.serailFilter 属性值,如果不为空,会返回 createFilter(var0)的结果(createFilter 实际返回的是一个 Global 对象)。

jdk.serailFilter 属性值获取的方法用两种,第一种是获取 JVM 的 jdk.serialFilter 属性,第二种通过在 %JAVA_HOME%\conf\security\java.security 文件中指定 jdk.serialFilter 来设置。另外从代码中可以看到,优先选择第一种。

Config#createFilter 方法

image-20210818121136424

Config#createFilter 则会进一步调用 Global.createFilter方法,这个方法在介绍 Global 类的时候会说,其实就是将传入的 JEP 290 规则字符串解析到Global对象的 filters 字段上,并且返回这个 Global 对象。

Config 类的静态块

image-20210818121359021

Config 类的静态块,会赋值 Config.configuredFilterConfig.serialFilter 上。

Config#getSerialFilter 方法

image-20210818121459319

返回 Config#serialFilter字段值。

Config 静态类总结

Config 静态类在初始化的时候,会将Config.serialFilter 赋值为一个Global对象,这个Global 对象的filters字段值是jdk.serailFilter属性对应的 Function 列表。(关于 Global 对象介绍下面会说到,大家先有这么一个概念)

image-20210818144020117

ObjectInputStream 的构造函数中,正好取的就是 Config.serialFilter 这个静态字段 , 所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 ObjectInputStream 类全局过滤器

image-20210810115732377

比如可以通过配置 JVM 的 jdk.serialFilter 或者 %JAVA_HOME%\conf\security\java.security 文件的 jdk.serialFilter 字段值,来设置 Config.serialFilter ,也就是设置了全局过滤。

另外还有就是一些框架,在开始的时候设置也会设置 Config.serialFilter ,来设置 ObjectInputStream 类的全局过滤。 weblogic 就是,在启动的时候会设置 Config.serialFilterWebLogicObjectInputFilterWrapper 对象。

Global 静态类

Global 静态类是 Config 类中的一个内部静态类。

Global 类的一个重要特征是实现了 `ObjectInputFilter 接口,实现了其中的 checkInput 方法。所以 Global 类可以直接赋值到 ObjectInputStream.serialFilter 上。

image-20210818121707155

Global#filters 字段

是一个函数列表。

image-20210812113720175

Global#checkInput 方法

Global 类的 checkInput 会遍历 filters 去检测要反序列化的类。

image-20210818121900216

Global 中的构造函数

Global 中的构造函数会解析 JEP 290 规则。Global 中的构造函数的作用用一句话总结就是:解析 JEP 290 规则为对应的 lambda 表达式,然后添加到 Global.filters

JEP 290 的规则如下:

image-20210818122154782

Global 类的构造函数:

image-20210818122231692

image-20210818122336558

image-20210818122401230

具体就是通过 filters add 添加 lambdd 表达式到 filters 中,也就是说对 Globalfilters 赋值的是一个个 lambada 函数。

Global#createFilter 方法

传入规则字符串,来实例化一个 Global 对象。

image-20210802161719328

Global 类的总结

Global 实现了ObjectInputFilter接口,所以是可以直接赋值到 ObjectInputStream.serialFilter 上。

Global#filters 字段是一个函数列表。

Global 类中的 chekInput 方法会遍历 Global#filters 的函数,传入需要检查的 FilterValues进行检查(FilterValues 中包含了要检查的 class, arrayLength,以及 depth 等)。

过滤器

在上面总结 ObjectInputStream 类的中说过,配置过滤器其实就是设置 ObjectInputStream 类中的 serialFilter 属性。

过滤器的类型有两种,第一种是通过配置文件或者 JVM 属性来配置的全局过滤器,第二种则是来通过改变 ObjectInputStreamserialFilter 属性来配置的局部过滤器。

全局过滤器

设置全局过滤器,其实就是设置Config静态类的 serialFilter 静态字段值。

具体原因是因为在 ObjectInputStream 的两个构造函数中,都会为 serialFilter 属性赋值为 ObjectInputFilter.Config.getSerialFilter()

image-20210818105907215

image-20210818105854468

ObjectInputFilter.Config.getSerialFilter 就是直接返回 Config#serialFilter

image-20210818120808350

jdk.serailFilter

在介绍 Config 静态类的时候说到,Config 静态类初始化的时候,会解析 jdk.serailFilter 属性设置的 JEP 290 规则到一个 Global 对象的 filters 属性,并且会将这个 Global 对象赋值到 Config 静态类的 serialFilter 属性上。

image-20210818120906421

所以,这里 Config.serialFilter 值默认是解析 jdk.serailFilter 属性得到得到的 Global 对象。

weblogic 全局过滤器

在 weblogic 启动的时候,会赋值 Config.serialFilterWebLogicObjectInputFilterWrapper

具体流程如下:

首先在 weblogic 启动的时候,先调用WeblogicObjectInputFilter.initializeInternal 方法,在 initializeInternal 方法中会先 new一个 JreFilterApiProxy 对象,这个对象是一个进行有关 JEP 290 操作的代理对象(具体原理是通过反射来调用的)。

image-20210819143057398

随后 new 一个 WeblogicFilterConfig 对象。

image-20210818171333160

在创建 WeblogicFilterConfig 对象的时候中会对 weblogic 黑名单进行整合,最后得到 WeblogicFilterConfigserailFiltergolbalSerailFilter,以及 unauthenticatedSerialFilter属性如下:

image-20210810145257651

接着调用 filterConfig.getWebLogicSerialFilter取出上面赋值的WeblogicFilterConfig#serailFilter,并调用 filterApliProxy.createFilterForString 方法把filter 字符串转化为 Object 类型,并且封装到 WebLogicObjectInputFilterWrapper 对象中。

image-20210810153702790

image-20210819143711589

image-20210819144002411

最后会取出刚刚设置的 filter,传入 filterApiProxy.setGlobalFilter方法中对 ConfigserialFilter 属性赋值:

image-20210819144145085

image-20210819144320736

调用完之后我们利用 filterApiProxy.methodConfigGetSerialFilter.invoke(null) 来查看 ConfigserailFilter 字段值, 可以看到 Config.serialFilter 成功被设置为一个 WeblogicObjectInputFilterWrapper 对象。

image-20210819145158768

查看 pattern 正是打了 7 月份补丁的全局反序列化黑名单:

image-20210819145336657

用一段话来阐述 weblogic 中 全局过滤器赋值的流程就是:

weblogic 启动的时候,会调用 WeblogicObjectInputFilterinitializeInternal 方法进行初始化,首先会new JreFilterApiProxy 对象,这个对象相当于JEP 290 有关操作的代理对象,里面封装了操作 Config 静态类的方法。然后会 new 一个 WeblogicFilterConfig 对象,这个对象在 new 的时候会把 weblogic 的黑名单赋值到 WeblogicFilterConfig 对象的属性中。之后,会从WeblogicFilterConfig 对象属性中取 serialFilter ,调用 JreFilterApiProxy 对象的 setGlobalFilter 来赋值 Config.serailFilter

局部过滤器

设置局部过滤器的意思是在 new objectInputStream 对象之后,再通过改变单个 ObjectInputStream 对象的 serialFilter字段值来实现局部过滤。

改变单个 ObjectInputStream 对象的 serialFilter 字段是有两种方法:

1.通过调用 ObjectInputStream 对象的 setInternalObjectInputFilter 方法:

image-20210818181731118

注:低于 JDK 9 的时候,是 getInternalObjectInputFiltersetInternalObjectInputFilterJDK 9 以及以上是 getObjectInputFiltersetObjectInputFIlter

2.通过调用 Config.setObjectInputFilter

image-20210818183207167

局部过滤器典型的例子是 RMI 中针对 RegsitryImplDGCImpl有关的过滤。

RMI 中采用了局部过滤

RMI 简单介绍

RMI 分为客户端和服务端,官方文档:https://docs.oracle.com/javase/tutorial/rmi/overview.html

下面是对 RMI 官方文档介绍的理解:

另外 RMI 中其实并不一定要 RegistryImpl ,也就是我们熟称的注册中心,RMI 完全可以脱离注册中心来运行。可以参考:https://www.jianshu.com/p/2c78554a3f36 。个人觉得之所以使用注册中心是因为注册中心的 Registry_Stub 以及 Registry_Skel 会为我们自动进行底层的协议数据通信(JRMP 协议),能让使用者可以不关心底层的协议数据交流,而专注在远程对象的调用上。

RMI 服务端远程对象导出实际上是将这个对象分装成一个 Target 对象,然后存放在 ObjectTable#objTable 这个静态的 HashMap 中:

image-20210827195948085

每个Target对象都包含一个唯一的 id 用来表示一个对象,像 RegistryImplid就比较特殊是 0 ,其他普通对象的 id 都是随机的:

image-20210827200211131

客户端要对服务端对象进行远程调用的时候,是通过这个 id 来定位的。

ObjectTable#putTarget 方法:

image-20210827200428621

ObjectTable#getTarget 方法:

image-20210827200436621

ObjectEndpoint 中的 equals 方法,可以看到是判断 idtransporttransport 一般情况是相等的,所以一般都是通过 id 来判断:

image-20210827200447422

RegistryImpl 对象与 JEP 290

RegistryImpl 作为一个特殊的对象,导出在 RMI 服务端,客户端调用的 bind , lookuplist 等操作,实际上是操作 RegistryImplbindings 这个 Hashtable

image-20210827093525696

bind

image-20210827093630437

lookup

image-20210827093746988

list

image-20210827093853132

这里我们之所以称RegistryImpl 是一个特殊的对象,是因为 `RegistryImpl 导出过程中生成 Target 对象是一个“定制”的 Target 对象,具体体现在:

1.这个Target 中 id 的 objNum 是固定的,为 ObjID.REGISTRY_ID ,也就是 0 。
2.这个Target 中 disp 是 filter 为 RegisryImpl::RegistryFilter ,skel 为 RegsitryImpl_skel 的 UnicastServerRef 对象。
3.这个Target 中 stub 为 RegistryImpl_stub。

image-20210827110840724

对比普通对象导出过程中生成的 Target

image-20210827111018853

导出过程

首先 LocateRegistry.createRegsitry

image-20210823030959379

image-20210823031216079

new RegistryImpl(port)中会 new 一个UnicastServerRef对象,将 RegistryImplidOBJID.REGISTRY_ID,也就是 0 ) 存入到 LiveRef 对象,随后 LiveRef对象赋值到 UnicastServerRef 对象中的 ref 字段,并且将 RegsitryImpl::registryFilter 赋值给这个 UnicastServerRef 对象的 filter 字段:

image-20210823031100532

RegistryImplid 是 0 :

image-20210827105704572

随后在 RegistryImpl#setup 中调用 UnicastServerRef.exportObject 进行对象导出:

image-20210823031307758

UnicastServerRef.exportObject 中会将远程对象分装成一个 Target 对象,并且在创建这个 Target 对象的时候,将上面的 UnicastServerRef 对象赋值为 Target中的 disp。于是这个 Target 对象的 disp 就设置为了有 filterUnicastserverRef

image-20210827105122982

随后调用 LiveRef.exportObject

image-20210827104225262

会调用 TCPEndpoint.export:

image-20210827104252738

调用 TCPTransport.exportObject,在这一步会开启端口进行监听:

image-20210827104408036

随后后调用到 Transport.export,可以看到就是将这个 Target 放到 ObjectTable#objTable 中:

image-20210827104601247

image-20210827104859948

服务端处理请求过程

处理请求是在 Transport#serviceCall,首先从输入流中读取 id , 匹配到 RegistryImpl 对象对应的 Target

image-20210823035056151

随后调用 UnicastServerRef.dispatch

image-20210823035329060

UnicastServerRef#dispatch 中,由于 UnicastServerRef.skel 不为 null ,所以会调用 UnicastServerRef#oldDispatch 方法:

image-20210823035439867

oldDispatch 中会先调用 unmarshalCustomCallData(in) 方法,再调用 RegistryImpl_skel.dispatch 方法。

image-20210823035730050

unmarshalCustomCallData 方法中会进行判断,如果 UnicastServerRef.filter 不为 null ,就会设置 ConnectionInputStreamserialFilter 字段值为 UnicastServerRef.filter (设置单个 ObjectInputStreamserialFilter 属性,局部过滤的体现):

image-20210823040109705

再看 RegistryImpl_skel.dispatch

image-20210827112153950

我们以 bind 为例来讲解:

image-20210827113103382

DGCImpl 对象与 JEP 290

DGCImpl 对象和 RegistryImpl 对象类似都是一个特殊的对象,他的”定制“ Target 对象的特殊体现在:

1.这个Target 中 id 的 objNum 是固定的,为 ObjID.DGC_ID ,也就是 2 。
2.这个Target 中 disp 是 filter 为 DGCImpl::DGCFilter ,skel 为 DGCImpl_skel 的 UnicastServerRef 对象。
3.这个Target 中 stub 为 DGC_stub。
导出过程

DGCImpl 会在导出 RegsitryImpl 的时候导出,具体分析如下:

DGCImpl静态代码块中会将一个 DGCImpl 封装为一个 Target 放到 ObjectTable 中,这个 Target 有以下特征:

image-20210825150610369

DGCImpl静态代码块会在 createRegistry 的时候触发,调用链如下:

image-20210825150936069

具体原因是在导出 RegistryImpl 对象的时候,会传入 permanenttrue :

image-20210825151132318

image-20210825151146319

就会导致 new Target 中会触发 pinImpl 方法:

image-20210825151220963

image-20210825151228499

然后在调用 WeakRef.pin 方法的时候,会触发 DGCImpl 的静态代码块。

image-20210825151048159

也就是说在 createRegistry 的时候,会把 DGCImplRegistryImpl 封装的 target 都放到 ObjectTable#objTable 中。

image-20210825151704113

服务端处理请求过程

服务端处理 DGCImpl的请求过程和 RegistryImpl 非常类似,都是在Transport#serviceCall中处理,调用 UnicastServerRef#dispatch,再调用UnicastServerRef#oldDispatch 最后在 UnicastServerRef#unmarshalCustomCallData 中为之后进行readObject 操作的 ConnectionInputStream.serialFilter 赋值为 DGCImpl::checkInput

DGCImpl#checkInput

image-20210827120113771

通过 JVM 参数或者配置文件进行配置

对于 RegistryImpl

RegistryImpl 中含有一个静态字段 registryFilter ,所以在 new RegistryImpl对象的时候,会调用 initRegistryFilter 方法进行赋值:

image-20210827120702592

initRegistryFilter方法会先读取 JVM 的 sun.rmi.registry.registryFilter 的属性,或者是读取 %JAVA_HOME%\conf\security\java.security 配置文件中的 sun.rmi.registry.registryFilter 字段来得到 JEP 290 形式的 pattern ,再调用 ObjectInputFilter.Config.createFilter2 创建 filter并且返回。

image-20210827120710219

image-20210827121013640

%JAVA_HOME\conf\security\java.security% 文件:

image-20210827121307116

RegistryImpl#registryFilter函数会先判断 RegistryImpl#regstiryFilter 字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED 就直接返回检查的结果,否则再使用默认的白名单检查。

image-20210827120629723

对于 DGCImpl

DGCImpl 中含有一个静态字段 dgcFilter ,所以在 new DGCImpl对象的时候,会调用 initDgcFilter 方法进行赋值:

image-20210827123958702

image-20210827124038685

initDgcFilter方法会先读取 JVM 的 sun.rmi.transport.dgcFilter 的属性,或者是读取 %JAVA_HOME\conf\security\java.security% 配置文件中的 sun.rmi.transport.dgcFilter 字段来得到 JEP 290 形式的 pattern ,再调用 ObjectInputFilter.Config.createFilter 创建 filter并且返回。

%JAVA_HOME%\conf\security\java.security 文件:

image-20210827124354081

DGCImpl#checkInputRegistryImpl#registryFilter函数类似,会先判断 DGCImpl#dgcFilter 字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED 就直接返回检查的结果,否则再使用默认的白名单检查。

RMI 中 JEP 290 的绕过

网上公开资料广泛说的是:如果服务端"绑定"了一个对象,他的方法参数类型是Object 类型的方法时,则可以绕过 JEP 290。

其实剖析本质,是因为服务端导出的这个 ”普通的对象“ 对应的 Target 对象中的 disp (其实是 UnicastServerRef 对象) 的 filternull

image-20210827153542315

普通的对象导出的 target 如下:

image-20210827111018853

下面我们来具体跟以下流程分析,首先准备客户端和服务端代码如下:

服务端和客户端共同包含接口的定义和实现:

image-20210827162757132

image-20210827162853386

服务端代码如下:

image-20210827162918895

恶意客户端代码如下:

image-20210827162644801

普通对象的导出过程

普通对象的导出有两种方式,一种是继承 UnicastRemoteObject 对象,会在 new 这个对象的时候自动导出。第二种是如果没有继承 UnicastRemoteObject 对象,则需要调用UnicastRemoteObject.export进行手动导出。但其实第一种底层也是利用 UnicastRemoteObject.export 来导出对象的。

下面我们来讨论继承 UnicastRemoteObject 类的情况:

image-20210827154244742

image-20210827154647314

因为这个普通对象继承自 UnicastRemoteObject类,所以在 new 这个普通对象的时候会调用到 UnicastRemoteObject 的构造方法:

image-20210823005130988

进而调用 UnicastRemoteObject.exportObject 方法:

image-20210827155229168

image-20210827155144661

UnicastRemoteObject#exportObject 方法中再使用 UnicastServerRef#exportObject ,这里可以看到在 new UnicastRemoteObject 的时候并没有传入 filter

image-20210823005152153

对比导出 RegistryImpl 对象的时候, new UnicastRemoteObject 对象传入了 RegistryImpl::registryFilter

image-20210823031100532

接着会调用 UnicastServerRef.exportObject 方法:

image-20210823005405191

所以普通对象生成的 Target 对象的 disp 中 filter 就为 null ,另外这里的 skel 也为 null 。

image-20210823005742261

后面导出 Target 的过程和 导出RegistryImpl对应的 Target是一样的,最后会将这个普通对象的 Target 放到 objectTable#objTable中。

绑定成功后的 ObjectTable#objTable:

image-20210827163249786

服务端处理请求的过程

同样处理请求的入口在 Transport#serviceCall,首先从输入流中读取 id , 匹配到 RegistryImpl 对象对应的 Target

image-20210827162445281

然后取出 disp ,调用 disp.dispatch

image-20210827163553250

首先由于 skelnull ,所以不会进入 oldDispatch , 像 RegistryImplDGCImpl 因为他们的 skel 不为 null ,所以会进入到 oldDispatch

image-20210827163735583

接着会匹配到方法,拿到方法的参数,接着进行反序列化:

image-20210827164817344

unmarshalCustomCallData 方法:

image-20210827164857411

unmarshalValue 方法对输入流中传入的参数进行反序列化:

image-20210827165045896

执行 in.readObject 之后,成功弹出计算器:

image-20210827165135739

反制

利用上面这种方法绕过 JEP 290 去攻击 RMI 服务端,网上有一些工具,比如 rmitast 和 rmisout 。

但是对于使用 rmitast 或者 rmisout 这些工具,或者调用 lookup() 来试图攻击RMI 服务端 的时候,我们可以使用 如下的恶意服务端代码进行反制:

image-20210827165940844

反制 RegistryImpl_Stub.lookup

我们来看一下RegistryImpl_Stub.lookup对服务端返回的结果是怎么处理的,可以看见在 RegistryImpl_Stub.lookup 会直接对服务端返回的对象调用 in.readObject 方法,而 inserialFilter在这里是为 null 的:

image-20210827171556540

所以客户端在进行 RegistryStub.lookup 操作的时候会直接导致 RCE :

image-20210827173057156

同理 RegistryStub.list 也是如此:

image-20210827172117128

但是用上面的服务端恶意代码并不能触发 RCE ,因为上面服务端恶意代码是利用 Registry_skel 来写入对象的,可以看到写入的是一个字符串数组:

image-20210827172909054

反制 rmitast

我们以 rmitast 中的 枚举模块为例:

image-20210827173427559

步入 enumerate.enumerate() 里面是具体的实现原理:

image-20210827173510006

首先 Enumerate.connect(this.registry) 返回的实际上是 RegistryImpl_Stub 对象,底层调用的是 LocateRegistry.getRegistry 方法。

然后调用 this.registry.loadObjects(), this.list() 实际调用的是 RegistyImpl_Stub.list() 方法,得到注册中心的所有绑定的对象名:

image-20210827173748613

接着会调用 this.loadObjects(names), 会调用 this.lookup(name) ,底层实际使用的是 RegistryImpl_Stub.lookup() 方法,上面分析过 RegistryImpl_Stub.lookup 会直接反序列化服务端传过来的恶意对象,并且 readObject时使用的ObjectInputStream 对象中的 serialFilternull

image-20210827173943082

我们启动上面的恶意服务端,然后使用 RmiTaste 的 enum 模块:

image-20210827174102324

运行之后会导致使用 RmiTast 的一端 RCE :

image-20210827174304598

总结

JEP 290 主要是在 ObjectInputStream 类中增加了一个serialFilter属性和一个 filterChcek 函数,其中 serialFilter就可以理解为过滤器。

ObjectInputStream 对象进行 readObject 的时候,内部会调用 filterChcek 方法进行检查,filterCheck方法中会对 `serialFilter属性进行判断,如果不是 null ,就会调用 serialFilter.checkInput 方法进行过滤。

设置过滤器本质就是设置 ObjectInputStreamserialFilter 字段值,设置过滤器可以分为设置全局过滤器和设置局部过滤器:

1.设置全局过滤器是指,通过修改 Config.serialFilter这个静态字段的值来达到设置所有 ObjectInputStream对象的 serialFilter值 。具体原因是因为 ObjectInputStream 的构造函数会读取Config.serialFilter的值赋值到自己的serialFilter字段上,所有就会导致所有 new 出来的 ObjectInputStream对象的 serailFilter 都为Config.serialFilter的值。

2.设置局部过滤器是指,在 new ObjectInputStream 的之后,再修改单个 ObjectInputStream 对象的 serialFilter 字段值。

参考

https://www.cnpanda.net/sec/968.html JEP290的基本概念

https://www.jianshu.com/p/2c78554a3f36 深入理解rmi原理

http://openjdk.java.net/jeps/290 JEP 290 官方文档

https://www.cnblogs.com/Ming-Yi/p/13832639.html Java序列化过滤器

https://www.runoob.com/java/java8-functional-interfaces.html Java 8 函数式接口

https://www.jianshu.com/p/40f833bf2c48 函数式接口和Lambda表达式深入理解

https://juejin.cn/post/6844903892166148110 「Java8系列」神奇的函数式接口


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1689/