<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/papers/5040

            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某個特殊情況的漏洞,但現代的開發技術,包括企業級的一些環境我并不熟悉也沒怎么接觸過,所以有什么欠考慮和不完善的地方,也需要各位去補充與糾正。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线