Skip to content

Commit 0e282fc

Browse files
committed
Optimize SSTI injection scenarios
1 parent 44e771b commit 0e282fc

3 files changed

Lines changed: 67 additions & 38 deletions

File tree

docs/instructions.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,49 @@ SpEL 注入的本质是不可信输入进入表达式“可执行上下文”后
928928
| 阻断类型引用 | `GET /spel/safe?ex=T(java.lang.Math).abs(-1)` | Java 类型引用 | 返回“表达式被安全上下文限制”,说明类型引用被禁止 |
929929
| 阻断命令执行 | `GET /spel/safe?ex=T(java.lang.Runtime).getRuntime().exec('true')` | 命令执行表达式 | 返回“表达式被安全上下文限制”,不执行系统命令 |
930930
| 安全场景错误处理 | `GET /spel/safe?ex=T(java.lang.Runtime).getRuntime().exec(` | 语法错误表达式 | 返回“表达式被安全上下文限制”或解析错误信息 |
931+
932+
## SSTI 模板注入
933+
934+
当前覆盖 Thymeleaf 视图名注入两类典型触发方式:Controller 返回值可控、URL 路径参数拼接进视图名。模块重点演示不可信输入进入服务端模板解析上下文后,`__${...}__` 预处理表达式会先被 Thymeleaf 计算,再进入模板名或 fragment 解析流程。
935+
936+
SSTI 的本质是不可信输入进入模板“可执行上下文”。在 Java Web 中风险入口不只包括页面内容,还包括视图名、fragment 表达式、邮件模板、报表模板、动态模板内容和多模板引擎配置。修复时应避免用户控制模板名、模板路径、fragment 或模板内容;动态选择模板时使用固定枚举/白名单映射;返回普通字符串时使用 `@ResponseBody``ResponseEntity` 或显式写入 `HttpServletResponse`,避免进入视图解析;变量输出优先使用自动转义能力,谨慎使用 `th:utext`
937+
938+
已覆盖类型
939+
940+
| 分类 | 已有场景 | 结论 |
941+
| --- | --- | --- |
942+
| 返回视图名可控 | `/ssti/vul1?para=...` | 覆盖 Controller 直接把用户输入拼接进视图名,Thymeleaf 预处理表达式被执行 |
943+
| URL 路径拼接视图名 | `/ssti/vul2/{path}` | 覆盖路径变量被拼接进视图名后触发 Thymeleaf 预处理表达式 |
944+
| 白名单模板选择 | `/ssti/safe1?para=...` | 覆盖只允许固定模板名,拒绝表达式进入模板路径 |
945+
| 跳过视图解析 | `/ssti/safe2/{path}` | 覆盖显式写入响应体后不再触发 Thymeleaf 视图解析 |
946+
| 内容输出边界 | `/ssti/vul3?para=...` | 当前仅通过 `th:utext` 输出字符串,不会把输入当模板表达式执行,应归类为 HTML 输出/XSS 边界而非 SSTI 主场景 |
947+
948+
模块覆盖基本符合综合性靶场定位,适合作为 Thymeleaf 视图名 SSTI 专题。后续如需增强,可补充“Freemarker 模板内容注入”“Velocity 模板注入”“Thymeleaf fragment 表达式注入”“邮件/通知模板可控”“模板缓存与模板加载器路径穿越”“沙箱绕过与安全配置对照”等场景。当前 `vul3` 与 SSTI 主线存在边界差异,建议保留为说明或移到 XSS/模板输出边界,不作为核心 SSTI 漏洞统计。
949+
950+
### SSTI 漏洞场景测试
951+
952+
页面:`/ssti`
953+
954+
| 场景 | 请求 | 测试输入 | 预期结果 |
955+
| --- | --- | --- | --- |
956+
| 页面访问 | `GET /ssti` | 已登录会话 | 页面正常打开,展示 return 可控、URL 可控和安全对照场景 |
957+
| return 可控算术探测 | `GET /ssti/vul1?para=__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回 500,错误信息中的模板名包含 `vul/ssti/49`,说明表达式已执行 |
958+
| return 可控命令链路 | `GET /ssti/vul1?para=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('id').getInputStream()).next()}__::.x` | 安全测试命令 `id` | 返回 500,错误信息中的模板名被替换为命令输出片段,说明可触达命令执行 Sink |
959+
| URL 可控算术探测 | `GET /ssti/vul2/__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回 500,错误信息中的模板名包含 `vul/ssti/49`,说明表达式已执行 |
960+
| 内容输出边界 | `GET /ssti/vul3?para=__${7*7}__::.x` | 模板表达式字符串 | 页面原样输出 `__${7*7}__::.x`,不执行表达式 |
961+
| return 流量包下载 | `GET /other/datapackage/ssti/ssti_return.pcapng` || 返回可下载流量包 |
962+
| URL 流量包下载 | `GET /other/datapackage/ssti/ssti_url.pcapng` || 返回可下载流量包 |
963+
964+
### SSTI 安全场景测试
965+
966+
页面:`/ssti`
967+
968+
| 场景 | 请求 | 测试输入 | 预期结果 |
969+
| --- | --- | --- | --- |
970+
| 白名单允许模板 | `GET /ssti/safe1?para=ssti` | 白名单模板名 | 返回 SSTI 模块页面 |
971+
| 白名单拒绝表达式 | `GET /ssti/safe1?para=__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回 401 页面,表达式不进入视图名 |
972+
| 跳过视图解析 | `GET /ssti/safe2/__${7*7}__::.x` | Thymeleaf 预处理表达式 | 返回纯文本“已跳过视图解析...”,不触发 Thymeleaf 解析 |
973+
931974
## 反序列化
932975

933976
当前覆盖 Java 原生 `ObjectInputStream.readObject()`、SnakeYAML `Yaml.load()`、XMLDecoder `readObject()` 三类 Java 反序列化入口。模块重点演示“不可信数据恢复为对象”时,攻击者如何通过对象图、类型标签、构造器、setter、`readObject` 或组件 gadget 链,把普通数据解析入口升级为代码执行、类加载、文件操作或拒绝服务风险。

src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,12 @@
55
import io.swagger.annotations.ApiOperation;
66
import io.swagger.annotations.ApiParam;
77
import lombok.extern.slf4j.Slf4j;
8-
import org.springframework.expression.EvaluationContext;
9-
import org.springframework.expression.Expression;
10-
import org.springframework.expression.ExpressionParser;
11-
import org.springframework.expression.spel.standard.SpelExpressionParser;
12-
import org.springframework.expression.spel.support.SimpleEvaluationContext;
13-
import org.springframework.expression.spel.support.StandardEvaluationContext;
148
import org.springframework.stereotype.Controller;
159
import org.springframework.ui.Model;
1610
import org.springframework.web.bind.annotation.*;
17-
import top.whgojp.common.utils.R;
1811

1912
import javax.servlet.http.HttpServletResponse;
13+
import java.io.IOException;
2014
import java.util.ArrayList;
2115
import java.util.Arrays;
2216
import java.util.List;
@@ -49,8 +43,9 @@ public String vul1(@ApiParam(name = "para", value = "用户输入参数", requir
4943
return "vul/ssti/" + para;
5044
}
5145
@GetMapping("/vul2/{path}")
52-
public void vul2(@PathVariable String path) {
53-
log.info("SSTI注入:"+path);
46+
public String vul2(@PathVariable String path) {
47+
log.info("SSTI注入:" + path);
48+
return "vul/ssti/" + path;
5449
}
5550
@GetMapping("/vul3")
5651
public String vul3(@ApiParam(name = "para", value = "用户输入参数", required = true) @RequestParam String para, Model model) {
@@ -62,16 +57,18 @@ public String vul3(@ApiParam(name = "para", value = "用户输入参数", requir
6257
public String safe1(@ApiParam(name = "para", value = "用户输入参数", required = true) @RequestParam String para, Model model) {
6358
List<String> white_list = new ArrayList<>(Arrays.asList("vul", "ssti"));
6459
if (white_list.contains(para)){
65-
return "vul/ssti" + para;
60+
return "vul/ssti/" + para;
6661
} else{
6762
return "common/401";
6863
}
6964
}
7065
@GetMapping("/safe2/{path}")
71-
public void safe2(@PathVariable String path, HttpServletResponse response) {
72-
log.info("SSTI注入:"+path);
66+
public void safe2(@PathVariable String path, HttpServletResponse response) throws IOException {
67+
log.info("SSTI注入:" + path);
68+
response.setContentType("text/plain;charset=UTF-8");
69+
response.getWriter().write("已跳过视图解析,输入路径:" + path);
7370
}
7471

7572

7673

77-
}
74+
}

src/main/resources/templates/vul/ssti/ssti.html

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
<blockquote class="layui-elem-quote layui-quote-nm"
1313
style="font-size: 15px;background-color: #a7deefab;box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075) !important">
1414
<p>
15-
<pre> SSTI(Server Side Template Injection):模板引擎是一种通过将模板中的占位符替换为实际数据来动态生成内容的工具,如HTML页面、邮件等。它简化了视图层的设计,但如果未对用户输入进行有效校验,可能导致安全风险如任意代码执行</pre>
16-
<pre> Java中常用的模板引擎有Freemarker、Velocity、Thymeleaf等,在这里以Thymeleaf引擎为例</pre>
15+
<pre> SSTI(Server Side Template Injection) 是不可信输入进入服务端模板解析上下文后,被模板引擎当作模板语法执行的问题。风险不只来自页面变量输出,也可能来自视图名、模板片段名、模板内容、邮件模板和报表模板等动态渲染入口。</pre>
16+
<pre> 本模块以 Thymeleaf 为例,重点演示 Spring MVC 返回视图名可控、URL 路径参数拼接进视图名两类触发方式。修复时应避免用户控制模板名或模板内容,确需动态选择模板时使用固定映射或白名单,并用 HttpServletResponse/@ResponseBody 等方式明确跳过视图解析。</pre>
1717
</p>
1818
</blockquote>
1919
</fieldset>
@@ -46,8 +46,8 @@ <h1 style="display: flex; justify-content: space-between; align-items: center">
4646
<div class="layui-tab-content">
4747
<div class="layui-tab-item layui-show">
4848
<blockquote class="layui-elem-quote main_btn">
49-
<p>攻击者可以操控return中的值,就有可能造成模板注入漏洞</p>
50-
<a target="_blank" href="/ssti/vul1?para=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27id%27).getInputStream()).next()%7d__::.x">
49+
<p>攻击者可控 Controller 返回的视图名时,Thymeleaf 会先处理 <code>__${...}__</code> 预处理表达式,再解析模板名。</p>
50+
<a target="_blank" href="/ssti/vul1?para=__$%7b7*7%7d__::.x">
5151
<button class="layui-btn layui-btn-normal" style="width: 100px; margin-left: 10px;">
5252
<span class="iconfont icon-zhihang">Run</span>
5353
</button>
@@ -57,19 +57,8 @@ <h1 style="display: flex; justify-content: space-between; align-items: center">
5757

5858
<div class="layui-tab-item">
5959
<blockquote class="layui-elem-quote main_btn">
60-
<p>当方法返回为void时,thymeleaf会以URL路由为视图名称,调用模板视图去解析</p>
61-
<a target="_blank" href="/ssti/vul2/__$%7BT(java.lang.Runtime).getRuntime().exec('open%20-a%20Calculator')%7D__::.x">
62-
<button class="layui-btn layui-btn-normal" style="width: 100px; margin-left: 10px;">
63-
<span class="iconfont icon-zhihang">Run</span>
64-
</button>
65-
</a>
66-
</blockquote>
67-
</div>
68-
69-
<div class="layui-tab-item">
70-
<blockquote class="layui-elem-quote main_btn">
71-
<p>text/html:浏览器在获取到这种文件时会自动调用html的解析器对文件进行相应的处理</p>
72-
<a target="_blank" href="ssti/vul3?para=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27id%27).getInputStream()).next()%7d__::.x">
60+
<p>URL 路径变量被拼接进 Controller 返回的视图名时,同样会进入 Thymeleaf 视图解析和预处理流程。</p>
61+
<a target="_blank" href="/ssti/vul2/__$%7b7*7%7d__::.x">
7362
<button class="layui-btn layui-btn-normal" style="width: 100px; margin-left: 10px;">
7463
<span class="iconfont icon-zhihang">Run</span>
7564
</button>
@@ -81,7 +70,7 @@ <h1 style="display: flex; justify-content: space-between; align-items: center">
8170
<div class="layui-card">
8271
<div class="layui-card-header"><i class="fa fa-bullhorn icon-tip"></i>tips</div>
8372
<div class="layui-card-body layui-text layadmin-text">
84-
<pre style="color: #28333e;font-size: 15px;"> 高版本SpringBoot/Thymeleaf不存在模板注入问题,这里SpringBoot版本为2.4.1,Thymeleaf同上</pre>
73+
<pre style="color: #28333e;font-size: 15px;"> 典型审计点是 Controller 返回的视图名、fragment 名或模板路径是否可被用户控制,例如 return "vul/ssti/" + para 或 return "vul/ssti/" + path。访问 __${7*7}__::.x 时,如果错误信息中的模板名变为 49,说明表达式已被 Thymeleaf 预处理执行。命令执行 payload 仅用于安全测试环境,日常验证优先使用 7*7 这类无副作用表达式。</pre>
8574
</div>
8675
</div>
8776
</div>
@@ -114,7 +103,7 @@ <h1><span class="iconfont icon-anquan"> 安全场景:thymeleaf模版注入</sp
114103
<div class="layui-tab-content">
115104
<div class="layui-tab-item layui-show">
116105
<blockquote class="layui-elem-quote main_btn">
117-
<p>基于白名单的路径校验</p>
106+
<p>基于固定模板白名单选择视图,用户只能命中预定义模板名。</p>
118107
<a target="_blank" href="/ssti/safe1?para=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27id%27).getInputStream()).next()%7d__::.x">
119108
<button class="layui-btn layui-btn-normal" style="width: 100px; margin-left: 10px;">
120109
<span class="iconfont icon-zhihang">Run</span>
@@ -125,8 +114,8 @@ <h1><span class="iconfont icon-anquan"> 安全场景:thymeleaf模版注入</sp
125114

126115
<div class="layui-tab-item">
127116
<blockquote class="layui-elem-quote main_btn">
128-
<p>控制器返回为void,方法参数设置为HttpServletResponse后,Spring MVC会跳过视图解析过程</p>
129-
<a target="_blank" href="ssti/safe2/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27id%27).getInputStream()).next()%7d__::.x">
117+
<p>控制器写入 HttpServletResponse 后,Spring MVC 不再用 URL 路径推导视图名。</p>
118+
<a target="_blank" href="/ssti/safe2/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27id%27).getInputStream()).next()%7d__::.x">
130119
<button class="layui-btn layui-btn-normal" style="width: 100px; margin-left: 10px;">
131120
<span class="iconfont icon-zhihang">Run</span>
132121
</button>
@@ -139,10 +128,10 @@ <h1><span class="iconfont icon-anquan"> 安全场景:thymeleaf模版注入</sp
139128
<div class="layui-card-header"><i class="fa fa-bullhorn icon-tip"></i>tips</div>
140129
<div class="layui-card-body layui-text layadmin-text">
141130
<pre style="color: #28333e;font-size: 15px;">安全编码规范:
142-
1、避免用户输入直接作为模板名称或路径
143-
2、对所有动态内容进行严格校验和转义,包括模板变量
144-
3、选择支持自动转义的安全模板引擎(如Thymeleaf的th:text)
145-
4、使用白名单限制动态渲染的模板,控制可访问的模板范围</pre>
131+
1、不要把用户输入拼接进模板名、模板路径、fragment 表达式或模板内容。
132+
2、动态模板选择应使用枚举/白名单映射到固定模板,拒绝直接透传用户输入。
133+
3、需要返回字符串内容时使用 @ResponseBody、ResponseEntity 或显式写入 HttpServletResponse,避免进入视图解析。
134+
4、变量输出优先使用 th:text 等自动转义能力,th:utext 只用于可信 HTML 内容。</pre>
146135
</div>
147136
</div>
148137
</div>

0 commit comments

Comments
 (0)