新型任意檔案讀取漏洞的研究

wyzsk發表於2020-08-19
作者: phith0n · 2015/03/04 9:21

0x00 前言


早前發現boooom在烏雲上發了很多個任意檔案讀取的漏洞,都是形如

http://target/../../../../etc/passwd

這樣。當時感覺很新奇,因為正常情況下,通常的伺服器中介軟體是不允許直接讀取web目錄以外的檔案的,為什麼這樣的漏洞卻出現在了很多案例中。

後來在lijiejie的文章給出瞭解釋:http://www.lijiejie.com/python-django-directory-traversal/ ,原來是python這種新型web開發方式造成的問題。然後翻了下我自己以前用web.py、tornado開發的一些應用,果然也存在這樣的問題。

這個問題就像lijiejie說的那樣,一方面是低版本django框架自身的一些漏洞,另一方面,就是開發者自身的疏忽造成的問題。

這不得不提到現今開發框架與以前的一些區別。不管是python還是node、ruby的框架,都是一個可以自定義URL分配的框架,不再是像php或asp中那樣根據目錄結構來請求檔案。所有的請求由使用者定義規則,而框架核心部分解析、配發、執行。比如我們請求的“/login/”這個URL,很可能是被配發給一個LoginHandler類去處理了,而不是請求到/login/index.php上。

這時候造成了一個問題,如果我們就是想去請求一個真實的檔案,比如css、js等靜態檔案,怎麼辦?

一般也會有一些區分,一些要求比較高的應用,多是採用了CDN快取或負載均衡,nginx作為負載分配的處理器。當發現我們請求的url是一個靜態檔案的話,就直接由CDN或nginx返回相應檔案。如下圖:

enter image description here

那麼這之中也存在這一個定義問題,什麼請求才說明是要請求“靜態檔案”?只要以.css、.js結尾就可以嗎?當然這也是一種方法,但一般應用會定義一個目錄,如/static/,所有請求匹配“/static/(.*)”的會被認為是靜態檔案,所以開發者一般將靜態檔案放在這個目錄下,我們使用者就能夠直接請求到他了。

如果不存在CDN、nginx等平臺,其實類似web.py、tornado這樣的框架自己也定義了靜態目錄,在web.py下,預設的靜態目錄都是/static/,也就是在這個目錄下的請求是不會經過URLPath的。如web.py文件中說到的:

http://webpy.org/cookbook/staticfiles.zh-cn

這時候,我就會有這個思考,框架內部如果是以/static/(.*)來匹配請求的話,如果我們求

/static/../../../../../etc/passwd

是不是就可以讀取到/etcs/passwd檔案?

0x01 web.py下可能的任意檔案讀取漏洞研究


我們先來看看web.py是怎樣處理這種請求的:

/static/../../../../../etc/passwd

我們在web/httpserver.py中可以看到這樣的程式碼:

#!python
def do_GET(self):
    if self.path.startswith('/static/'):
        SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
    else:
        self.run_wsgi_app()

當請求以/static/開頭的話,就直接交給SimpleHTTPServer處理了。SimpleHTTPServer是python自帶的一個簡單的HTTP Server,我們在任意一個目錄下執行

python -m SimpleHTTPServer

都會啟動一個web伺服器,可以直接透過HTTP協議訪問目錄下的檔案。

web.py的Server其實就是對SimpleHTTPServer的一個繼承與重寫,這裡它簡單的把這個請求交給父類SimpleHTTPServer處理,而這個HTTP Server當然不會允許請求到web目錄(也就是./static/)以外的地方去,所以得到的回覆是404:

enter image description here

框架本身保證了靜態檔案不會造成任意檔案讀取。但複雜的邏輯關係應用中,開發者往往不滿足於/static/一種靜態目錄。比如,網站允許使用者上傳、下載檔案,可能我們會新建一個uploadfile目錄,按日期、時間專門儲存上傳的檔案。

那麼,開發者為了讓/uploadfile目錄下的檔案也能被直接訪問,往往會這樣寫:

#!python
#!/usr/bin/python
import web

urls = (
    '/uploadfile/(.*)', 'download',
    '/', 'hello',
)
app = web.application(urls, globals())

class hello:        
    def GET(self, name):
        if not name: 
            name = 'World'
        return 'Hello, ' + name + '!'

class download:
    def GET(self, filepath):
        try:
            with open("./uploadfile/%s" % filepath, "rb") as f:
                content = f.read()
            return content
        except:
            return web.notfound("Sorry, the file you were looking for was not found.")

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

有個download類專門解析這類請求,直接在GET方法中讀取檔案,並作為response寫入HTTP資料包。

我們請求一個正常的檔案/uploadfile/01.txt,是可以得到它的內容的:

enter image description here

但我們請求一個非uploadfile目錄下的檔案,卻發現也能讀取,導致了一個任意檔案讀取漏洞:

enter image description here

這就是由於開發者的失誤,並沒有檢查我們傳入的path是否合法而導致,與框架無關。

0x02 Tornado下可能的任意檔案讀取漏洞研究


tornado是一個全非同步的web框架,它允許我們在配置中定義靜態目錄static_path。在tornado中,專門給出了一個方法來驗證我們的請求是否在允許的目錄內:

#!python
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

這樣,一旦請求不在我們定義的靜態目錄下,就會丟擲“is not in root static directory”錯誤:

