作者:kejaly@白帽汇安全研究院
校对:r4v3zn@白帽汇安全研究院
在研究高版本 JDK 反序列化漏洞的时候,往往会涉及到 JEP 290 规范。但是网上公开针对 JEP 290 规范原理研究的资料并不是很多,这就导致在研究高版本 java 反序列化的时候有些无能为力,所以最近对 JEP 290 规范好好的研究的一番,输出这篇文章,希望和大家一起交流学习。
官方描述:Filter Incoming Serialization Data
,即过滤传入的序列化数据。
主要内容有:
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 涉及的核心类有: ObjectInputStream
类,ObjectInputFilter
接口,Config
静态类以及 Global
静态类。其中 Config
类是 ObjectInputFilter
接口的内部类,Global
类又是Config
类的内部类。
JEP 290 进行过滤的具体实现方法是在 ObjectInputStream
类中增加了一个serialFilter
属性和一个 filterChcek
函数,两者搭配来实现过滤的。
有两个构造函数,我们需要关注的是在这两个构造函数中都会赋值 serialFilter
字段为 ObjectInputFilter.Config.getSerialFilter()
:
ObjectInputFilter.Config.getSerialFilter()
返回 ObjectInputFilter#Config
静态类中的 serialFilter
静态字段
serialFilter
属性是一个 ObjectInputFilter
接口类型,这个接口声明了一个 checkInput
方法(关于 ObjectInputFilter
后面会更细致的讲解)。
filterCheck
函数逻辑可以分三步。
第一步,先会判断 serialFilter
属性值是否为空,只有不为空,才会进行后续的过滤操作。
第二步,将我们需要检查的 class
,以及 arryLength
等信息封装成一个FilterValues
对象,
传入到 serialFilter.checkInput
方法中,返回值为 ObjectInputFilter.Status
类型。
最后一步,判断 status
的值,如果 status
是 null
或者是 REJECTED
就会抛出异常。
到这里可以知道,serialFilter
属性就可以认为是 JEP 290 中的"过滤器"。过滤的具体逻辑写到 serialFilter
的checkInput
方法中,配置过滤器其实就是设置 ObjectInputStream
对象的 serialFilter
属性。并且在 ObjectInputStream
构造函数中会赋值 serialFilter
为 ObjectInputFilter#Config
静态类的 serialFilter
静态字段。
是 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
静态类是 ObjcectInputFilter
接口的一个内部静态类。
configuredFilter
是一个静态字段,所以调用 Config
类的时候就会触发 configuredFilter
字段的赋值。
可以看到会拿到 jdk.serailFilter
属性值,如果不为空,会返回 createFilter(var0)
的结果(createFilter
实际返回的是一个 Global
对象)。
jdk.serailFilter
属性值获取的方法用两种,第一种是获取 JVM 的 jdk.serialFilter
属性,第二种通过在 %JAVA_HOME%\conf\security\java.security
文件中指定 jdk.serialFilter
来设置。另外从代码中可以看到,优先选择第一种。
Config#createFilter
则会进一步调用 Global.createFilter
方法,这个方法在介绍 Global
类的时候会说,其实就是将传入的 JEP 290 规则字符串解析到Global
对象的 filters
字段上,并且返回这个 Global
对象。
Config 类的静态块,会赋值 Config.configuredFilter
到 Config.serialFilter
上。
返回 Config#serialFilter
字段值。
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
静态类是 Config 类中的一个内部静态类。
Global
类的一个重要特征是实现了 `ObjectInputFilter
接口,实现了其中的 checkInput
方法。所以 Global
类可以直接赋值到 ObjectInputStream.serialFilter
上。
是一个函数列表。
Global
类的 checkInput
会遍历 filters
去检测要反序列化的类。
Global
中的构造函数会解析 JEP 290 规则。Global
中的构造函数的作用用一句话总结就是:解析 JEP 290 规则为对应的 lambda
表达式,然后添加到 Global.filters
。
JEP 290 的规则如下:
Global
类的构造函数:
具体就是通过 filters
add
添加 lambdd
表达式到 filters 中,也就是说对 Global
的 filters
赋值的是一个个 lambada
函数。
传入规则字符串,来实例化一个 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
:
在介绍 Config
静态类的时候说到,Config
静态类初始化的时候,会解析 jdk.serailFilter
属性设置的 JEP 290 规则到一个 Global
对象的 filters
属性,并且会将这个 Global
对象赋值到 Config
静态类的 serialFilter
属性上。
所以,这里 Config.serialFilter
值默认是解析 jdk.serailFilter
属性得到得到的 Global
对象。
在 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 分为客户端和服务端,官方文档: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
作为一个特殊的对象,导出在 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
对象和 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
:
在 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
中含有一个静态字段 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
就直接返回检查的结果,否则再使用默认的白名单检查。
网上公开资料广泛说的是:如果服务端"绑定"了一个对象,他的方法参数类型是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
会直接对服务端返回的对象调用 in.readObject
方法,而 in
的 serialFilter
在这里是为 null
的:
所以客户端在进行 RegistryStub.lookup
操作的时候会直接导致 RCE :
同理 RegistryStub.list
也是如此:
但是用上面的服务端恶意代码并不能触发 RCE ,因为上面服务端恶意代码是利用 Registry_skel
来写入对象的,可以看到写入的是一个字符串数组:
我们以 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
字段值。
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系列」神奇的函数式接口
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1689/