0x00 Sanic是什么
一个基于 Python3.5+ 的异步(asyncio+uvloop)web框架,与Flask有点相似,特点就是非常的快,每秒钟可处理30k请求。
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
▌ ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
▀▀█████████▀
▄▄██▀██████▀█
▄██▀ ▀▀▀ █
▄█ ▐▌
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
▌ ▐ ▀▀▄▄▄▀
▀▀▄▄▀
0x01 复现环境及漏洞影响版本
Python 3.6
Sanic <= 0.5.0
macOS 10.12.4
0x02 漏洞分析
首先看下一个Sanic
处理静态文件的一个示例代码,第九行对static
的路由进行了注册。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from sanic import Sanic
from sanic.response import text
app = Sanic()
app.static('/static', '/var/tmp')#这里注册了static url为/static 实际物理路径为/var/tmp
@app.route("/")
async def index(request):
return text('Hello!')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8787, debug=True)
我们直接看static.py中的静态文件处理逻辑,在_handler
函数下个断点接着debug把应用运行起来
然后访问http://localhost:8787/static/vulbox
程序进入这个流程,往下跟发现Sanic
判断了../
是否存在于file_uri中,这样看似就能阻止我们穿越目录去访问别的文件。
继续往下走sub('^[/]*', '', file_uri)
这里对file_uri进行了处理就是将开头/的字符全部替换为'',下面一行就是造成此次漏洞的关键代码了。
unquote
函数对file_path进行了 urldecode 可以将%20
、%2f
等字符还原成原字符。因为有这一行我们就可以为所欲为了。
0x03 漏洞利用
分析好了代码,接着我们就利用吧这次我们访问http://localhost:8787/static/..%2f..%2f..%2ftmp%2ftest
在通过ide去debug看下整个过程。传入的file_uri
为
在这一步因为前面没有decode所以绕过了检查限制没有抛出错误。
通过sub
和path_join
函数这时的文件路径为/var/tmp/..%2f..%2f..%2ftmp%2ftest
,如果此时直接交给File response handler进行处理就会抛出FileNotFound
。
机智的程序员为了防止这种情况,用了unquote
函数对file_path
进行了 decode,经过转换后file_path
就变为了/var/tmp/../../../tmp/test
。
最后通过response.py
中的file
函数将文件返回。
成功目录穿越读取到别的文件。
0x04 如何修复
大多数修复手段都是通过对../
字符进行替换,或者判断路径中是否有../
字符,这样的修复手段还是存在安全隐患。
比较彻底的修复方式是在读取文件之前对最终路径的abspath进行判断,如果传入的abspath与注册的不一致则发生了目录穿越。
修复代码示例
#!/usr/bin/python
# -*- coding:utf-8 -*-
import os
#root_path注册的static文件路径
root_path = file_path = '/opt/sanic/static/'
#file_uri用户输入的路径
file_uri = request.args.get('file', None)
if file_uri:
file_path = os.path.join(root_path, sub('^[/]*', '', file_uri))
file_path = os.path.abspath(file_path)
if not file_path.startwith(root_paht):
raise FileNotFound('File Not Found')
...