enter image description here

那麼如果tornado中,也需要定義一個"/uploadfile/"作為使用者上傳目錄,那麼我們怎麼做?

文件中也提到了,我們只需要自定義一個URLPath即可,tornado內部有專門處理靜態檔案的控制器web.StaticFileHandler:

#!python
application = web.Application([

    (r"/uploadfile/(.*)", web.StaticFileHandler, {"path": "/var/www"}),

])

就算不考慮安全問題,作為一個非同步的框架,如果我們還用同步的read、write這些IO函式自己去處理靜態檔案,也是不可取的。

不過,在後面的研究中,我也發現了tornado的處理方式並不算完美,更多詳情可以等這個洞公開後檢視:

WooYun: Python開源框架Tornado某缺陷可能造成檔案讀取漏洞

0x03 Django中的問題


Django低版本自身存在的漏洞導致的任意檔案讀取,實際上就是犯了我之前說的靜態檔案未檢查的問題,如這個09年的BUG:

https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2009-2659

enter image description here

正如我在web.py中說到的,如果django也單純使用open、read來讀取檔案,而不檢查PATH的合法性,同樣能夠造出任意檔案讀取。

比如我們django的view如此寫:

#!python
from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def index(request, path):
    with open(path, "rb") as f:
        content = f.read()
    return HttpResponse(content)

沒有驗證path的合法性,依舊可以出現任意檔案讀取的現象:

enter image description here

如上圖,直接讀取了sqlite資料庫內容,拿下管理員賬號密碼。

那麼我們怎麼在這樣的應用中防禦任意檔案讀取?我們簡單修改django的view防禦這個漏洞:

#!python
from django.shortcuts import render
from django.http import HttpResponse
from django.http import Http404
import os

# Create your views here.

def index(request, path):
    static = "uploadfile"
    root = os.path.join(os.getcwd(), static) + os.path.sep
    path = os.path.abspath(root + path)
    if not (path).startswith(root):
        raise Http404
    else:
        with open(path, "rb") as f:
            content = f.read()
        return HttpResponse(content)

其實有些框架都有自己檢查靜態檔案的方式,django自身應該也有的。像web.py這樣的輕型框架沒有自帶函式檢查的情況下,可以考慮用我上面寫的這個方法來剔除不合法的靜態檔案路徑。

0x04 PHP等語言會不會出現這個問題?


這個問題其實是有思考價值的。

最開始我就提到了,現代的python/node/ruby等web開發框架與老式的php、asp等語言的區別,也是造成這個漏洞的原因之一就是因為URL的分配導致靜態檔案不能被直接訪問到,所以需要自定義靜態檔案的訪問方式。但一旦訪問引數未檢查,就造成任意檔案讀取問題。

但傳統php應用就是一個以目錄形式訪問的,靜態檔案訪問應該不會經過php的,這確實是一個很大的區別。

先不論我們的請求會不會經過php,看到zblog最新版中一個實際的案例。

/zb_system/function/c_system_event.php

大概415行:

#!php
if (isset($_SERVER['SERVER_SOFTWARE']) && (strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) && (isset($_GET['rewrite']) == true)){
    //iis+httpd.ini下如果存在真實檔案
    $realurl = $zbp->path . urldecode($url);
    if(is_readable($realurl)&&is_file($realurl)){
        die(file_get_contents($realurl));
    }
    unset($realurl);
}

這裡判斷了

$_SERVER['SERVER_SOFTWARE']

是否包含Microsoft-IIS,當伺服器中介軟體是IIS,而且

$_GET['rewrite']

的話,就進入這個if語句。再判斷$url指向的檔案是否存在,存在就把它使用file_get_contents讀取並顯示出來。

實際上這段程式碼所做的工作和我們之前看到的python程式碼是一樣的。為什麼php也需要這樣的工作,我們url請求的檔案不應該就直接由webserver返回給使用者了嗎?

實際上,這裡開發者也考慮了url重寫造成的問題。zblog重寫的規則是將所有請求都指向index.php去處理,最後由index.php去處理,有可能我們的靜態檔案就被rewrite到index.php去了,這裡的工作就是把被重寫到php裡的這個靜態檔案直接顯示出來。

可惜我還是才疏學淺,不知道怎麼寫rewrite規則才能讓靜態檔案請求被重寫到index.php裡,所以也做不到任意檔案讀取了。

但我們這裡很明顯的可以發現,zblog的開發者也沒有檢查這個$url是否在網站目錄內,或是否在靜態檔案目錄內,也是直接讀取顯示了。導致我們請求http://10.211.55.3/zblog/index.php?action=&rewrite=1是可以讀取index.php的原始碼的(因為我沒有IIS環境,我手工將SERVER_SOFTWARE改成IIS了~):

enter image description here

所以,傳統PHP應用下是否可能存在這樣的安全漏洞,這個問題還是有待繼續研究的。理論上,我們如果寫出一個這樣的rewrite規則:將所有請求都交給index.php處理。那麼,index.php的功能實際上就和之前的python框架主檔案功能類似了。

0x05 結語


最後,自己也只是淺顯得研究和討論了幾種python框架、php某個特殊情況的漏洞,但現代的開發技術,包括企業級的一些環境我並不熟悉也沒怎麼接觸過,所以有什麼欠考慮和不完善的地方,也需要各位去補充與糾正。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章