一次安全測試引發的對Django框架檔案上傳安全機制的初步分析
*本文原創作者:ForrestX386,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載
0×00. 起因
我司的堡壘機是基於jumpserver 0.3版本進行二次開發,進行了大量的重構和新功能的新增,基本滿足了公司安全運維的需求。在對檔案上傳介面進行安全審計的時候發現,其對上傳檔名沒有過濾處理,然後直接寫入磁碟(部分程式碼如下)
隱隱覺得可以搞搞任意檔案寫入漏洞(jumpserver web 控制面板都是以root許可權執行,如果可以任意檔案寫入,危害呵呵,你懂得)。啟動burpsuite ,攔截請求,修改檔名(含有目錄穿越字元),但是結果沒成功,除錯發現upload_file.name 已經是../../等目錄穿越字元過濾後的結果,有點奇怪,莫非是框架自動幫我過濾掉了,好奇心驅使我必須弄明白其中的原理,於是有了本文。
0×01. 分析過程
切入點就是request.FILES 物件的由來,整個流程涉及到5個模組,如下:
django.core.handlers.wsgi django.http.request django.http.multipartparser django.core.files.uploadhandler django.core.files.uploadedfile
request.FILES 是一個類似於dict的物件,上傳檔案輸入框name屬性的值為鍵名,鍵值指向處理後的檔案物件(框架會呼叫指定的檔案處理器處理),這個檔案物件就是django.core.files.uploadedfile 模組中UploadedFile類的例項。詳細分析如下:
訪問request.FILES 就是訪問 django.core.handlers.wsgi 模組中WSGIRequest(繼承至django.http.request模組的HttpRequest類)類的FILES屬性
也即訪問WSGIRequest._get_files,這個方法會先判斷是否已經解析過上傳的檔案(也即判斷是否有_files屬性,其實FILES 就是_files,MultiValueDict 類的例項),跟進_load_post_and_files 方法(這是繼承至其父類django.http.request模組的HttpRequest類中的方法),如下:
跟進parse_file_upload方法(這裡的data為WSGIRequest 的例項),如下:
這裡要先說下upload_handlers 成員,如下:
初始化upload_handlers的時候會呼叫django.core.files.uploadhandler模組的load_handler載入系統預設的檔案處理器,如下:
settings.FILE_UPLOAD_HANDLERS
預設就是指的紅框中的兩個檔案處理器,大於2.5M的就用TemporaryFileUploadHandler 處理器,否則用MemoryFileUploadHandler。
初始化檔案上傳處理器之後,就開始呼叫django.http.multipartparser 模組的MultiPartParser 類的parse 方法對上傳檔案進行解析處理,在解析處理過程中,會呼叫 handle_file_complete 對上傳後的檔案進行再次處理(處理完成後就返回一個django.core.files.uploadedfile.UploadedFile類的例項, 這個例項物件會被新增到_files 物件中,然後由parse 方法返回此物件, 這個過程就包含檔名被過濾掉的過程),如下:
圖中的old_filed_name 即為上一個解析完畢的檔案,跟進handle_file_complete ,如下:
跟進檔案處理器的file_complete方法, 這個方法返回的就是處理後的檔案物件,也就是0×00 圖中upload_file 變數指向的檔案物件,這裡我們以MemoryFileUploadHandler 檔案處理器為例進行說明:
也就是說0×00 中的upload_file 也即InMemoryUploadedFile 類的例項,所以呼叫upload_file.name 即呼叫InMemoryUploadedFile 的name屬性,如下:
呼叫InMemoryUploadedFile 的name屬性,即呼叫_get_name方法,在InMemoryUploadedFile 例項話的過程中有name的賦值操作(在其父類__init__方法中)如下:
賦值操作就會觸發_set_name方法的執行:
在_set_name中就會對上傳的檔案進行過濾處理,os.path.basename(name)防止了目錄穿越漏洞,所以我們在0×00 圖中使用uploadfile.name獲取到的是經過os.path.basename 處理後的檔名,當然沒法任意檔案寫入了
0×02. 總結
梳理完成之後,終於對Django 檔案上傳中的安全機制有了一些瞭解,解決了我的困惑,像Django 這種現代的web框架對傳統的安全漏洞(比如XSS,CSRF、檔案上傳等)都做了比較好的處理,在開發中,這些都是值得我們借鑑和學習的地方。
*本文原創作者:ForrestX386,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載