Java SSTI
本文最后更新于 2025年12月23日 晚上
引言
模板是一种预定义的文档结构或格式,其中包含静态内容和动态占位符(变量、表达式、控制语句等)。在 Web 开发中,模板通常用于生成 HTML 页面或其他文本输出。
常见的模板引擎包括:
- Python:Jinja2、Mako、Django Templates
- Java:Thymeleaf、Freemarker、Velocity
- PHP:Twig、Smarty
- JavaScript(服务端):EJS、Handlebars
使用模板的主要目的是实现 关注点分离(Separation of Concerns),具体优势包括:
- 前后端解耦:业务逻辑(后端代码)与展示逻辑(HTML 页面)分离,便于开发和维护。
- 动态内容生成:可根据用户请求、数据库数据等动态填充页面内容,而无需硬编码 HTML。
- 复用性高:模板可被多次调用,传入不同参数生成不同页面(如用户资料页、商品详情页等)。
- 提高开发效率:模板引擎通常提供循环、条件判断、继承等语法,简化复杂页面的构建。
Freemarker

demo
JAVA17
1 | |
1 | |
application.yml 配置文件,设置模板路径等freemarker的配置
1 | |
hello.ftl 就是要渲染的html模板文件
1 | |
HelloController
1 | |

语法和标签
注释<#– xxx–>
1 | |
${xxx} 插值
用于在模板中插入变量的值。
支持渲染的数据类型
| Java 类型 | 渲染方式 |
|---|---|
String |
直接输出 |
int, Integer, long, Long等 |
调用 toString() |
boolean, Boolean |
输出 true/false |
null |
默认报错(除非配置了 ?default 或 !) |
| java.util.Date | ${date?datetime}/${date?string("yyyy-MM-dd")} |
| Map<K,V> | ${map.key} 等价于 map.get("key")(key 必须是字符串) |
| JavaBean 对象 | 访问getter/public方法 |
枚举(Enum) |
public enum Status { ACTIVE, INACTIVE }// ${user.status} → "ACTIVE" |
如通过传递对象,调用其方法
1 | |
record(Java14+)不用构造函数就可以实例化

#xxx 指令
以 # 开头,用于控制逻辑流程、宏定义等。
- 条件判断:
#if,#elseif,#else等
1 | |
- 循环 #list
1 | |
user_index:当前索引(从 0 开始)

- 包含模板 #include
1 | |

- 定义宏 @#macro
1 | |
- 导入宏库 #import
1 | |

- 赋值 #assign
1 | |

也可用于创建复杂对象:
1 | |

- 局部变量 #local
仅在宏内部使用。
1 | |
- 设置配置 #setting
1 | |

- 停止处理 #stop
调试时使用,立即停止模板处理。
- <#break>类似于循环中断,只能在 #list里使用
1 | |
内建函数(Built-ins)
通过 ? 调用,例如:
1 | |
攻击面
不像其他模板的ssti,freemarker的ssti无法通过传参的方式(业务十分少见)来利用,一般需要有模板编辑/上传功能,即需要有能直接编写模板代码的手段。并且在freemarker高版本的配置中能rce的相关功能和函数是默认禁止的。
| 配置项 | 默认值 | 风险说明 |
|---|---|---|
api_builtin_enabled |
false(自 2.3.22 起) |
启用后可通过 ?api调用 Java 方法 |
newBuiltinClassResolver |
UNRESTRICTED_RESOLVER无限制 |
可通过 ?new 实例化任意类 |
| 允许用户控制模板源码 | — | 最高风险 |
| 数据模型包含危险工具类 | — | 如 Execute, ObjectConstructor |
代码审计时查看是否有以下配置
1 | |
一些探测可利用点的payload
- 探测
?api是否启用
1 | |
- 探测
?new是否启用
1 | |
- 基础信息探测
1 | |
- 模板上下文变量获取
- 信息收集:了解模板中有哪些对象可用,从而寻找可利用的入口(如
request、springContext、application等)。 - 判断上下文:确认是否处于 Spring Boot、Struts、自定义应用等环境中。
1 | |
- 其他payload
- 通过构造响应超时或者崩溃页面测试payload是否生效
1 | |
利用场景demo
用户输入直接作为 FreeMarker 模板源码渲染。
1 | |
1 | |

1 | |
数据模型包含 Execute 对象
1 | |

sstimap
https://github.com/vladko312/SSTImap/
sstimap支持freemarker的扫描和利用
在plugins\java\freemarker.py
1 | |
render用于测试模板是否可被渲染

利用${xxx}结合注释,预期输出为两个随机数拼接(注释中的数字被丢弃)
render_error用报错的方式测试模板是否可被渲染,依赖与错误回显/http状态码

${1/0}会抛出除零异常,可能在响应中暴露错误信息。- 结合
?new()尝试构造恶意对象。
布尔盲注测试

