一个为了解决读取文件异常对文件路径使用unquote函数decode所引发的任意文件读取血案。

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把应用运行起来point

然后访问http://localhost:8787/static/vulbox程序进入这个流程,往下跟发现Sanic判断了../是否存在于file_uri中,这样看似就能阻止我们穿越目录去访问别的文件。

logic

继续往下走sub('^[/]*', '', file_uri)这里对file_uri进行了处理就是将开头/的字符全部替换为'',下面一行就是造成此次漏洞的关键代码了。

unquote函数对file_path进行了 urldecode 可以将%20%2f等字符还原成原字符。因为有这一行我们就可以为所欲为了。

unquote_example

0x03 漏洞利用

分析好了代码,接着我们就利用吧这次我们访问http://localhost:8787/static/..%2f..%2f..%2ftmp%2ftest在通过ide去debug看下整个过程。传入的file_urialt

在这一步因为前面没有decode所以绕过了检查限制没有抛出错误。

variables

通过subpath_join函数这时的文件路径为/var/tmp/..%2f..%2f..%2ftmp%2ftest,如果此时直接交给File response handler进行处理就会抛出FileNotFound

file_uri

机智的程序员为了防止这种情况,用了unquote函数对file_path进行了 decode,经过转换后file_path就变为了/var/tmp/../../../tmp/test

unquote_file_uri

最后通过response.py中的file函数将文件返回。

out_stream

成功目录穿越读取到别的文件。

pwned

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')
...
本文版权归漏洞盒子所有