作者:phith0n@長亭科技

Github賬號被封了以后,Vulhub也無法繼續更新了,余下很多時間,默默看了點代碼,偶然還能遇上一兩個漏洞,甚是有趣。

這個漏洞出現在python核心庫http中,發送給官方團隊后被告知撞洞了,且官方也認為需要更多人看看怎么修復這個問題,所以我們來分析一下。

0x01 http.server庫簡單分析

眾所周知Python有一個一鍵啟動Web服務器的方法:

python3 -m http.server

在任意目錄執行如上命令,即可啟動一個web文件服務器。其實這個方法就用到了http.server模塊。這個模塊包含幾個比較重要的類:

  1. HTTPServer這個類繼承于socketserver.TCPServer,說明其實HTTP服務器本質是一個TCP服務器
  2. BaseHTTPRequestHandler,這是一個處理TCP協議內容的Handler,目的就是將從TCP流中獲取的數據按照HTTP協議進行解析,并按照HTTP協議返回相應數據包。但這個類解析數據包后沒有進行任何操作,不能直接使用。如果我們要寫自己的Web應用,應該繼承這個類,并實現其中的do_XXX等方法。
  3. SimpleHTTPRequestHandler,這個類繼承于BaseHTTPRequestHandler,從父類中拿到解析好的數據包,并將用戶請求的path返回給用戶,等于實現了一個靜態文件服務器。
  4. CGIHTTPRequestHandler,這個類繼承于SimpleHTTPRequestHandler,在靜態文件服務器的基礎上,增加了執行CGI腳本的功能。

簡單來說就是如下:

+-----------+          +------------------------+    
| TCPServer |          | BaseHTTPRequestHandler |
+-----------+          +------------------------+ 
     ^                            |
     |                            v
     |                +--------------------------+
     +----------------| SimpleHTTPRequestHandler |
     |                +--------------------------+
     |                            |
     |                            v
     |                 +-----------------------+
     +-----------------| CGIHTTPRequestHandler |
                       +-----------------------+

我們看看SimpleHTTPRequestHandler的源代碼:

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    server_version = "SimpleHTTP/" + __version__

    def do_GET(self):
        """Serve a GET request."""
        f = self.send_head()
        if f:
            try:
                self.copyfile(f, self.wfile)
            finally:
                f.close()

    # ...

    def send_head(self):
        path = self.translate_path(self.path)
        f = None
        if os.path.isdir(path):
            parts = urllib.parse.urlsplit(self.path)
            if not parts.path.endswith('/'):
                # redirect browser - doing basically what apache does
                self.send_response(HTTPStatus.MOVED_PERMANENTLY)
                new_parts = (parts[0], parts[1], parts[2] + '/',
                             parts[3], parts[4])
                new_url = urllib.parse.urlunsplit(new_parts)
                self.send_header("Location", new_url)
                self.end_headers()
                return None
            for index in "index.html", "index.htm":
                index = os.path.join(path, index)
                if os.path.exists(index):
                    path = index
                    break
            else:
                return self.list_directory(path)
        # ...

前面HTTP解析的部分不再分析,如果我們請求的是GET方法,將會被分配到do_GET函數里,在do_GET()中調用了send_head()方法。

send_head()中調用了self.translate_path(self.path)將request path進行一個標準化操作,目的是獲取用戶真正請求的文件。如果這個path是一個已存在的目錄,則進入if語句。

如果用戶請求的path不是以/結尾,則進入第二個if語句,這個語句中執行了HTTP跳轉的操作,這就是我們當前漏洞的關鍵點了。

0x02 任意URL跳轉漏洞

如果我們請求的是一個已存在的目錄,但PATH沒有以/結尾,則將PATH增加/并用301跳轉。

這就涉及到了一個有趣的問題:在chrome、firefox等主流瀏覽器中,如果url以//domain開頭,瀏覽器將會默認認為這個url是當前數據包的協議。比如,我們訪問http://example.com,跳轉到//baidu.com/,則瀏覽器會默認認為跳轉到http://baidu.com,而不是跳轉到.//baidu.com/目錄。

所以,如果我們發送的請求的是GET //baidu.com HTTP/1.0\r\n\r\n,那么將會被重定向到//baidu.com/,也就產生了一個任意URL跳轉漏洞。

在此前,由于目錄baidu.com不存在,我們還需要繞過if os.path.isdir(path)這條if語句。繞過方法也很簡單,因為baidu.com不存在,我們跳轉到上一層目錄即可:

GET //baidu.com/%2f.. HTTP/1.0\r\n\r\n

如何測試這個漏洞呢?其實也很簡單,直接用python3 -m http.server啟動一個HTTP服務器即可。訪問http://127.0.0.1:8000//example.com/%2f%2e%2e即可發現跳轉到了http://example.com/%2f../

0x03 web.py任意URL跳轉漏洞

那么,雖然說python核心庫存在這個漏洞,不過通常情況下不會有人直接在生產環境用python -m http.server

Python框架web.py在處理靜態文件的代碼中繼承并使用了SimpleHTTPRequestHandler類,所以也會受到影響。

我們可以簡單測試一下,我們用web.py官網的示例代碼創建一個web應用:

import web

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


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


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

然后模擬真實環境,創建一個static目錄,和一些子目錄:

static
├── css
│   └── app.css
└── js
    └── app.js

運行后,直接訪問http://127.0.0.1:8080////static%2fcss%2f@www.example.com/..%2f即可發現已成功跳轉。

web.py的具體分析我就不多說了,由于請求必須有/static/前綴,所以利用方法有些不同,不過核心原理也無差別。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/494/