Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
新增thymeleaf片段选择器注入&Freemarker&velocity模版注入示例及修复代码
  • Loading branch information
x1ongsec committed Feb 24, 2025
commit 9670c6114e2e8fcb6d310e182725fe209c027801
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@
<version>2.3.1</version>
</dependency>

<!-- Freemarker 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>1.5.8.RELEASE</version>
</dependency>

</dependencies>


Expand Down
165 changes: 159 additions & 6 deletions src/main/java/com/best/hello/controller/SSTI.java
Original file line number Diff line number Diff line change
@@ -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 (服务端模板注入)
Expand All @@ -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);

/**
Expand Down Expand Up @@ -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);
}


Expand All @@ -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", "[email protected]");
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>", username);
StringReader reader = new StringReader(templateString);
VelocityContext ctx = new VelocityContext();
ctx.put("name", "Hello-Java-Sec");
ctx.put("phone", "012345678");
ctx.put("email", "[email protected]");

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();
}
}
17 changes: 11 additions & 6 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
spring.profiles.active=dev

# Actuator设置全部暴露
# Actuator\u8BBE\u7F6E\u5168\u90E8\u66B4\u9732
management.endpoints.web.exposure.include=*
#management.endpoints.enabled-by-default=false
# 自定义端点
# \u81EA\u5B9A\u4E49\u7AEF\u70B9
info.author=nul1
info.create=2021-07-10

# 配置mapper.xml路径
# \u914D\u7F6Emapper.xml\u8DEF\u5F84
mybatis.mapper-locations=classpath:mapper/*.xml

# 启动日志
# \u542F\u52A8\u65E5\u5FD7
server.tomcat.accesslog.enabled=true
server.tomcat.basedir=./
management.health.ldap.enabled=false

# 默认账号,请及时修改
# \u9ED8\u8BA4\u8D26\u53F7\uFF0C\u8BF7\u53CA\u65F6\u4FEE\u6539
local.admin.name = admin
local.admin.password = admin
local.admin.password = admin

# Freemarker \u6A21\u7248\u914D\u7F6E
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
46 changes: 46 additions & 0 deletions src/main/resources/templates/commons/400.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>400</title>
<meta charset="UTF-8">
</head>
<body>

<div class="container-fluid">
<div class="row">

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<pre>
...
;::::;
;::::; :;
;:::::' :;
;:::::; ;.
,:::::' ; OOO\
::::::; ; OOOOO\
;:::::; ; OOOOOOOO
,;::::::; ;' / OOOOOOO
;:::::::::`. ,,,;. / / DOOOOOO
.';:::::::::::::::::;, / / DOOOO
,::::::;::::::;;;;::::;, / / DOOO
;`::::::`'::::::;;;::::: ,#/ / DOOO
:`:::::::`;::::::;;::: ;::# / DOOO
::`:::::::`;:::::::: ;::::# / DOO
`:`:::::::`;:::::: ;::::::#/ DOO
:::`:::::::`;; ;:::::::::## OO
::::`:::::::`;::::::::;:::# OO
`:::::`::::::::::::;'`:;::# O
`:::::`::::::::;' / / `:#
::::::`:::::;' / / `#

<span style="color: red;font-size:16px">400 请求错误!!!</span>
</pre>
</main>
</div>
</div>

<!-- 引入script -->
<div th:replace="~{commons/commons::script}"></div>

</body>
</html>
46 changes: 46 additions & 0 deletions src/main/resources/templates/commons/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>404</title>
<meta charset="UTF-8">
</head>
<body>

<div class="container-fluid">
<div class="row">

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<pre>
...
;::::;
;::::; :;
;:::::' :;
;:::::; ;.
,:::::' ; OOO\
::::::; ; OOOOO\
;:::::; ; OOOOOOOO
,;::::::; ;' / OOOOOOO
;:::::::::`. ,,,;. / / DOOOOOO
.';:::::::::::::::::;, / / DOOOO
,::::::;::::::;;;;::::;, / / DOOO
;`::::::`'::::::;;;::::: ,#/ / DOOO
:`:::::::`;::::::;;::: ;::# / DOOO
::`:::::::`;:::::::: ;::::# / DOO
`:`:::::::`;:::::: ;::::::#/ DOO
:::`:::::::`;; ;:::::::::## OO
::::`:::::::`;::::::::;:::# OO
`:::::`::::::::::::;'`:;::# O
`:::::`::::::::;' / / `:#
::::::`:::::;' / / `#

<span style="color: red;font-size:16px">404 页面未找到!!!</span>
</pre>
</main>
</div>
</div>

<!-- 引入script -->
<div th:replace="~{commons/commons::script}"></div>

</body>
</html>
2 changes: 1 addition & 1 deletion src/main/resources/templates/commons/commons.html
Original file line number Diff line number Diff line change
Expand Up @@ -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)
</script>
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/main/resources/templates/freemarker/index.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h1>Welcome to the home page</h1>
</body>
</html>
Loading