前言


前段时间发现较为鸡肋的洞和当时写的文章。

一个存在于Flask框架Debugger页面上的通用XSS,Werkzeug0.11.10之前版本受影响,已经报告给Flask官方并提交修复代码。官方在确认之后,及时发布了0.11.11。

记下了发现的过程。

正文

看了一段时间Flask的源码,想学习一下项目架构和一些感兴趣的实现,其中就包括Flask功能强大的Debugger页面。用过Flask的人都知道,Flask的Debug模式能帮助我们在开发Web应用时跟踪异常信息,调试代码,解决问题。

在使用Debugger的时候,我就在想这个页面难道也是使用render_template或其他Flask模板调用函数生成的吗?稍微翻了一下代码,发现这个功能既没有使用Jinja2模板,甚至它的主要代码写在Flask的另一个基础库werkzeug上面。

Werkzeug是Flask官方开发的一个WSGI工具箱,可以作为一个Web框架的底层库。事实上Flask就是基于Werkzeug和Jinja2开发的一个Web框架。

我以前曾经在记一下PythonWeb代码审计应该注意的地方提及PythonWeb开发中容易容易产生XSS的几种情况,其中提到:

如果webapp没有使用模板语言的话,又没有对用户输入进行过滤直接返回给客户端的话,就容易产生XSS。

Flask的程序员显然不会犯这样的错误,我可以看一下werkzeug/debug/tbtools.py这个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
PAGE_HTML = HEADER + u'''\
<h1>%(exception_type)s</h1>
<div class="detail">
<p class="errormsg">%(exception)s</p>
</div>
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
%(summary)s
<div class="plain">
<form action="/?__debugger__=yes&amp;cmd=paste" method="post">
<p>
<input type="hidden" name="language" value="pytb">
This is the Copy/Paste friendly version of the traceback. <span
class="pastemessage">You can also paste this traceback into
a <a href="https://gist.github.com/">gist</a>:
<input type="submit" value="create paste"></span>
</p>
<textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea>
</form>
</div>
<div class="explanation">
The debugger caught an exception in your WSGI application. You can now
look at the traceback which led to the error. <span class="nojavascript">
If you enable JavaScript you can also use additional features such as code
execution (if the evalex feature is enabled), automatic pasting of the
exceptions and much more.</span>
</div>
''' + FOOTER + '''
<!--

%(plaintext_cs)s

-->
'''

可以发现Debugger页面是以字符串拼接和字符串格式化的形式构成的。这个字符串可以传进五个变量,分别是exception_type, exception, summary, plaintext, plaintext_cs。看变量名就可以知道,应该是一些异常信息。这些信息由render_full函数填充进字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def render_full(self, evalex=False, secret=None,
evalex_trusted=True):
"""Render the Full HTML page with the traceback info."""
exc = escape(self.exception)
return PAGE_HTML % {
'evalex': evalex and 'true' or 'false',
'evalex_trusted': evalex_trusted and 'true' or 'false',
'console': 'false',
'title': exc,
'exception': exc,
'exception_type': escape(self.exception_type),
'summary': self.render_summary(include_title=False),
'plaintext': self.plaintext,
'plaintext_cs': re.sub('-{2,}', '-', self.plaintext),
'traceback_id': self.id,
'secret': secret
}

代码里使用了escape函数过滤了异常信息和异常类型,但是这两行代码引起了我注意。

1
2
'plaintext':        self.plaintext,
'plaintext_cs': re.sub('-{2,}', '-', self.plaintext),

self.plaintext依然包含着异常信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def generate_plaintext_traceback(self):
"""Like the plaintext attribute but returns a generator"""
yield u'Traceback (most recent call last):'
for frame in self.frames:
yield u' File "%s", line %s, in %s' % (
frame.filename,
frame.lineno,
frame.function_name
)
yield u' ' + frame.current_line.strip()
yield self.exception

def plaintext(self):
return u'\n'.join(self.generate_plaintext_traceback())
plaintext = cached_property(plaintext)

plaintext_cs是放在html注释内的完整异常信息,为了避免异常内出现-->闭合之前的注释符,这里会把重复的-替换为一个-,但是plaintext没有经过任何处理,plaintext放在一个textarea里,显然我们的Flask程序员们没有想到异常信息会闭合textarea而造成问题。

这样思路就清晰了:

  1. 我们需要在Flask的WebApp产生一个异常,以至于它能返回Debugger页面。
  2. 我们需要一个异常信息内包含着我们所构造数据的异常,以便我们构造payload字符串

很巧,把字符串转换成数字的常用函数int()就拥有这样的特性。

于是我们写了一个小Demo:

1
2
3
4
5
6
7
8
9
10
from flask import Flask, request
app = Flask(__name__)

@app.route("/xss-debug-test/", methods=['POST'])
def xss_debug_test():
id = int(request.form['id'])
return "XSS"

if __name__ == "__main__":
app.run(debug=True)

接下来的活就是普通的XSS构造payload了。只要闭合前面的textarea标签即可。

例如:

</textarea><script>alert(/XSS/)</script>

id=</textarea><script>alert(document.cookie)</script>

写了一个脚本稍微跑了一下,int()的这个异常最多含有200个字符串值,如果传入的字符串长度大于200,就会截取前200位。然而200位的payload已经足够我们构造所有xss利用代码了。

修复

我自己提交的修复代码,官方也采用了,只是加了转义函数而已。现在的话,只要运行pip install -U werkzeug,把werkzeug更新到0.11.11版本就没事了。当然更重要的是,任何框架的debug模式都不要放到生产环境。

待续

我当时还有几个小想法,一个就是想思考如何通过一个简单的脚本fuzz获取看哪些常见的字符串处理函数其产生的异常,是带有字符串值的,另一个就是经P牛提醒如果可以构造Payload的去获取pin码的话,就可直接代码执行了。

但是一来是Debug模式在线上确实不多见,所以这个洞影响也不是特别大。二来也要忙着审计php和找工作就没再花时间在这上面了。等有时间再好好弄弄。

参考

https://github.com/pallets/werkzeug/commits?author=neargle

本文URL: https://blog.neargle.com/2016/09/21/flask-src-review-get-a-xss-from-debuger/index.html