Python开源框架Tornado某缺陷可能造成文件读取漏洞

最近在筹备写一篇关于“任意文件读取”的文章( 新型任意文件读取漏洞的研究 )。。。没想到写文章的过程中发现tornado的一个小问题,提出来希望引起大家的重视。

首先,tornado是一个全异步的框架,它有有一个专门处理静态文件的控制器,名字叫StaticFileHandler,在文档(http://www.tornadoweb.org/en/stable/web.html#tornado.web.Application)中可以得知:

QQ20150301-8@2x.png

我们只要指定一个目录对应到这个控制器中,即可在HTTP请求中直接请求到这个目录下的文件。
而且,tornado在setting中,也可以直接指定一个static_path,来说明静态文件放在哪个目录下:

QQ20150301-9@2x.png

那么我们直接吧example拿来,增加一个static_path的设置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/python
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")

if __name__ == "__main__":
setting = {
"debug": True,
"static_path": "/Users/phithon/pro/python/wooyun/tornado-file-read/static/",
}

application = tornado.web.Application([
(r"/", MainHandler),
], **setting)
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()

这时候我们就可以直接请求到/static/01.txt这样的静态文件了:

QQ20150301-10@2x.png

正常情况下,我们是不能够请求到这个目录以外的文件的,如/etc/passwd:

QQ20150301-11@2x.png

如上图,tornado对请求的路径进行了一定判断,抛出了这个错误。然后我们去他源码中看看是怎么判断的:

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
34
35
36
37
38
39
40
def validate_absolute_path(self, root, absolute_path):
"""Validate and return the absolute path.

``root`` is the configured path for the `StaticFileHandler`,
and ``path`` is the result of `get_absolute_path`

This is an instance method called during request processing,
so it may raise `HTTPError` or use methods like
`RequestHandler.redirect` (return None after redirecting to
halt further processing). This is where 404 errors for missing files
are generated.

This method may modify the path before returning it, but note that
any such modifications will not be understood by `make_static_url`.

In instance methods, this method's result is available as
``self.absolute_path``.

.. versionadded:: 3.1
"""
root = os.path.abspath(root)
# os.path.abspath strips a trailing /
# it needs to be temporarily added back for requests to root/
if not (absolute_path + os.path.sep).startswith(root):
raise HTTPError(403, "%s is not in root static directory",
self.path)
if (os.path.isdir(absolute_path) and
self.default_filename is not None):
# need to look at the request.path here for when path is empty
# but there is some prefix to the path that was already
# trimmed by the routing
if not self.request.path.endswith("/"):
self.redirect(self.request.path + "/", permanent=True)
return
absolute_path = os.path.join(absolute_path, self.default_filename)
if not os.path.exists(absolute_path):
raise HTTPError(404)
if not os.path.isfile(absolute_path):
raise HTTPError(403, "%s is not a file", self.path)
return absolute_path

关键代码是

1
2
root = os.path.abspath(root),
if not (absolute_path + os.path.sep).startswith(root):

os.path.abspath 获取root变量指定的绝对路径。

root 实际上就是我们之前传入的setting中的“static_path”,这里获得静态文件目录的绝对路径。但在python中,os.path.abspath这个函数获得的路径,是没有结尾处的”/“的。

也就是说,我传入的路径是“/Users/phithon/pro/python/wooyun/tornado-file-read/static/”,经过这个函数处理,变成了“/Users/phithon/pro/python/wooyun/tornado-file-read/static”。

好,接下来这个if语句,absolute_path + os.path.sep就是我请求的静态文件路径,一旦它不是以root开头的话,就会爆403错误。

我们想想可能会出现什么漏洞?
因为root这个变量是没有最后那个“/“的,那么如果我有一个文件是“/Users/phithon/pro/python/wooyun/tornado-file-read/static.db”,或一个目录是“/Users/phithon/pro/python/wooyun/tornado-file-read/static_private/”,那是不是都以“/Users/phithon/pro/python/wooyun/tornado-file-read/static”开头的?但这些文件并不在static这个目录下,所以并没有触发403错误。

这种情况下造成了一个文件读取漏洞,我们读取到了本不应该被读取的某些文件。

举个例子吧,有这么一个应用(下载链接: http://.../s/1dD6cLWH 密码: gal2),如下是目录结构:

QQ20150301-12@2x.png

有一个数据库叫static_private.sqlite3,有个目录叫static_private,里面放着敏感信息私钥private.key。

我们通过如下请求把他们都读取了:

QQ20150301-13@2x.png

QQ20150301-14@2x.png

所以在特殊情况下,如果开发者指定的静态目录为xxx,那么我们就可以读取所有xxx上层的目录下的名字开头为xxx的文件、目录。

这个其实和设置open_basedir的时候没有带最后一个“/”产生的效果类似。

QQ20150301-13@2x.png

QQ20150301-14@2x.png