漫谈 JEP 290
![]()
作者:白帽汇安全研究院@kejaly
校对:白帽汇安全研究院@r4v3zn
前言
在研究高版本 JDK 反序列化漏洞的时候,往往会涉及到 JEP 290 规范。但是网上公开针对 JEP 290 规范原理研究的资料并不是很多,这就导致在研究高版本 java 反序列化的时候有些无能为力,所以最近对 JEP 290 规范好好的研究的一番,输出这篇文章,希望和大家一起交流学习。
简介
官方描述:Filter Incoming Serialization Data,即过滤传入的序列化数据。
![]()
主要内容有:
- 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():
![]()
![]()
objectInputFilter.Config.getSerialFilter() 返回 objectInputFilter#Config 静态类中的 serialFilter静态字段
![]()
![]()
serialFilter 属性
![]()
serialFilter 属性是一个 objectInputFilter 接口类型,这个接口声明了一个 checkInput 方法(关于 objectInputFilter 后面会更细致的讲解)。
![]()
filterCheck 函数
![]()
filterCheck 函数逻辑可以分三步。
第一步,先会判断 serialFilter 属性值是否为空,只有不为空,才会进行后续的过滤操作。
第二步,将我们需要检查的 class ,以及 arryLength等信息封装成一个FilterValues对象,
![]()
传入到 serialFilter.checkInput 方法中,返回值为 objectInputFilter.Status 类型。
![]()
最后一步,判断 status 的值,如果 status 是 null 或者是 REJECTED 就会抛出异常。
objectInputStream 总结
到这里可以知道,serialFilter 属性就可以认为是 JEP 290 中的"过滤器"。过滤的具体逻辑写到 serialFilter 的checkInput 方法中,配置过滤器其实就是设置 objectInputStream 对象的 serialFilter属性。并且在 objectInputStream 构造函数中会赋值 serialFilter 为 objectInputFilter#Config 静态类的 serialFilter 静态字段。
objectInputFilter 接口
是 JEP 290 中实现过滤的一个最基础的接口,想理解 JEP 290 ,必须要了解这个接口。
在低于 JDK 9 的时候的全限定名是 sun.misc.objectInputFilter,JDK 9 及以上是 java.io.objectInputFilter 。
另外低于 JDK 9 的时候,是 getInternalobjectInputFilter 和 setInternalobjectInputFilter,JDK 9 以及以上是 getobjectInputFilter 和 setobjectInputFilter 。
先来看一下 objectInputFilter接口的结构:
![]()
有一个 checkInput 函数,一个静态类 Config ,一个 FilterInfo 接口,一个 Status 枚举类。
函数式接口
@FunctionalInterface 注解表明, objectInputFilter 是一个函数式接口。对于不了解函数式接口的同学,可以参考:https://www.runoob.com/java/java8-functional-interfaces.html 以及 https://www.jianshu.com/p/40f833bf2c48 , https://juejin.cn/post/6844903892166148110 。
在这里我们其实只需要关心函数式接口怎么赋值,函数式接口的赋值可以是: lambda 表达式或者是方法引用,当然也可以赋值一个实现了这个接口的对象。
lambda 赋值:
![]()
使用函数引用赋值,比如 RMI 中 RegistryImpl 使用的就是函数引用赋值:
![]()
![]()
Config 静态类
Config 静态类是 ObjcectInputFilter 接口的一个内部静态类。
![]()
Config#configuredFilter 静态字段
configuredFilter 是一个静态字段,所以调用 Config 类的时候就会触发 configuredFilter 字段的赋值。
![]()
可以看到会拿到 jdk.serailFilter 属性值,如果不为空,会返回 createFilter(var0)的结果(createFilter 实际返回的是一个 Global 对象)。
jdk.serailFilter 属性值获取的方法用两种,第一种是获取 JVM 的 jdk.serialFilter 属性,第二种通过在 %JAVA_HOME%\conf\security\java.security 文件中指定 jdk.serialFilter 来设置。另外从代码中可以看到,优先选择第一种。
Config#createFilter 方法
![]()
Config#createFilter 则会进一步调用 Global.createFilter方法,这个方法在介绍 Global 类的时候会说,其实就是将传入的 JEP 290 规则字符串解析到Global对象的 filters 字段上,并且返回这个 Global 对象。
Config 类的静态块
![]()
Config 类的静态块,会赋值 Config.configuredFilter 到 Config.serialFilter 上。
Config#getSerialFilter 方法
![]()
返回 Config#serialFilter字段值。
Config 静态类总结
Config 静态类在初始化的时候,会将Config.serialFilter 赋值为一个Global对象,这个Global 对象的filters字段值是jdk.serailFilter属性对应的 Function 列表。(关于 Global 对象介绍下面会说到,大家先有这么一个概念)
![]()
而 objectInputStream 的构造函数中,正好取的就是 Config.serialFilter 这个静态字段 , 所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 objectInputStream 类全局过滤器。
![]()
比如可以通过配置 JVM 的 jdk.serialFilter 或者 %JAVA_HOME%\conf\security\java.security 文件的 jdk.serialFilter 字段值,来设置 Config.serialFilter ,也就是设置了全局过滤。
另外还有就是一些框架,在开始的时候设置也会设置 Config.serialFilter ,来设置 objectInputStream 类的全局过滤。 weblogic 就是,在启动的时候会设置 Config.serialFilter 为 WebLogicobjectInputFilterWrapper 对象。
Global 静态类
Global 静态类是 Config 类中的一个内部静态类。
Global 类的一个重要特征是实现了 `objectInputFilter 接口,实现了其中的 checkInput 方法。所以 Global 类可以直接赋值到 objectInputStream.serialFilter 上。
![]()
Global#filters 字段
是一个函数列表。
![]()
Global#checkInput 方法
Global 类的 checkInput 会遍历 filters 去检测要反序列化的类。
![]()
Global 中的构造函数
Global 中的构造函数会解析 JEP 290 规则。Global 中的构造函数的作用用一句话总结就是:解析 JEP 290 规则为对应的 lambda 表达式,然后添加到 Global.filters 。
JEP 290 的规则如下:
![]()
Global 类的构造函数:
![]()
![]()
![]()
具体就是通过 filters add 添加 lambdd 表达式到 filters 中,也就是说对 Global 的 filters 赋值的是一个个 lambada 函数。
Global#createFilter 方法
传入规则字符串,来实例化一个 Global 对象。
![]()
Global 类的总结
Global 实现了objectInputFilter接口,所以是可以直接赋值到 objectInputStream.serialFilter 上。
Global#filters 字段是一个函数列表。
Global 类中的 chekInput 方法会遍历 Global#filters 的函数,传入需要检查的 FilterValues进行检查(FilterValues 中包含了要检查的 class, arrayLength,以及 depth 等)。
过滤器
在上面总结 objectInputStream 类的中说过,配置过滤器其实就是设置 objectInputStream 类中的 serialFilter 属性。
过滤器的类型有两种,第一种是通过配置文件或者 JVM 属性来配置的全局过滤器,第二种则是来通过改变 objectInputStream 的 serialFilter 属性来配置的局部过滤器。
全局过滤器
设置全局过滤器,其实就是设置Config静态类的 serialFilter 静态字段值。
具体原因是因为在 objectInputStream 的两个构造函数中,都会为 serialFilter 属性赋值为 objectInputFilter.Config.getSerialFilter() 。
![]()
![]()
而 objectInputFilter.Config.getSerialFilter 就是直接返回 Config#serialFilter:
![]()
jdk.serailFilter
在介绍 Config 静态类的时候说到,Config 静态类初始化的时候,会解析 jdk.serailFilter 属性设置的 JEP 290 规则到一个 Global 对象的 filters 属性,并且会将这个 Global 对象赋值到 Config 静态类的 serialFilter 属性上。
![]()
所以,这里 Config.serialFilter 值默认是解析 jdk.serailFilter 属性得到得到的 Global 对象。
weblogic 全局过滤器
在 weblogic 启动的时候,会赋值 Config.serialFilter 为 WebLogicobjectInputFilterWrapper 。
具体流程如下:
首先在 weblogic 启动的时候,先调用WeblogicobjectInputFilter.initializeInternal 方法,在 initializeInternal 方法中会先 new一个 JreFilterApiProxy 对象,这个对象是一个进行有关 JEP 290 操作的代理对象(具体原理是通过反射来调用的)。
![]()
随后 new 一个 WeblogicFilterConfig 对象。
![]()
在创建 WeblogicFilterConfig 对象的时候中会对 weblogic 黑名单进行整合,最后得到 WeblogicFilterConfig中 serailFilter,golbalSerailFilter,以及 unauthenticatedSerialFilter属性如下:
![]()
接着调用 filterConfig.getWebLogicSerialFilter取出上面赋值的WeblogicFilterConfig#serailFilter,并调用 filterApliProxy.createFilterForString 方法把filter 字符串转化为 object 类型,并且封装到 WebLogicobjectInputFilterWrapper 对象中。
![]()
![]()
![]()
最后会取出刚刚设置的 filter,传入 filterApiProxy.setGlobalFilter方法中对 Config 的 serialFilter 属性赋值:
![]()
![]()
调用完之后我们利用 filterApiProxy.methodConfigGetSerialFilter.invoke(null) 来查看 Config 的 serailFilter 字段值, 可以看到 Config.serialFilter 成功被设置为一个 WeblogicobjectInputFilterWrapper 对象。
![]()
查看 pattern 正是打了 7 月份补丁的全局反序列化黑名单:
![]()
用一段话来阐述 weblogic 中 全局过滤器赋值的流程就是:
weblogic 启动的时候,会调用 WeblogicobjectInputFilter 的 initializeInternal 方法进行初始化,首先会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 方法:
![]()
注:低于 JDK 9 的时候,是 getInternalobjectInputFilter 和 setInternalobjectInputFilter,JDK 9 以及以上是 getobjectInputFilter 和 setobjectInputFilter 。
2.通过调用 Config.setobjectInputFilter :
![]()
局部过滤器典型的例子是 RMI 中针对 RegsitryImpl 和 DGCImpl有关的过滤。
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 中:
![]()
每个Target对象都包含一个唯一的 id 用来表示一个对象,像 RegistryImpl 的 id就比较特殊是 0 ,其他普通对象的 id 都是随机的:
![]()
客户端要对服务端对象进行远程调用的时候,是通过这个 id 来定位的。
objectTable#putTarget 方法:
![]()
objectTable#getTarget 方法:
![]()
objectEndpoint 中的 equals 方法,可以看到是判断 id 和 transport , transport 一般情况是相等的,所以一般都是通过 id 来判断:
![]()
RegistryImpl 对象与 JEP 290
RegistryImpl 作为一个特殊的对象,导出在 RMI 服务端,客户端调用的 bind , lookup,list 等操作,实际上是操作 RegistryImpl 的 bindings 这个 Hashtable。
![]()
bind:
![]()
lookup:
![]()
list:
![]()
这里我们之所以称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。
![]()
对比普通对象导出过程中生成的 Target :
![]()
导出过程
首先 LocateRegistry.createRegsitry:
![]()
![]()
new RegistryImpl(port)中会 new 一个UnicastServerRef对象,将 RegistryImpl 的 id(OBJID.REGISTRY_ID,也就是 0 ) 存入到 LiveRef 对象,随后 LiveRef对象赋值到 UnicastServerRef 对象中的 ref 字段,并且将 RegsitryImpl::registryFilter 赋值给这个 UnicastServerRef 对象的 filter 字段:
![]()
RegistryImpl 的 id 是 0 :
![]()
随后在 RegistryImpl#setup 中调用 UnicastServerRef.exportobject 进行对象导出:
![]()
UnicastServerRef.exportobject 中会将远程对象分装成一个 Target 对象,并且在创建这个 Target 对象的时候,将上面的 UnicastServerRef 对象赋值为 Target中的 disp。于是这个 Target 对象的 disp 就设置为了有 filter 的 UnicastserverRef。
![]()
随后调用 LiveRef.exportobject :
![]()
会调用 TCPEndpoint.export:
![]()
调用 TCPTransport.exportobject,在这一步会开启端口进行监听:
![]()
随后后调用到 Transport.export,可以看到就是将这个 Target 放到 objectTable#objTable 中:
![]()
![]()
服务端处理请求过程
处理请求是在 Transport#serviceCall,首先从输入流中读取 id , 匹配到 RegistryImpl 对象对应的 Target 。
![]()
随后调用 UnicastServerRef.dispatch:
![]()
在 UnicastServerRef#dispatch 中,由于 UnicastServerRef.skel 不为 null ,所以会调用 UnicastServerRef#oldDispatch 方法:
![]()
oldDispatch 中会先调用 unmarshalCustomCallData(in) 方法,再调用 RegistryImpl_skel.dispatch 方法。
![]()
unmarshalCustomCallData 方法中会进行判断,如果 UnicastServerRef.filter 不为 null ,就会设置 ConnectionInputStream的 serialFilter 字段值为 UnicastServerRef.filter (设置单个 objectInputStream 的 serialFilter 属性,局部过滤的体现):
![]()
再看 RegistryImpl_skel.dispatch :
![]()
我们以 bind 为例来讲解:
![]()
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 有以下特征:
![]()
DGCImpl静态代码块会在 createRegistry 的时候触发,调用链如下:
![]()
具体原因是在导出 RegistryImpl 对象的时候,会传入 permanent 为 true :
![]()
![]()
就会导致 new Target 中会触发 pinImpl 方法:
![]()
![]()
然后在调用 WeakRef.pin 方法的时候,会触发 DGCImpl 的静态代码块。
![]()
也就是说在 createRegistry 的时候,会把 DGCImpl 和 RegistryImpl 封装的 target 都放到 objectTable#objTable 中。
![]()
服务端处理请求过程
服务端处理 DGCImpl的请求过程和 RegistryImpl 非常类似,都是在Transport#serviceCall中处理,调用 UnicastServerRef#dispatch,再调用UnicastServerRef#oldDispatch 最后在 UnicastServerRef#unmarshalCustomCallData 中为之后进行readobject 操作的 ConnectionInputStream.serialFilter 赋值为 DGCImpl::checkInput。
DGCImpl#checkInput:
![]()
通过 JVM 参数或者配置文件进行配置
对于 RegistryImpl
在 RegistryImpl 中含有一个静态字段 registryFilter ,所以在 new RegistryImpl对象的时候,会调用 initRegistryFilter 方法进行赋值:
![]()
initRegistryFilter方法会先读取 JVM 的 sun.rmi.registry.registryFilter 的属性,或者是读取 %JAVA_HOME%\conf\security\java.security 配置文件中的 sun.rmi.registry.registryFilter 字段来得到 JEP 290 形式的 pattern ,再调用 objectInputFilter.Config.createFilter2 创建 filter并且返回。
![]()
![]()
%JAVA_HOME\conf\security\java.security% 文件:
![]()
RegistryImpl#registryFilter函数会先判断 RegistryImpl#regstiryFilter 字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED 就直接返回检查的结果,否则再使用默认的白名单检查。
![]()
对于 DGCImpl
在 DGCImpl 中含有一个静态字段 dgcFilter ,所以在 new DGCImpl对象的时候,会调用 initDgcFilter 方法进行赋值:
![]()
![]()
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 文件:
![]()
DGCImpl#checkInput和 RegistryImpl#registryFilter函数类似,会先判断 DGCImpl#dgcFilter 字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED 就直接返回检查的结果,否则再使用默认的白名单检查。
![]()
RMI 中 JEP 290 的绕过
网上公开资料广泛说的是:如果服务端"绑定"了一个对象,他的方法参数类型是object 类型的方法时,则可以绕过 JEP 290。
其实剖析本质,是因为服务端导出的这个 ”普通的对象“ 对应的 Target 对象中的 disp (其实是 UnicastServerRef 对象) 的 filter 是 null 。
![]()
普通的对象导出的 target 如下:
![]()
下面我们来具体跟以下流程分析,首先准备客户端和服务端代码如下:
服务端和客户端共同包含接口的定义和实现:
![]()
![]()
服务端代码如下:
![]()
恶意客户端代码如下:
![]()
普通对象的导出过程
普通对象的导出有两种方式,一种是继承 UnicastRemoteobject 对象,会在 new 这个对象的时候自动导出。第二种是如果没有继承 UnicastRemoteobject 对象,则需要调用UnicastRemoteobject.export进行手动导出。但其实第一种底层也是利用 UnicastRemoteobject.export 来导出对象的。
下面我们来讨论继承 UnicastRemoteobject 类的情况:
![]()
![]()
因为这个普通对象继承自 UnicastRemoteobject类,所以在 new 这个普通对象的时候会调用到 UnicastRemoteobject 的构造方法:
![]()
进而调用 UnicastRemoteobject.exportobject 方法:
![]()
![]()
UnicastRemoteobject#exportobject 方法中再使用 UnicastServerRef#exportobject ,这里可以看到在 new UnicastRemoteobject 的时候并没有传入 filter :
![]()
对比导出 RegistryImpl 对象的时候, new UnicastRemoteobject 对象传入了 RegistryImpl::registryFilter:
![]()
接着会调用 UnicastServerRef.exportobject 方法:
![]()
所以普通对象生成的 Target 对象的 disp 中 filter 就为 null ,另外这里的 skel 也为 null 。
![]()
后面导出 Target 的过程和 导出RegistryImpl对应的 Target是一样的,最后会将这个普通对象的 Target 放到 objectTable#objTable中。
绑定成功后的 objectTable#objTable:
![]()
服务端处理请求的过程
同样处理请求的入口在 Transport#serviceCall,首先从输入流中读取 id , 匹配到 RegistryImpl 对象对应的 Target 。
![]()
然后取出 disp ,调用 disp.dispatch :
![]()
首先由于 skel 为 null ,所以不会进入 oldDispatch , 像 RegistryImpl 和 DGCImpl 因为他们的 skel 不为 null ,所以会进入到 oldDispatch:
![]()
接着会匹配到方法,拿到方法的参数,接着进行反序列化:
![]()
unmarshalCustomCallData 方法:
![]()
unmarshalValue 方法对输入流中传入的参数进行反序列化:
![]()
执行 in.readobject 之后,成功弹出计算器:
![]()
反制
利用上面这种方法绕过 JEP 290 去攻击 RMI 服务端,网上有一些工具,比如 rmitast 和 rmisout 。
但是对于使用 rmitast 或者 rmisout 这些工具,或者调用 lookup() 来试图攻击RMI 服务端 的时候,我们可以使用 如下的恶意服务端代码进行反制:
![]()
反制 RegistryImpl_Stub.lookup
我们来看一下RegistryImpl_Stub.lookup对服务端返回的结果是怎么处理的,可以看见在 RegistryImpl_Stub.lookup 会直接对服务端返回的对象调用 in.readobject 方法,而 in 的 serialFilter在这里是为 null 的:
![]()
所以客户端在进行 RegistryStub.lookup 操作的时候会直接导致 RCE :
![]()
同理 RegistryStub.list 也是如此:
![]()
但是用上面的服务端恶意代码并不能触发 RCE ,因为上面服务端恶意代码是利用 Registry_skel 来写入对象的,可以看到写入的是一个字符串数组:
![]()
反制 rmitast
我们以 rmitast 中的 枚举模块为例:
![]()
步入 enumerate.enumerate() 里面是具体的实现原理:
![]()
首先 Enumerate.connect(this.registry) 返回的实际上是 RegistryImpl_Stub 对象,底层调用的是 LocateRegistry.getRegistry 方法。
然后调用 this.registry.loadobjects(), this.list() 实际调用的是 RegistyImpl_Stub.list() 方法,得到注册中心的所有绑定的对象名:
![]()
接着会调用 this.loadobjects(names), 会调用 this.lookup(name) ,底层实际使用的是 RegistryImpl_Stub.lookup() 方法,上面分析过 RegistryImpl_Stub.lookup 会直接反序列化服务端传过来的恶意对象,并且 readobject时使用的objectInputStream 对象中的 serialFilter 是 null。
![]()
我们启动上面的恶意服务端,然后使用 RmiTaste 的 enum 模块:
![]()
运行之后会导致使用 RmiTast 的一端 RCE :
![]()
总结
JEP 290 主要是在 objectInputStream 类中增加了一个serialFilter属性和一个 filterChcek 函数,其中 serialFilter就可以理解为过滤器。
在 objectInputStream 对象进行 readobject 的时候,内部会调用 filterChcek 方法进行检查,filterCheck方法中会对 `serialFilter属性进行判断,如果不是 null ,就会调用 serialFilter.checkInput 方法进行过滤。
设置过滤器本质就是设置 objectInputStream 的 serialFilter 字段值,设置过滤器可以分为设置全局过滤器和设置局部过滤器:
1.设置全局过滤器是指,通过修改 Config.serialFilter这个静态字段的值来达到设置所有 objectInputStream对象的 serialFilter值 。具体原因是因为 objectInputStream 的构造函数会读取Config.serialFilter的值赋值到自己的serialFilter字段上,所有就会导致所有 new 出来的 objectInputStream对象的 serailFilter 都为Config.serialFilter的值。
2.设置局部过滤器是指,在 new objectInputStream 的之后,再修改单个 objectInputStream 对象的 serialFilter 字段值。
参考
[1] https://www.cnpanda.net/sec/968.html JEP290的基本概念
[2] https://www.jianshu.com/p/2c78554a3f36 深入理解rmi原理
[3] http://openjdk.java.net/jeps/290 JEP 290 官方文档
[4 https://www.cnblogs.com/Ming-Yi/p/13832639.html Java序列化过滤器
[5] https://www.runoob.com/java/java8-functional-interfaces.html Java 8 函数式接口
[6] https://www.jianshu.com/p/40f833bf2c48 函数式接口和Lambda表达式深入理解
[7] https://juejin.cn/post/6844903892166148110 「Java8系列」神奇的函数式接口

keeeee 1530天前
最新评论