(1.0==1.0)为真 →'1'→eval成功 → 不报错。(1.0==0.1)为假 →'0'→1/0→ 报错。- 通过是否返回 500 错误判断布尔值。
命令执行测试

- 通过字符串替换测试
通过报错命令执行

各种payload,包括布尔盲注、时间盲注、写文件

上下文逃逸

这些定义了在不同模板上下文中如何“闭合”原有语法并注入恶意代码。例如:
- 在
<#-- 用户输入 -->中,可用--> ${malicious} <#--逃逸注释。 - 在
<#if user_input>中,需闭合if语句:1} > ${malicious} <#--。
velocity
demo
pom
1 | |
VelocityConfig.java
1 | |
helloController.java
1 | |
hello.vm
1 | |

语法和指令
$xxx/${xxx}变量引用
1 | |
#set 赋值
1 | |
#if / #elseif / #else:条件判断
1 | |
#foreach:循环
1 | |
#macro:定义宏(类似函数)
1 | |
#include 直接插入文件内容(不解析)
1 | |
#parse 插入模板解析
1 | |
#import 导入外部类
1 | |
#stop:停止模板渲染(调试用)
##注释
1 | |
字符串拼接
1 | |
两个触发点
evaluate
攻击面:不预设模板而是直接将用户输入作为模板内容
1 | |
代码审计重点:
- 用户输入作为模板内容
- 对用户输入调用了
.evaluate() - 版本<=1.7
1 | |

同时也存在xss
1 | |

merge
实际上并不是merge造成的漏洞,根源在于不安全的设计让其模板文件名可控,导致可以执行任意模板
1 | |
审计重点:
- 能够上传文件(要保证上传后内容字符一致)
- 调用了
velocityEngine.getTemplate().merge(),并且模板文件名可控


回显payload
1 | |

sstimap
plugins\java\velocity.py
1 | |
render测试是否是velocity

- header拼接字符串输出
test_render: 在 body 中设置$b = rand1 * rand2,为随机数乘法运算,预期返回乘积test_render_expected: 同样是预期的乘积。
render_error报错探测

trailer: 构造一个非法路径Y:/A:/...并调用#include($r),这会因文件不存在而抛出异常,异常信息中可能包含$h+$b+$t的拼接内容(其中$b是 body 中设置的输出)。test_render: 在 body 中设置$b = rand1 * rand2。test_render_expected: 同样是预期的乘积。
boolen布尔盲注,在完全无回显且无错误泄露时,通过响应时间或状态码差异进行逐位探测。

test_bool_true/false: 利用#if条件判断 +#include("Y:/A:/xxx")触发错误,根据回显差异判断verify_bool_*: 用1.0.equals(0.1/1.0)测试浮点比较是否可靠(避免整数优化干扰)。
execute 有回显命令执行

- 使用 Java 反射调用
Runtime.getRuntime().exec()。 - 命令被 base64 编码,并通过
tr '+/' '_-'转换为 URL-safe 字符(避免模板解析问题)。 - 执行
bash -c 'eval $(echo ... | base64 -d)'解码并运行。 - 读取
InputStream逐字符拼接输出(因 Velocity 不支持直接 toString 流)。 - 最终
${output}回显结果。
execute_error 通过报错回显

execute_boolean 命令执行布尔盲注(基于退出码判断0执行成功/非0执行失败)

execute_blind 延时盲注,通过响应时间判断命令执行结果

write 写文件

Thymeleaf
demo
pom
1 | |
index.html
1 | |
1 | |

