前言 前段时间发现较为鸡肋的洞和当时写的文章。
一个存在于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&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而造成问题。
这样思路就清晰了:
我们需要在Flask的WebApp产生一个异常,以至于它能返回Debugger页面。
我们需要一个异常信息内包含着我们所构造数据的异常,以便我们构造payload字符串
很巧,把字符串转换成数字的常用函数int()就拥有这样的特性。
于是我们写了一个小Demo:
1 2 3 4 5 6 7 8 9 10 from flask import Flask, requestapp = 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"