diff --git a/pom.xml b/pom.xml index 2217139..50b2a60 100644 --- a/pom.xml +++ b/pom.xml @@ -294,6 +294,13 @@ 2.3.1 + + + org.springframework.boot + spring-boot-starter-freemarker + 1.5.8.RELEASE + + diff --git a/src/main/java/com/best/hello/controller/SSTI.java b/src/main/java/com/best/hello/controller/SSTI.java index 7c1dab0..dc8fece 100644 --- a/src/main/java/com/best/hello/controller/SSTI.java +++ b/src/main/java/com/best/hello/controller/SSTI.java @@ -1,19 +1,29 @@ package com.best.hello.controller; +import freemarker.cache.StringTemplateLoader; +import freemarker.core.TemplateClassResolver; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.RuntimeSingleton; +import org.apache.velocity.runtime.parser.ParseException; +import org.apache.velocity.runtime.parser.node.SimpleNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import java.util.Map; /** * SSTI (服务端模板注入) @@ -25,6 +35,15 @@ @Controller @RequestMapping("/vulnapi/SSTI") public class SSTI { + + private final Configuration conf; + private final StringTemplateLoader stringTemplateLoader; + + public SSTI(Configuration configuration) { + this.conf = configuration; + this.stringTemplateLoader = new StringTemplateLoader(); + configuration.setTemplateLoader(stringTemplateLoader); + } Logger log = LoggerFactory.getLogger(SSTI.class); /** @@ -62,7 +81,7 @@ public String thymeleafSafe(@RequestParam String lang) { @ApiOperation(value = "vul:url作为视图名") @GetMapping("/doc/vul/{document}") public void getDocument(@PathVariable String document) { - log.info("[vul] SSTI payload: " + document); + log.info("[vul] SSTI payload: {}", document); } @@ -72,5 +91,139 @@ public void getDocument(@PathVariable String document, HttpServletResponse respo log.info("[safe] SSTI payload: " + document); } + /** + * SpringBoot Thymeleaf 片段选择器注入 + * @poc http://127.0.0.1:8888/vulnapi/SSTI/thymeleaf/fragment/vul?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22).getInputStream()).next()%7d__::.x + */ + @ApiOperation(value = "val:url作为片段选择器") + @GetMapping("/thymeleaf/fragment/vul") + public String fragmentVul(@RequestParam String section) { + return "lang/en :: " + section; + } + /** + * 设置 @ResponseBody 注解告诉 Spring 将返回值作为响应体处理,而不再是视图名称,因此无法进行模版注入攻击 + */ + @ApiOperation(value = "safe", notes = "由于设置 @ResponseBody 注解告诉 Spring 将返回值作为响应体处理,而不再是视图名称,因此无法进行模版注入攻击") + @GetMapping("/thymeleaf/fragment/safe") + @ResponseBody + public String fragmentSafe(@RequestParam String section) { + return "lang/en :: " + section; + } + + /** + * SpringBoot FreeMarker 模版注入 + * @poc http://127.0.0.1:8888/vulnapi/SSTI/freemarker/vul?file=index.ftl&content=%3C%23assign%20ex%3d%22freemarker%2etemplate%2eutility%2eExecute%22%3fnew%28%29%3E%20%24%7b%20ex%28%22whoami%22%29%20%7d + */ + + @ApiOperation(value = "vul:freemarker模版注入") + @GetMapping("/freemarker/vul") + public String freemarkerVul(@RequestParam String file, @RequestParam String content, Model model, HttpServletRequest request) { + log.info("[vul] FreeMarker payload: {}", content); + if (!file.trim().isEmpty()) { + // 防止目录穿越 + if (!file.contains("..") && !file.startsWith("/")) { + if (!content.trim().isEmpty()) { + // 检查类路径下模板是否存在 + String resourcePath = "templates/freemarker/" + file; + InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath); + if (is != null) { + try { + is.close(); + } catch (IOException e) { + log.error("关闭流失败", e); + } + + // 如果请求路径为 /vulnapi/SSTI/freemarker/safe 则不设置解析所有类 + if (!request.getRequestURI().startsWith("/vulnapi/SSTI/freemarker/safe")) { + conf.setNewBuiltinClassResolver(TemplateClassResolver.UNRESTRICTED_RESOLVER); + } + + // 添加模板到StringTemplateLoader(名称需与后续查找一致) + stringTemplateLoader.putTemplate(file, content); + // 禁用缓存以确保立即生效 + conf.setTemplateUpdateDelayMilliseconds(0); + // 关闭模版加载时的异常日志 + conf.setLogTemplateExceptions(false); + return file.replace(".ftl", ""); + } + model.addAttribute("error", "模版文件不存在!"); + return "commons/404"; + } + model.addAttribute("error", "文件内容不能为空!"); + return "commons/400"; + } + model.addAttribute("error", "文件名称非法!"); + return "commons/400"; + } + model.addAttribute("error", "文件名不能为空!"); + return "commons/400"; + } + + @ApiOperation(value = "vul:freemarker模版注入修复代码") + @GetMapping("/freemarker/safe") + public String freemarkerSafe(@RequestParam String file, @RequestParam String content, Model model, HttpServletRequest request) throws TemplateException { + // 使用安全的解析器 + conf.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); + // 关闭 FreeMarker debug 信息 + conf.setTemplateExceptionHandler(freemarker.template.TemplateExceptionHandler.RETHROW_HANDLER); + return this.freemarkerVul(file, content, model, request); + } + + /** + * velocity 模版注入evaluate场景 + * @poc http://127.0.0.1:8888/vulnapi/SSTI/velocity/evaluate/vul?username=%23set(%24e%3D%22e%22)%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22%2Cnull).invoke(null%2Cnull).exec(%22open%20-a%20Calculator%22) + */ + @ApiOperation(value = "vul:velocity模版注入evaluate场景") + @GetMapping("/velocity/evaluate/vul") + @ResponseBody + public String velocityEvaluateVul(@RequestParam(defaultValue = "Hello-Java-Sec") String username) { + String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email"; + Velocity.init(); + VelocityContext ctx = new VelocityContext(); + ctx.put("name", "Hello-Java-Sec"); + ctx.put("phone", "012345678"); + ctx.put("email", "xxx@xxx.com"); + StringWriter out = new StringWriter(); + Velocity.evaluate(ctx, out, "test", templateString); + return out.toString(); + } + + /** + * velocity 模版注入merge场景 + * @poc http://127.0.0.1:8888/vulnapi/SSTI/velocity/merge/vul?username=%23set(%24e%3D%22e%22)%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22%2Cnull).invoke(null%2Cnull).exec(%22open%20-a%20Calculator%22) + */ + @ApiOperation(value = "vul:velocity模版注入merge场景") + @GetMapping("/velocity/merge/vul") + @ResponseBody + public String velocityMergeVul(@RequestParam(defaultValue = "Hello-Java-Sec") String username) throws IOException, ParseException { + // 获取模版文件内容 + BufferedReader bufferedReader = new BufferedReader(new FileReader(String.valueOf(Paths.get(this.getClass().getClassLoader().getResource("templates/velocity/merge.vm").toString().replace("file:", ""))))); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line); + } + String templateString = stringBuilder.toString(); + templateString = templateString.replace("", username); + StringReader reader = new StringReader(templateString); + VelocityContext ctx = new VelocityContext(); + ctx.put("name", "Hello-Java-Sec"); + ctx.put("phone", "012345678"); + ctx.put("email", "xxx@xxx.com"); + + StringWriter out = new StringWriter(); + org.apache.velocity.Template template = new org.apache.velocity.Template(); + + RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices(); + SimpleNode node = runtimeServices.parse(reader, String.valueOf(template)); + + template.setRuntimeServices(runtimeServices); + template.setData(node); + template.initDocument(); + + template.merge(ctx, out); + + return out.toString(); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 328d90f..be2de22 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,4 +17,9 @@ management.health.ldap.enabled=false # 默认账号,请及时修改 local.admin.name = admin -local.admin.password = admin \ No newline at end of file +local.admin.password = admin + +# Freemarker 模版配置 +spring.freemarker.template-loader-path=classpath:/templates/ +spring.freemarker.suffix=.ftl +spring.freemarker.charset=UTF-8 diff --git a/src/main/resources/templates/commons/400.html b/src/main/resources/templates/commons/400.html new file mode 100644 index 0000000..56e40e7 --- /dev/null +++ b/src/main/resources/templates/commons/400.html @@ -0,0 +1,46 @@ + + + + 400 + + + + +
+
+ +
+
+                               ...
+                             ;::::;
+                           ;::::; :;
+                         ;:::::'   :;
+                        ;:::::;     ;.
+                       ,:::::'       ;           OOO\
+                       ::::::;       ;          OOOOO\
+                       ;:::::;       ;         OOOOOOOO
+                      ,;::::::;     ;'         / OOOOOOO
+                    ;:::::::::`. ,,,;.        /  / DOOOOOO
+                  .';:::::::::::::::::;,     /  /     DOOOO
+                 ,::::::;::::::;;;;::::;,   /  /        DOOO
+                ;`::::::`'::::::;;;::::: ,#/  /          DOOO
+                :`:::::::`;::::::;;::: ;::#  /            DOOO
+                ::`:::::::`;:::::::: ;::::# /              DOO
+                `:`:::::::`;:::::: ;::::::#/               DOO
+                 :::`:::::::`;; ;:::::::::##                OO
+                 ::::`:::::::`;::::::::;:::#                OO
+                 `:::::`::::::::::::;'`:;::#                O
+                  `:::::`::::::::;' /  / `:#
+                   ::::::`:::::;'  /  /   `#
+
+                      400 请求错误!!!
+      
+
+
+
+ + +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/commons/404.html b/src/main/resources/templates/commons/404.html new file mode 100644 index 0000000..fbbaada --- /dev/null +++ b/src/main/resources/templates/commons/404.html @@ -0,0 +1,46 @@ + + + + 404 + + + + +
+
+ +
+
+                               ...
+                             ;::::;
+                           ;::::; :;
+                         ;:::::'   :;
+                        ;:::::;     ;.
+                       ,:::::'       ;           OOO\
+                       ::::::;       ;          OOOOO\
+                       ;:::::;       ;         OOOOOOOO
+                      ,;::::::;     ;'         / OOOOOOO
+                    ;:::::::::`. ,,,;.        /  / DOOOOOO
+                  .';:::::::::::::::::;,     /  /     DOOOO
+                 ,::::::;::::::;;;;::::;,   /  /        DOOO
+                ;`::::::`'::::::;;;::::: ,#/  /          DOOO
+                :`:::::::`;::::::;;::: ;::#  /            DOOO
+                ::`:::::::`;:::::::: ;::::# /              DOO
+                `:`:::::::`;:::::: ;::::::#/               DOO
+                 :::`:::::::`;; ;:::::::::##                OO
+                 ::::`:::::::`;::::::::;:::#                OO
+                 `:::::`::::::::::::;'`:;::#                O
+                  `:::::`::::::::;' /  / `:#
+                   ::::::`:::::;'  /  /   `#
+
+                      404 页面未找到!!!
+      
+
+
+
+ + +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/commons/commons.html b/src/main/resources/templates/commons/commons.html index 4c87820..0a3bd3a 100644 --- a/src/main/resources/templates/commons/commons.html +++ b/src/main/resources/templates/commons/commons.html @@ -397,7 +397,7 @@ } // 创建编辑器 - const editorIds = ["code1", "code2", "code3", "code4", "code5", "code6", "code7", "code8"]; + const editorIds = ["code1", "code2", "code3", "code4", "code5", "code6", "code7", "code8", "code9", "code10"]; editorIds.forEach(createEditor) diff --git a/src/main/resources/templates/freemarker/index.ftl b/src/main/resources/templates/freemarker/index.ftl new file mode 100644 index 0000000..f0147e3 --- /dev/null +++ b/src/main/resources/templates/freemarker/index.ftl @@ -0,0 +1,10 @@ + + + + + Index + + +

Welcome to the home page

+ + \ No newline at end of file diff --git a/src/main/resources/templates/ssti.html b/src/main/resources/templates/ssti.html index 27840af..438a953 100644 --- a/src/main/resources/templates/ssti.html +++ b/src/main/resources/templates/ssti.html @@ -61,7 +61,7 @@
漏洞代码 - thymeleaf模版注入
+

+ + + + + + Run +
漏洞代码 - 片段选择器语法
+

+ + + + + + Run +
漏洞代码 - Freemarker模版注入
+

+ + + + + + Run +
漏洞代码 - velocity注入evaluate场景
+

@@ -100,7 +178,7 @@
漏洞代码 - url作为视图
Run
安全代码 - 白名单
- +

+ + + + + + Run +
安全代码 设置 @ResponseBody
+

+ + + + + + + Run +
安全代码
+

+ + + + + + Run +
漏洞代码 - velocity注入merge场景
+

diff --git a/src/main/resources/templates/velocity/merge.vm b/src/main/resources/templates/velocity/merge.vm new file mode 100644 index 0000000..5c1dbad --- /dev/null +++ b/src/main/resources/templates/velocity/merge.vm @@ -0,0 +1 @@ +Hello, | Full name: $name, phone: $phone, email: $email \ No newline at end of file diff --git a/velocity.log b/velocity.log new file mode 100644 index 0000000..b366a3b --- /dev/null +++ b/velocity.log @@ -0,0 +1,28 @@ +2025-02-24 17:50:07,351 - Log4JLogChute initialized using file 'velocity.log' +2025-02-24 17:50:07,351 - Initializing Velocity, Calling init()... +2025-02-24 17:50:07,351 - Starting Apache Velocity v1.7 (compiled: 2010-11-19 12:14:37) +2025-02-24 17:50:07,351 - Default Properties File: org/apache/velocity/runtime/defaults/velocity.properties +2025-02-24 17:50:07,351 - Trying to use logger class org.apache.velocity.runtime.log.AvalonLogChute +2025-02-24 17:50:07,351 - Target log system for org.apache.velocity.runtime.log.AvalonLogChute is not available (java.lang.NoClassDefFoundError: org/apache/log/Priority). Falling back to next log system... +2025-02-24 17:50:07,351 - Trying to use logger class org.apache.velocity.runtime.log.Log4JLogChute +2025-02-24 17:50:07,351 - Using logger class org.apache.velocity.runtime.log.Log4JLogChute +2025-02-24 17:50:07,353 - ResourceLoader instantiated: org.apache.velocity.runtime.resource.loader.FileResourceLoader +2025-02-24 17:50:07,353 - Do unicode file recognition: false +2025-02-24 17:50:07,353 - FileResourceLoader : adding path '.' +2025-02-24 17:50:07,357 - ResourceCache: initialized (class org.apache.velocity.runtime.resource.ResourceCacheImpl) with class java.util.Collections$SynchronizedMap cache map. +2025-02-24 17:50:07,358 - Loaded System Directive: org.apache.velocity.runtime.directive.Stop +2025-02-24 17:50:07,358 - Loaded System Directive: org.apache.velocity.runtime.directive.Define +2025-02-24 17:50:07,359 - Loaded System Directive: org.apache.velocity.runtime.directive.Break +2025-02-24 17:50:07,359 - Loaded System Directive: org.apache.velocity.runtime.directive.Evaluate +2025-02-24 17:50:07,359 - Loaded System Directive: org.apache.velocity.runtime.directive.Literal +2025-02-24 17:50:07,359 - Loaded System Directive: org.apache.velocity.runtime.directive.Macro +2025-02-24 17:50:07,360 - Loaded System Directive: org.apache.velocity.runtime.directive.Parse +2025-02-24 17:50:07,360 - Loaded System Directive: org.apache.velocity.runtime.directive.Include +2025-02-24 17:50:07,360 - Loaded System Directive: org.apache.velocity.runtime.directive.Foreach +2025-02-24 17:50:07,366 - Created '20' parsers. +2025-02-24 17:50:07,366 - Velocimacro : "velocimacro.library" is not set. Trying default library: VM_global_library.vm +2025-02-24 17:50:07,366 - Velocimacro : Default library not found. +2025-02-24 17:50:07,366 - Velocimacro : allowInline = true : VMs can be defined inline in templates +2025-02-24 17:50:07,366 - Velocimacro : allowInlineToOverride = false : VMs defined inline may NOT replace previous VM definitions +2025-02-24 17:50:07,366 - Velocimacro : allowInlineLocal = false : VMs defined inline will be global in scope if allowed. +2025-02-24 17:50:07,366 - Velocimacro : autoload off : VM system will not automatically reload global library macros