本节主要解释前文留下的问题:
只是建立了恶意JRMP的连接,但是没执行bind,为什么可以打成功?
结论
流程比较复杂,先直接给结论,绕过JEP290的原理是
- 利用JEP290白名单内的类,覆盖掉registerRefs(此时未建立JRMP连接)
- Registry 处理bind请求,最后会触发DGC逻辑,DGC向registerRefs里保存的Server发送dirty请求
- DGC底层使用的是StreamRemoteCall.executeCall进行io的传输,executeCall发送完之后,会根据对方的反馈进行不同的操作,当对方返回一定条件时(初步判断是对方RMI Exception),会直接序列化InputStream,造成反序列化漏洞
其中步骤2类似《RMI1》中提到的Client攻击Registry的思路,步骤3与 Registry攻击Client手法类似
之前一直简单以为是Vuln Registry充当了Vuln Server的角色,走的是Evil Registry Attack Vuln Server的角色,其实并不是。纸上得来终觉浅啊
攻击流程
- Server发起bind(name,EvilObj)请求
- Vuln Registry接受到请求,RegistryImpl_Skel处理该请求
- RegistryImpl_Skel反序列化obj,在JEP290白名单内,功能为触发JRMP连接,连接Evil Registry(对应上图步骤1)(纠正,其实并未建立连接,仅覆盖registerRefs)
- Vuln Registry底层ConnectionInputStream,负责管理维护连接,有一个表registerRefs,维护当前的client连接记录,因为反序列化EvilObj,被覆盖成了Evil Registry的JRMP连接信息
- Vuln Registry准备结束掉 Server发起bind请求,调用releaseInputStream(对应上图步骤2),准备通知DGC开始介入监控这个EvilObj的生命周期。
- Registry DGC初始化监控远程对象时,DGCImpl_Stub会发送一个dirty请求给Server,Server信息保存在registerRefs内。但是此时的Server已经被替换为Evil Registry。
- DGCImpl_Stub发送dirty请求给Evil Registry,最终是用的是StreamRemoteCall.executeCall来执行(对应步骤3)。《RMI1》中Client攻击Registry时有提到StreamRemoteCall.executeCall的问题,发送完之后会根据对方返回进行下一步操作,case2会直接反序列化InputStream
- Evil Registry接受到Client 的DGC dirty call,发送反馈payload,被Vuln Registry接受,进入case2逻辑触发反序列化boom💣(对应步骤4)
复现
在cc链上下断点,调用栈如下:
1 | readObject:150, LazyMap (org.apache.commons.collections.map) |
需要跟一遍流程,这里就不详细的讲解了,重点需要关注
bind流程,如何触发DGC,如何发送Dirty请求
sun.rmi.transport.StreamRemoteCall.in.incomingRefTable是如何被覆盖的
正常bind 的交互流程
以HelloServer、Registry为例,一次正常的bind产生的网络交互行为:
1. Server端逻辑
server 序列化 “hello”、new HelloImpl(),发送给registry 1088端口,并监听等待DGC call
对应的代码逻辑
跟进invoke(var3),到StreamRemoteCall.executeCall。发送完数据之后会监听返回,并判断,如果第一个bypte为2则进行序列化(初步判断==2表示registry返回异常)
这里的var14 = this.in.readObject();
就是多次提到的《RMI1》中registry攻击client的原理
2. Registry逻辑
registry 收到server bind请求包,反序列化”hello”、HelloImpl,并准备释放链接,释放链接前会将这些这些远程类引用记录传给DGC逻辑,DGC建立每个远程类的管理记录,且发送一个dirty请求给server
注意这里的DGC逻辑
对应的几个重要代码逻辑
覆盖StreamRemoteCall.in.incomingRefTable
先来看一遍反序列化EvilObj之后的对比:可以看到incomingRefTable已经被替换为恶意的Evil Registry连接地址了
incomingRefTable会被当作DGC发送dirty时,认为的server地址
具体是如何被覆盖的?
断下ConnectionInputStream.saveRef
1 | saveRef:73, ConnectionInputStream (sun.rmi.transport) |
实例{ConnectionInputStream@1023}.readObject()
POC中的 LiveRef(id, te, false)反序列化
LiveRef.read(var0={ConnectionInputStream@1023})
继续调用{ConnectionInputStream@1023}.saveRef, incomingRefTable被更新
RemoteObjectInvocationHandler
如何将UnicastRef在Register上反序列化,bind参数为Remote类型,不能直接传UnicastRef。
参考【2】,给出了寻找RemoteObjectInvocationHandler 的过程,利用Proxy 和Handler 动态转换,是很典型的反序列化利用手段。原文讲的很详细了,而且发现了其他几个 衔接类,具体参考原文。
小结
本节详细跟踪了JEP290的绕过流程,发现并不是简单的JRMP连接+Registry攻击Server的原理,而是因为DGC机制、StreamRemoteCall、incomingRefTable一些更为复杂的逻辑。
发现者需要对RMI、DGC的逻辑有深刻的了解,很是赞叹与崇拜。与其相比,发现RemoteObjectInvocationHandler都只是反序列化利用基础知识而已。
高山仰止
参考【1】中提到还有8u231之后还有修复及绕过,留待下篇再学习吧
参考
- [1] 针对RMI服务的九重攻击 - 下
- [2] 一次攻击内网rmi服务的深思