RMI(Remote Method Invocation)是由JDK自带提供的一套远程方法调用框架,用于实现跨JVM间的方法调用。其整体架构与常见的RPC框架类似。
如图共有三个角色参与其中:
Registry: 注册中心,负责维护当前集群提供的Service列表及对应的Server地址。
Server: 服务提供者,其具体实现了各项Service,注册到Registry,同时对外接受Client的请求。
Client: 服务调用者,通过Registry查询需要的Service, 后对Server发起请求。
样例代码:
Registry
Server
Client
RPC类框架的核心技术均大同小异,在客户端调用Service时,代理技术将调用涉及的数据(目标方法、参数..)序列化并通过网络传输到服务端,服务端反序列化数据执行本地调用,后将返回值序列化传输回客户端。
走读RMI源码可得,在运行时期,Registry通过TCP 1099对外提供Service查询与注册能力,暴露的方法包括:list、lookup、bind、rebind、unbind,Server启动时, 将监听一随机TCP端口作为服务端口,并通过bind将Service及服务地址注册到Registry,Client通过Registry的list、lookup查询Service,根据查询结果的服务地址直连Server,进行方法调用。
Registry对外接口: sun.rmi.registry.RegistryImpl_Stub#operations
客户端方法调用代理逻辑: sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)
这里我们关注下RMI的通信协议部分, 参考官方文档如下:
https://docs.oracle.com/javase/8/docs/technotes/guides/serialization/index.html
https://docs.oracle.com/javase/8/docs/platform/rmi/spec/rmi-protocol3.html
https://docs.oracle.com/javase/8/docs/platform/rmi/spec/rmi-protocol4.html
RMI使用的通信协议为: JRMP, 其在JDK实现中通过TCP传输,其中主要包括头部、数据部分,其中序列化方案使用的是Java原生序列化技术:
https://docs.oracle.com/javase/8/docs/technotes/guides/serialization/index.html
从JDK实现核心逻辑(Client方法调用)看协议实现
sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)写JRMP Header
sun.rmi.transport.tcp.TCPChannel#createConnection
sun.rmi.transport.tcp.TCPChannel#writeTransportHeader
确定调用类型及目标对象&方法
sun.rmi.transport.StreamRemoteCall#StreamRemoteCall(sun.rmi.transport.Connection, java.rmi.server.ObjID, int, long)
备注:
这里可以看到RMI对远程目标对象及方法的定位实现:
目标对象通过java.rmi.server.ObjID来描述
目标方法通过一个hash值来描述,sun.rmi.server.Util#computeMethodHash
写目标方法传参 (基于Java序列化协议)
sun.rmi.server.UnicastRef#marshalValue
RMI 对于参数传递和返回使用了Java原生序列化技术,故存在反序列化攻击风险。
Registry的查询注册能力本质是对外暴露一个ObjID RMI对象,其objnum固定为0,该对象包括如下5个方法,其中4个方法都有Object类型的传参,我们可通过构造恶意反序列化对象,通过RMI投递到Registry并实现反序列化攻击。
void bind(java.lang.String, java.rmi.Remote)
java.lang.String list()[]
java.rmi.Remote lookup(java.lang.String)
void rebind(java.lang.String, java.rmi.Remote)
void unbind(java.lang.String)
1.RMI Server没有 Registry ObjID(objnum=0)咋办?
RMI提供一个分布式GC方案(https://docs.oracle.com/javase/8/docs/platform/rmi/spec/rmi-dgc2a.html), 其使得每个Server会提供一个DGC ObjID(objnum=2),具体实现位于sun.rmi.transport.DGCImpl,公开方法为:
void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)
java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)
2.不知道Server服务端口咋办?
如果通过Registry#list、Registry#lookup,去查询Service的话,返回的代理对象内部是记录了Server的服务端口的,但该方式要求你的classpath下存在Service对应的接口类,否则在将抛出ClassNotFoundException。
方案: 直接解析Java反序列化字节数组,获取服务端口。
备注:
这里依赖一个Java反序列化字节解析工具me.trini7.run.serializes.bytes.SerializationProtocols,它根据序列化协议实现,可以在不依赖classpath的情况下解析出对象数据,并以JSON格式展示,后续有机会介绍反序列化协议时会在展开介绍。
lookup接口返回的是一个代理对象,其中服务端口的存储较为特殊,非代理对象及其字段对象的直接属性,其隐蔽的写入在java.rmi.server.RemoteObject#writeObject中,根据逻辑逆向出上述解码代码。
3.Attack
略
以上的攻击演示Registry、Server环境JDK版本为8u77,而当目标环境版本为8u121+时,攻击将不再有效。
上述原因及为JDK为反序列化攻击提供的缓解方案: JEP290(http://openjdk.java.net/jeps/290), 其最早在JDK9中引入,后续向下移植到老版本的JDK中(JDK 8u121、JDK 7u131、JDK 6u141),JEP290支持创建自定义过滤器,在反序列化时检查对象类型。
Registry ObjID Filter: sun.rmi.registry.RegistryImpl#registryFilter
只允许反序列化: String, Number, Remote, Proxy, UnicastRef, RMIClientSocketFactory, RMIServerSocketFactory, ActivationID, UID及子类
DGC ObjID Filter: sun.rmi.transport.DGCImpl#checkInput
只允许反序列化: ObjID、UID、VMID、Lease
Yso的方案: ysoserial.exploit.JRMPListener & ysoserial.payloads.JRMPClient
使用yso.JRMPListener建立一个恶意的Registry, 其针对来连接的RMI Server Or RMI Client会返回恶意Payload序列化字节流。
使用yso.JRMPClient攻击正常的Registry,使用对恶意Registry发起请求。当对返回的字节流做序列化时候被攻击。
备注: 这里的突破8u121的核心逻辑是,正常的Registry(Client角色), 去连接恶意Registry后,会对恶意Registry进行一次DGC#dirty的调用,并在未配置FIlter的情况下反序列化返回值。在后续的8u241中修复了该问题。
之前的介绍中,通过DGC ObjID去攻击Server,由于增加了Filter而无法生效,但如果Server提供的Service也接受了Object类型的参数,利用JRMPCall攻击仍然有效(通过lookup获取objID,计算methodhash即可)。
如何获取Server提供的Service methods呢?RMI提供的动态类下载是个机会:
https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/codebase.html
RMI Codebase支持客户端从服务端下载本地不存在的类,我们可以以此来获取获取Service并分析弱点方法(存在Object类型参数),自动根据lookup返回的objID及计算出methodhash,及自动Fuzz参数填充(Object类型参数填充payload),完成自动化攻击。
0x00 解析codebase
0x01 通过codebase远程下载类 & 自动分析弱点方法 (注意别被反杀了:)
0x02 攻击弱点方法(含参数自填充)
0x03 Attack