浅谈 JEP290
浅谈 JEP290
0x01 前言
属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。
一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI
8u121 ~ 8u230 打不了 RMI
0x02 关于 JEP290
JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事
1、提供一个限制反序列化类的机制,白名单或者黑名单。
2、限制反序列化的深度和复杂度。
3、为 RMI 远程调用对象提供了一个验证类的机制。
4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。
官方从 8u121,7u13,6u141 分别支持了这个 JEP
0x03 JEP290 防御手段分析
先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI
尝试去攻击,这里会报错,报错部分信息为
1 | java.io.ObjectInputStream filterCheck |
可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290
- 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了
ObjectInputFilter
相关的类
我这里去看了看 ObjectInputFilter
相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel
类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。
跟进,ObjectInputStream
类调用了 readObject0()
方法,继续跟进
先获取输入当中 blkmode
,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve()
方法,跟进
跟进 readClassDesc()
方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。
在 readClassDesc()
方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc()
方法
readProxyDesc()
方法做完一系列基础判断之后调用了 filterCheck()
方法,跟进
而 filterCheck()
方法又调用了 checkInput()
方法,这里应该是最终来判断输入是否合法的地方。
这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote
类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler
,第一次会先判断 java.rmi.Remote
类是否合法
对应的判断代码,其实也就是白名单了。代码会首先判断 var2
是否等于 String
类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个:
1 | return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED; |
而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler
类并不在这些白名单中,所以会被过滤
0x04 JEP290 绕过
这里我们可以先看一下白名单里面都能过什么,白名单如下
1 | String.class |
这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass
绕过利用
思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样
先用 ysoserial 开启 JRMP 3333 端口的监听
1 | java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc" |
然后编写 RMI 的 EXP
1 | import sun.rmi.server.UnicastRef; |
这个 payload 的原理就是伪造了一个 UnicastRef
用于跟注册中心通信,我们从 bind()
方法开始分析一下这一整个流程。
绕过分析
我们通过 getRegistry
时获得的注册中心,其实就是一个封装了 UnicastServerRef
对象的对象
当我们调用 bind
方法后,会通过 UnicastRef
对象中存储的信息与注册中心进行通信
这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化
接着来看看 yso 中的 JRMPClient 是做了什么操作
1 | ObjID id = new ObjID(new Random().nextInt()); // RMI registry |
这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler
父类RemoteObject
的readObject
方法(因为RemoteObjectInvacationHandler
没有readObject
方法),在readObject
里的最后一行会调用ref.readExternal
方法,并将ObjectInputStream
传进去:
这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下
1 | readObject:455, RemoteObject (java.rmi.server) |
一路跟进到 sun.rmi.transport.LiveRef#read
可以看到这里把 payload 里所传入的 LiveRef
解析到 var5
变量处,里面包含了 ip
与 端口
信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。
跟进 saveRef()
方法,里面做了一个映射,其建立了一个 TCPEndpoint
到 ArrayList<LiveRef>
的映射关系。
到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch()
方法,在调用了 readObject
方法之后调用了 var2.releaseInputStream();
,跟进
releaseInputStream()
方法调用了 this.in.registerRefs()
方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs()
方法
var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息
接着看到 registerRefs
方法中的 this.makeDirtyCall(var2, var3);
,跟进一下
里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty()
方法,跟进。
在 dirty()
方法中调用 wirteObject()
方法后,会用 invoke()
将数据发出去。
invoke()
方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP
至此绕过分析结束
0x05 小结
本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。
0x06 Ref
- 本文标题:浅谈 JEP290
- 创建时间:2023-04-18 22:00:48
- 本文链接:2023/04/18/浅谈-JEP290/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!