在本文中我们会乱弹Flask框架中的安全特性以及一些潜在的问题,例如服务端模版注入,跨站脚本,HTML属性注入攻击。如果你还没有体验过Flask带来的乐趣,建议可以先用用。Flask是一个使用Python编写的强大的轻量级Web应用框架(毕竟是Python)。
卧谈注入
对于表示层,Flask利用 Jinga2 引擎,其使用方便,自动转义.html,htm,xml以及.xhtml文件中的内容。Flask允许在Python源代码中使用HTML字符串创建模版,Flask内部使用本地线程对象,这样就可以不用为了线程安全的缘故在同一个请求中在函数之间传递对象。
服务端模版注入
Flask框架中提供的模版引擎可能会被一些无量开发者利用引入一个服务端模版注入漏洞,如果对此感到有些困惑可以看看James Kettle在黑帽大会中分享的 议题 (PDF),简而言之这个漏洞允许将语言/语法注入到模板中。在服务器的context中执行这个输入重现,根据应用的context可能导致任意远程代码执行(远端控制设备)
接下来为我们就看看使用模板字符串功能如何探索安全问题,思考下面的代码片段:
from flask import Flask, request, render_template_string, render_templateapp = Flask(__name__)@app.route('/hello-template-injection')def hello_ssti(): person = {'name':"world", 'secret':"UGhldmJoZj8gYWl2ZnZoei5wYnovcG5lcnJlZg=="} if request.args.get('name'): person['name'] = request.args.get('name') template = '''<h2>Hello %s!</h2>''' % person['name'] return render_template_string(template, person=person)##### Private function if the user has local files.###def get_user_file(f_name): with open(f_name) as f: return f.readlines()app.jinja_env.globals['get_user_file'] = get_user_file # Allows for use in Jinja2 templatesif __name__ == "__main__": app.run(debug=True)
接着浏览应用
非常好,虽然我不是一位大逗逼,但是我也起了一些不错的昵称,挑一个我们来试试:
Ryan. {{person.secret}}
凹不,我们的代码已经把密码泄漏出来!毫无疑问这是一件非常糟糕的事情。保险起见,我们再试试另一个payload。最终得知这些隐私信息存储在tmp目录,get_user_file方法看起来就更有趣了,尝试下:
Ryan. {{ get_user_file("/tmp/secrets.txt") }}
通过模版进行本地文件包含(LFI)?这太吓人了!由于使用字符串连接或替换出现这个问题,如果你是一位Flask开发者,你可能已经知道答案。Jinja2在模版中使用花括号{{}}包围环境变量,通过将我们的输出放置到这些括号内,可以阻止用户输入包含模版语法的数据在服务器的context中执行。
在修复后的适当位置尝试整行读取:
template = '<h2>Hello {{ person.name }}!</h2>'
这样做勉强降低了服务端模板注入的威胁。
跨站脚本
如上所述,Flask对某些文件提供了一个自动转义的特性。
虽然这个特性非常不错,但是我们还是要提醒你:
1.模版可以禁用该特性 2.模板字符串非公共文件扩展名默认情况下是不启用自动转义功能的
尝试使用一个常见的XSS测试字符串:
Ryan. <script>alert("Full protection not yet achieved.")</script>
还记得我说过模版字符串不会自动转义么?现在可以理解了吧。为了修复这个问题,我们可以通过手工绕过输出过滤
,加上|e就可以保证在过滤进行之前就反馈给用户。所以我们最终的模版字符串应该是这样的:
template = '<h2>Hello {{ person.name | e }}!</h2>'
并非所有的应用都在使用on-the-fly模版,那么更传统的跨站脚本攻击是在静态模版中?
思考下面的函数:
def hello_xss(): name = "world" template = 'hello.unsafe' # 'unsafe' file extension... totally legit. if request.args.get('name'): name = request.args.get('name') return render_template(template, name=name)
注意:Python代码在模版中调用render_template,这不是一个会自动转义的文件扩展。根据模版中的代码hello.unsafe,我们可能得到了一个跨站脚本漏洞,以下为模版代码:
{% autoescape true %} <h2>Good</h2><p> Hello {{ name }}! I don't trust your input. I escaped it, just in case.</p>{% endautoescape %}<h2>Bad</h2><p> I trust all data! How are you {{ name }}? </p>
测试下:
自动转义模块和预期一般正常工作;我们对输出进行适当转义。然而,第二部分允许payload注入在浏览器中执行。
"Good"部分利用Jinga2引擎的自动转义功能,我们也可以利用|e过滤。以下为在"Bad"部分中使用|e过滤的输出
附注:Flask中转移输出
转义函数对于防护HTML属性注入没啥卵用,以下面代码为例:
def hello_hi(): template = '''<title>No Injection Allowed!</title> <a href={{ url_for('hello_xss')}}?name={{ name |e}}> Click here for a welcome message</a>''' name = "world" if request.args.get('name'): name = request.args.get('name') return render_template_string(template, name=name)
我们可以看到,变量周围都是{{}},使用|e绕过输出过滤
现在,鼠标在链接周围移动
哦哦,payload执行了。首先这里有一个问题:由于name参数出现在HTML属性的context,我们的payload成功执行。
其次,单/双引号中的context属性输出封装可以解决这个问题,以下为更新后的链接标签:
<a href='{{ url_for('hello_xss')}}?name={{ name |e}}'>...</a>
尾声
我们已经看到了一些Flask中提供的特性进行输出过滤绕过,潜在的安全问题,以及如果你遇到这些情况该怎么修复。
谨记一定要对输出进行验证!
*参考来源: nvisium ,编译/FB小编鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)