作者:phith0n@長亭科技
Github賬號被封了以后,Vulhub也無法繼續更新了,余下很多時間,默默看了點代碼,偶然還能遇上一兩個漏洞,甚是有趣。
這個漏洞出現在python核心庫http中,發送給官方團隊后被告知撞洞了,且官方也認為需要更多人看看怎么修復這個問題,所以我們來分析一下。
0x01 http.server庫簡單分析
眾所周知Python有一個一鍵啟動Web服務器的方法:
python3 -m http.server
在任意目錄執行如上命令,即可啟動一個web文件服務器。其實這個方法就用到了http.server模塊。這個模塊包含幾個比較重要的類:
HTTPServer這個類繼承于socketserver.TCPServer,說明其實HTTP服務器本質是一個TCP服務器BaseHTTPRequestHandler,這是一個處理TCP協議內容的Handler,目的就是將從TCP流中獲取的數據按照HTTP協議進行解析,并按照HTTP協議返回相應數據包。但這個類解析數據包后沒有進行任何操作,不能直接使用。如果我們要寫自己的Web應用,應該繼承這個類,并實現其中的do_XXX等方法。SimpleHTTPRequestHandler,這個類繼承于BaseHTTPRequestHandler,從父類中拿到解析好的數據包,并將用戶請求的path返回給用戶,等于實現了一個靜態文件服務器。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/前綴,所以利用方法有些不同,不過核心原理也無差別。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/494/
暫無評論