语法和指令
表达式
| 变量表达式 | ${...} |
访问上下文变量(如 Model 中的数据) |
|---|---|---|
| 选择表达式 | *{...} |
针对已选定对象(通常配合 th:object) |
| 消息表达式 | #{...} |
国际化消息(i18n) |
| 链接表达式 | @{...} |
构建 URL(支持参数、协议、相对路径等) |
| 片段表达式 | ~{...} |
引用模板片段(用于 th:insert, th:replace) |
- 变量表达式
${...}
用于访问上下文中的变量(如 Controller 传入的 Model 数据)。
1 | |
- 选择表达式
*{...}
用于在已通过 th:object 指定的对象上进行属性访问(相当于“当前对象”的快捷方式)。
1 | |
- 消息表达式
#{...}
用于国际化(i18n),从 message properties 文件中读取对应语言的消息
1 | |
- 链接表达式
@{...}
用于构建 URL,支持相对/绝对路径、参数、协议等。
1 | |
- 片段表达式
~{...}
用于引用模板片段,常与 th:insert、th:replace、th:include(已弃用)配合使用。
1 | |
指令
格式**th:***
th:text
替换元素的文本内容(自动转义 HTML 特殊字符)。
1 | |
th:utext(Unescaped Text)
替换元素的文本内容,不转义 HTML(用于渲染富文本)。
1 | |
th:attr
动态设置任意 HTML 属性(可一次设置多个)。
1 | |
更推荐使用专用属性指令(如 th:src),除非需要动态属性名。
这些是 th:attr 的快捷方式,更清晰安全:
| 指令 | 作用 |
|---|---|
th:src |
设置 <img>, <script> 的 src |
th:href |
设置 <a>, <link> 的 href |
th:value |
设置 <input>, <option> 的 value |
th:title |
设置 title属性 |
th:id |
设置 id |
th:class |
设置 class(会覆盖原 class) |
th:style |
设置 style |
th:alt, th:placeholder, th:onclick等 |
几乎所有标准 HTML 属性都支持 |
1 | |
th:classappend/th:styleappend
在保留原有 class/style 的基础上追加新值。
1 | |
th:if
条件为 true 时渲染元素。
1 | |
th:unless
条件为 false 时渲染(即 if not)。
1 | |
th:switch/th:case
多分支选择(类似 Java switch)。
1 | |
th:each
遍历集合、数组、Map 等。
1 | |
th:insert
插入片段内容到当前标签内部(保留宿主标签)。
1 | |
th:replace
用片段完全替换当前标签。
1 | |
th:include( 已弃用)
旧版用法,Thymeleaf 3+ 建议用 th:insert 或 th:replace。
th:object
指定表单绑定的命令对象(通常为 ModelAttribute)。
1 | |
th:field
绑定表单字段到 th:object 指定的对象属性(自动处理 name、value、errors)。
1 | |
th:errors
显示字段的验证错误信息。
1 | |
th:with
定义局部变量(作用域限于当前元素及其子元素)。
1 | |
th:assert
断言,用于调试或确保条件成立(失败时抛异常)。
1 | |
th:remove
移除元素(可用于条件移除、占位清理等)。
1 | |
内联表达式(Inline)
虽然不是 th: 指令,但密切相关:
内联文本 [[...]](等价于 th:text)
1 | |
需启用内联(默认开启),或显式声明:
1 | |
内联 JavaScript [(...)](等价于 th:utext,常用于 JS 变量)
1 | |
需开启内联:th:inline="text" 或 th:inline="javascript"
攻击面
thymeleaf的ssti实际上注入的是SpEL表达式(Spring Expression Language)。
代码审计重点:
- thymeleaf<3.0.14
- thymeleaf通过return来寻找模板,需要return 参数可控
- 或者没有return而通过url路径来寻找模板
3.0.11
1 | |
1 | |

1 | |

3.0.12
pom
1 | |
在这个版本中在<font style="color:rgb(51, 51, 51);">util</font>目录添加了<font style="color:rgb(51, 51, 51);">SpringStandardExpressionUtils.java</font>文件
其主要逻辑是首先 倒序检测是否包含<font style="color:rgb(51, 51, 51);">wen</font>关键字、在<font style="color:rgb(51, 51, 51);">(</font>的左边的字符是否是<font style="color:rgb(51, 51, 51);">T</font>,如包含,那么认为找到了一个实例化对象,返回<font style="color:rgb(51, 51, 51);">true</font>,阻止该表达式的执行
因此绕过这个函数的检测的方法:
1、表达式中不能含有关键字<font style="color:rgb(51, 51, 51);">new</font>
2、在<font style="color:rgb(51, 51, 51);">(</font>的左边的字符不能是<font style="color:rgb(51, 51, 51);">T</font>
3、不能在<font style="color:rgb(51, 51, 51);">T</font>和<font style="color:rgb(51, 51, 51);">(</font>中间添加的字符使得原表达式出现问题
可以在<font style="color:rgb(51, 51, 51);">T</font>``<font style="color:rgb(51, 51, 51);">(</font>之间使用绕过的字符
<font style="color:rgb(51, 51, 51);">%20</font><font style="color:rgb(51, 51, 51);">%0a</font><font style="color:rgb(51, 51, 51);">%09</font><font style="color:rgb(51, 51, 51);">%0d</font><font style="color:rgb(51, 51, 51);">%00</font>
1 | |
在用url路径视图的时候
1 | |
3.0.13
改动在<font style="color:rgb(77, 77, 77);">SpringStandardExpressionUtils#containsSpELInstantiationOrStatic</font>
1 | |
对”T”的前一个字符会检测是否满足<font style="color:rgb(77, 77, 77);">Character#isJavaIdentifierPart</font>
1 | |
payload
1 | |
不拼接路径的话会报错
1 | |

下面的方式也可用
1 | |
url方式
1 | |
3.0.14
Thymeleaf3.0.14版本的修复,对<font style="color:rgb(68, 68, 68);">containsSpELInstantiationOrStatic()</font>函数检测进行了完善,对<font style="color:rgb(68, 68, 68);">T</font>和<font style="color:rgb(68, 68, 68);">()</font>中间空字符进行绕过修复
payload
1 | |
url方式
1 | |