Java RMI 攻击分析(二)ByPass JEP290
什么是JEP290
JEP290 是 Java
底层为了缓解反序列化攻击提出的一种解决方案。这是一个针对 JAVA 9
提出的安全特性,但同时对 JDK 6,7,8
都进行了支持,在 JDK 6u141
、JDK 7u131
、JDK 8u121
版本进行了更新。
JEP290
支持的版本:
- JEP 290 在 JDK 9 中加入
- 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)
JEP 290
主要提供了几个机制:
- 提供了一种灵活的机制,将可反序列化的类从任意类限制为上下文相关的类(黑白名单);
- 限制反序列化的调用深度和复杂度;
- 为
RMI
导出的对象设置了验证机制。( 比如对于RegistryImpl
,DGCImpl
类内置了默认的白名单过滤) - 过滤器机制不得要求对
ObjectInputStream
的现有子类进行子类化或修改。 - 提供一个全局过滤器,可以在
properties
或配置文件中进行配置。
实际上就是为了给用户提供一个更加简单有效并且可配置的过滤机制,以及对RMI
导出对象执行检查。
其核心实际上就是提供了一个名为 ObjectInputFilter
的接口,用户在进行反序列化操作的时候,将 filter
设置给 ObjectInputStream
对象。这里就是调用 setInternalObjectInputFilter
即可:
每当进行一次反序列化操作时,底层就会根据 filter
中的内容来进行判断,从而防止恶意的类进行反序列化操作。此外,还可以限制反序列化数据的信息,比如数组的长度、字节流长度、字节流深度以及使用引用的个数等。filter
返回 ALLOWED
,REJECTED
或者 UNDECIDED
几个状态,然后用户根据状态进行决策。
而对于RMI
来说,主要是导出远程对象前,先要执行过滤器逻辑,然后才进行接下来的动作,即对反序列化过程执行检查。
此外,还提供了两种可配置过滤器的方式:
(1)通过设置jdk.serialFilter System.property
如 -Djdk.serialFilter=<白名单类1>;<白名单类2>;!<黑名单类>
(2)直接通过conf/security/java.properties
文件进行配置 如 -Djava.security.properties=<黑白名单配置文件名>
具体规则方面的内容可以直接参考原始链接:
http://openjdk.java.net/jeps/290
RMI的过滤机制
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
字段值。
RMI 中 JEP 290 的绕过
参数利用的方式
这种利用局限性太强了。
为什么RMI
客户端利用传递参数反序列化攻击RMI
服务端就不受JEP290
限制
那是因为JEP290
提供了一些系列过滤器形式:进程级过滤器、自定义过滤器、内置过滤器。但是默认只为RMI
注册表和RMI
分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。(就像我们上面看到的一样)
但是RMI
客户端利用参数反序列化攻击没有也不能跟RMI
注册表和RMI
分布式垃圾收集器一样使用内置白名单过滤器。
绕过方式参考Java RMI 攻击分析(一)
JRMP服务端打JRMP客户端(ysoserial.exploit.JRMPListener)
JRMP(Java 远程消息交换协议),是Java的一种通信协议,其中RMI协议中的对象传输部分底层就可以通过JRMP协议实现。
而JRMP传输对象时就是基于序列化来实现的,因此在这个过程中可能会存在一些问题。JRMP
客户端反序列化顺序:
- 反序列化服务端给的
returnType
- 反序列化服务端给的一个
ID
- 反序列化服务端给的报错信息
小问题:为啥一定要利用报错信息写payload
,前两个不可以么?
当然不可以,readObject
才行
那我们知道了JRMP
客户端存在一个反序列化点,是可以被攻击,再来看看对应的服务端是在哪里插入payload
的(我们已经知道了大概是一个报错信息处)
这里网上的文章大多是直接拿yso exploit
的JRMPlistener
攻击代码来看了,那个代码是直接重构了JRMP
服务端,把报错信息改成payload
的,但是都没有说原生服务端在哪里写序列化。
服务端在三处地方可以写入payload
去发起对于客户端的请求(其实应该还有更多地方,比如我们下断点找过来的路径就不是这三个的任何一个),找到之后我们就会发现,报错信息都是代码中写好的了,我们没法去控制。
那么就只有自实现拼接出一个JRMP
服务端,来发送给JRMP
客户端一个序列化数据,这就是YSOSERIAL-exploit-JRMPListener
做的事情
复现
起一个JRMP
服务端
1 | java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "/System/Applications/Calculator.app/Contents/MacOS/Calculator" |
这个exploit
会对任何请求回应一个响应包,其中报错信息被替换成了CC6
链的payload
。
然后客户端运行Client.java
成功,而且我们测试的JDK1.8.0_312
是在JEP290(8u121)
修复之后!
这说明JRMP
服务端打JRMP
客户端的攻击方法不受JEP290
的限制!
因为之前也说到JEP290
默认只为RMI
注册表(RMI Register
层)和RMI
分布式垃圾收集器(DGC
层)提供了相应的内置过滤器,但是最底层的JRMP
是没有做过滤器的。所以可以攻击执行payload
JRMP服务端打JRMP客户端的过程:
- 要JRMP客户端去主动连接我们的JRMP服务端(白名单过滤器只对反序列化过程有效,对序列化过程无效)
- 我们恶意的JRMP服务端在原本是报错信息的位置写入利用链,序列化成数据包返回到JRMP客户端。
- 由于JRMP客户端的反序列化过程不存在JEP290的过滤器,所以我们的payload可以成功被执行,从而完成RCE
与RMI服务端反序列化攻击RMI注册端-Bind结合
服务端攻击注册端时是bind一个恶意类,注册端会反序列化这个类。但JEP290添加反序列化过滤器后,很多利用链被限制失效,就无法攻击成功了。
具体规则如下:
1 | 数组最大长度maxarray=1000000; |
那是否可以像JRMP服务端打JRMP客户端一样,要RMI注册端作为JRMP客户端去主动连接我们的JRMP服务端(白名单过滤器只对反序列化过程有效,对序列化过程无效)。我们恶意的JRMP服务端在原本是报错信息的位置写入利用链,序列化成数据包返回到JRMP客户端(RMI注册端)。由于JRMP客户端的反序列化过程不存在JEP290的过滤器,所以我们的payload可以成功被执行,从而完成RCE。
这里唯一需要做的步骤就是让原本目标进行第一条bind攻击,转换目标成让RMI注册端去作为JRMP客户端向我们指定的JRMP服务端去发起请求,从而完成一整个攻击链的衔接,这需要我们去寻找一个所有对象都在白名单中的Gadget去完成这一任务。
UnicastRef对象-构造可以指定连接目标的UnicastRef对象
Ysoserial中的payloads-JRMPClient就是一个可以完成JRMP服务器发起JRMP连接的调用栈:
1 | /** |
反序列化的入口其实不止readobject(),还有readExternal(),只不过后者稍微少见点。
通过调试可以发现UnicastRef对象的readExternal方法作为反序列化入口的话,我们可以通过控制序列化的内容使服务器向我们指定的服务器发起JRMP连接(通过DGC层的dirty方法发起),再通过之前讲到的JRMP客户端报错信息反序列化点完成RCE。
知道服务端反序列化处的触发流程之后,我们来看payload的构造。
一个基础的可以指定连接目标的UnicastRef对象:
1 | //让受害者主动去连接的攻击者的JRMPlister的host和port |
如果这个对象在目标服务器反序列化成功了,就可以顺着之前分析的反序列化过程向外发起连接。但是如何让这个对象反序列化呢?还需要进一步的封装
如何让UnicastRef对象反序列化
与bind操作进行拼接
我们的目标是:将UnicastRef对象封装进入register.bind(String,Remote)的Remote参数中,从而在反序列化Remote参数的时候因为反序列化的递归的特性,进行UnicastRef对象的反序列化。那又回归到了前面讨论过的问题,如何将UnicastRef对象封装成Remote类型:
- 压根不封装,跟Barmie工具一样自实现通讯协议,直接发送UnicastRef(因为其实只有客户端上层函数需要remote类型的输入,服务端并没有要求是remote类型,都会反序列化)
- 跟RMIRegisterExploit一样,使用动态代理来实现封装
回顾一下动态代理封装的原理:将我们的payload放在拦截器的类参数中,然后封装拦截器成Remote类型,反序列化的时候就会反序列化拦截器里面的payload的参数,从而形成反序列化。
但是跟之前不同的是:没有白名单的时候我们可以用到AnnotationInvocationHandler装载UnicastRef对象,再把它动态代理变成Remote对象。
但是在JEP290之后有了白名单限制,AnnotationInvocationHandler对象被禁了。
我们需要用到
动态代理-RemoteObjectInvocationHandler就是Ysoserial-Payload-JRMPClient实现逻辑 - 找一个同时继承实现两者的类或者一个实现Remote,并将UnicastRef类型作为其一个字段的类。这样只需要把我们的UnicastRef对象塞入这个类中,然后直接塞进register.bind(String,Remote)中就可以了。
bind的限制:
bind操作中注册端对于服务端的地址验证。可以通过lookup来替换bind操作来进行攻击,这样可以绕过bind操作中对于服务端得地址验证。
https://xz.aliyun.com/t/7932#toc-3
与RMI客户端反序列化攻击RMI服务端-Lookup结合
https://xz.aliyun.com/t/7932#toc-4
An Trinh的RMI注册端Bypass思路
先回顾一下我们之前是如何绕过JEP290的:
攻击者发送payload
让RMI
注册端发起一个JRMP
请求去链接我们的JRMP
服务器,然后接受并反序列化我们JRMP
服务器返回的报错信息,反序列化的时候通过RMI
注册端内部的利用链(比如CC)完成命令执行An Trinh
的绕过思路还是这个套路,JRMP
的部分一模一样没有改变,与我们之前不同的是如何让RMI注册端发起JRMP
请求这一部分。
之前我们提出许多许多攻击方式:绕过客户端-自实现协议去封装、动态代理、UnicastRef
类型参数实现Remote
接口的类等等、甚至可以自定义一个符合要求的类来攻击。
但是回归到这些攻击方式,其本质都是利用:
readobject
反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef
类。- 在
readobejct
反序列化的过程中填装UnicastRef
类到incomingRefTable
- 在
releaseInputStream
语句中从incomingRefTable
中读取ref
进行开始JRMP
请求
但是An Trinh
提出了一个新的思路来发起JRMP
请求,不是利用readobject
的递归-填装-触发的模式,而是readobject
函数调用过程直接触发JRMP
请求。
他的payload
攻击过程中:会在readobject
函数中触发他的Gadgets
发起JRMP
连接,但是在完成后,又会回到我们的readobject
的递归-填装-触发的模式中发起第二次JRMP
连接。具体流程如下:
readobject
递归反序列化到payload
对象中的UnicastRef
对象,填装UnicastRef
对象的ref
到incomingRefTable
- 在根据
readobject
的第二个最著名的特性:会调用对象自实现的readobject
方法,会执行UnicastRemoteObject
的readObject
,他的Gadgets
会在这里触发一次JRMP
请求 - 在
releaseInputStream
语句中从incomingRefTable
中读取ref
进行开始JRMP
请求
同时他Gadgets
发起JRMP
请求只会发起一次请求,而readobject
的递归-填装-触发的JRMP
请求,由于会检测DGC
是否绑定成功会循环发起JRMP
,形成天然的心跳木马(划重点)。
具体分析可看https://xz.aliyun.com/t/7932#toc-5
总结三个关键点:
- 利用
readobject
的复写特性执行UnicastRemoteObject
的readObject
- 利用动态代理的拦截执行
invoke
的特性,在UnicastRemoteObject#readObject
的调用链中执行proxy对象.createServerSocket
跳到了RemoteObjectInvocationHandler
的invoke
方法 RemoteObjectInvocationHandler
的invoke
方法可以根据内置的ref
向外发起JRMP
连接,再反序列化返回结果
复现—绕过8u231
使用ysomap
复现
首先开启一个Server端
然后使用ysoserial开启JRMP服务端
1 | java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 1111 CommonsCollections6 "cmd" |
接着使用ysomap进行攻击
可以看到反弹shell到远程服务器,判断路径,来自Registry端。
看一下JRMP服务端发生了什么
另外,我使用JDK1.8.0_05可以攻击成功,使用1.8.0_312攻击失败。
修复
8u231的修复
- sun.rmi.registry.RegistryImpl_Skel#dispatch报错情况消除ref
- sun.rmi.transport.DGCImpl_Stub#dirty在解析之前进行了黑名单限制
第一处修复:
其实只有一行的区别,在每个动作比如lookup,bind等中都添加了一个逻辑:如果出现了序列化报错都会进入catch,执行discardPedingRefs。
在discardPendingRefs中其实也就是做了一件事情,把我们之前装载的incomingRefTable清空
那么很清楚假如我们的payload在序列化中发生了报错,那么我们想尽办法装载的ref就会被清掉,那么之前的绕过方式都会有异常抛出
自定义类(动态代理或接口):报错ClassNotFoundException
1 | 因为我们传入的类虽然会完成装载,但是在后续的序列化逻辑中肯定是会因为找不到我们的恶意类而发生ClassNotFoundException报错的。 |
动态代理转换接口或者找内置接口:报错ClassCastException
1 | 其他的payload虽然因为都是有内置类的,这些内置类在序列化的时候var9.readObject();是没问题的。 |
第二处修复:
实际上第一处修复已经完美修复了,但是还有第二处修复针对的是ref被触发的时候,即var7.releaseInputStream();
在处理UnicastRef对象时,它必定会经过sun.rmi.transport.DGCImpl_Stub#dirty
在dirty方法中三个关键语句:
1 | this.ref.newCall:发起JRMP请求 |
把过滤器放在解析之前,那么JRMP请求是可以发起的,但是你最后命令执行的payload(比如CC)会被过滤器给干掉。
看下过滤器sun.rmi.transport.DGCImpl_Stub#leaseFilter:一样对长度、深度、黑名单做了限制
8u231修复的绕过
8u241的修复
8u241的修复第一处
在8u241
版本,针对这个绕过链进行了修复:修复说明在Oracle
官网也有说明
重点就是把应该是String
的地方从本来的(String)var9.readobject()
改成了SharedSecrets.getJavaObjectInputStreamReadString().readString(var9);
前者是可以反序列化Object
的,但是后者就完全不接受反序列化Object
。
8u241的修复第二处8u241
还修复了调用栈中的java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod
方法。
添加了一处针对传入method的验证,验证远程调用的method是一个remote,接口,这个method不是我们可控的。
由于动态代理特性过来的,method就是createServerSocket这个方法,然而它理所当然不是一个remote接口
所以即使我们用bind
绕过第一个修复,还是被第二个修复处给干掉了。
Reference
https://www.anquanke.com/post/id/200860
https://xz.aliyun.com/t/7932
https://www.anquanke.com/post/id/263726
https://m.freebuf.com/articles/web/324692.html