應對安全漏洞:如何將LFI變為RFI
前言
PHP文件包含漏洞的產生原因是在通過PHP的函數引入文件時,由於傳入的文件名沒有經過合理的校驗,從而操作了預想之外的文件,就可能導致意外的文件泄露甚至惡意的代碼注入。最常見的就屬於本地文件包含(Local File Inclusion)漏洞了。
常見漏洞代碼
if ($_GET['method']) { include $_GET['method']; } else { include 'index.php'; }
一般情況下,程序的執行過程是當用戶提交url為 https://xianzhi.aliyun.com/sth.php?method=search.php 時,調用search.php裏麵的樣式內容和功能。直接訪問 https://xianzhi.aliyun.com/search.php 則會包含默認的index.php裏麵的樣式內容和功能。那麼問題來了,如果我們提交),且1.jpg是由黑客上傳到服務器上的一個圖片,並在圖片的末尾添加了惡意的php代碼,那麼惡意的代碼就會被當前文件執行,以此觸發本地文件包含漏洞。
有趣的發現
我和我的好朋友Mike Brooks一直致力於對一些開源的Web框架進行代碼審計工作,在對這些Web開源框架代碼審計的過程中,我們找到了一種將本地文件包含漏洞(LFI)轉換為遠程文件包含漏洞(RFI)的方法。並且依賴於我們駐留在Web服務器上的JAR包文件,我們發現了一個能夠執行任意代碼的方法。通常情況下,當以特定方式配置Web應用程序時,它將能夠加載Web服務器上的JAR包文件並在文件中搜索實現的java類。有意思的是,在Java類中,我們可以在正在被執行的java類上定義一個靜態代碼塊,具體如下所示:
public class LoadRunner { static { System.out.println("Load runner'ed"); } public static void main(String[] args) { } }
我們首先編譯這個java類,然後在代碼中加載它,具體實現如下圖所示:
現在,我們已經有了兩個有趣的發現:一個是可以在加載的JAR文件中插入執行代碼,另一個是在Web服務器上找到一個合適的文件路徑來加載JAR包文件。因此,我們現在必須找到的一種方式來讓應用程序以某種方式引用我們駐留在服務器上的JAR包文件。在這個探索的過程中,我們嚐試了很多的方法,包括去查看應用程序中的所有請求處理程序以確定能否進行文件上傳;甚至嚐試尋找可以在服務器上毒化文件的方法,以便將其轉化為JAR包文件,但是這些方法卻都沒有能夠奏效。盡管這樣,我們仍然沒有放棄去研究和探索,最終Mike Brooks想出了一個好主意。
文件描述符
一般情況下,大多數Web開源框架都會將上傳的文件落地到服務器的某個磁盤上,但文件的路徑是不可猜測的(通常使用GUID或其他隨機標識符來表示),如果我們不知道文件路徑,那又該如何去訪問上傳的文件呢?在Linux中,當一個進程有一個文件被打開時,它將在其 /proc/ 目錄中打開一個指向該文件的文件描述符。因此,如果我們有一個PID為1234的進程,並且該進程打開了磁盤上的某個文件,那麼我們可以通過**/proc/1234/fd/***文件描述符來訪問該文件。這意味著,我們不需要猜測GUID或其他隨機值,我們隻需要猜測HTTP請求處理程序的PID和上傳文件的文件描述符即可。因而用於訪問上傳文件的搜索空間就會大幅減少。不僅如此,如果我們已經有了LFI,加上磁盤上那些經常出現的、可以預測的PID(Web服務器上HTTP請求處理程序的PID)文件,因此獲取PID和文件描述符編號要比想象中簡單得多。
加載文件描述符
為了實現上麵的方法,我們首先需要在程序中找到那些處理文件上傳的請求,之後嚐試通過Web開源框架中的LFI漏洞來查看所有PID文件描述符,並通過文件描述符來訪問我們在Web服務器上上傳的文件。在我們測試過的Web框架中,當訪問FILES字典時,這些文件描述符總是被緩慢加載,而Flask Web框架直接在HTTP GET請求填充了FILES字典字段,以下是超簡單的Flask應用程序:
# -*- coding: utf-8 -*- import os from flask import Flask, request UPLOAD_FOLDER = "/tmp" app = Flask(__name__) app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER @app.route("/", methods=["GET"]) def show_me_the_money(): x = request import code code.interact(local=locals()) if __name__ == "__main__": app.run()
在這個應用程序中,我們有一個單一的處理程序,該處理程序允許在URL上掛載HTTP GET請求。然後我們在Ubuntu VM中運行這個程序,並通過HTTP GET請求將文件上傳到該服務器上。對於以前沒有使用過 import code 技巧的人來說,這是一個很好的方法來調試Python代碼和庫,因為在 code.interact 被調用時你將進入到python的REPL環境中去。
以下是通過HTTP GET請求上傳文件的簡單腳本:
# -*- coding: utf-8 -*- import requests response = requests.get( "https://127.0.0.1:5000/", files={ "upload_file": open("/tmp/hullo", "rb"), }, )
而在 /tmp/hullo 的文件中,我們可以看到很多的“Hello World”:
然後,我們首先運行服務器,之後上傳文件,並進入Flask請求處理程序上下文中的python REPL環境,具體如下圖所示:
通過使用請求處理程序的PID,我們可以查看到磁盤上打開的文件描述符:
然後我們返回到REPL並訪問上傳的文件:
現在該文件已經在Web服務器中被訪問過了,此刻我們回到 /proc 目錄,看看是否可以找到上傳文件的內容:
果然,我們找到了上傳的文件!因此,對於我們正在評估的Web應用程序來說,我們確認通過這種上傳和引用文件的方法可以正常訪問到我們駐留在Web服務器中的JAR包文件!
我們可以通過 多次上傳相同的文件 來進一步減少文件路徑搜索空間,因此我修改了文件上傳的代碼使得可以上傳相同的九個文件:
# -*- coding: utf-8 -*- import requests response = requests.get( "https://127.0.0.1:5000/", files={ "upload_file": open("/tmp/hullo", "rb"), "upload_file2": open("/tmp/hullo", "rb"), "upload_file3": open("/tmp/hullo", "rb"), "upload_file4": open("/tmp/hullo", "rb"), "upload_file5": open("/tmp/hullo", "rb"), "upload_file6": open("/tmp/hullo", "rb"), "upload_file7": open("/tmp/hullo", "rb"), "upload_file8": open("/tmp/hullo", "rb"), }, )
運行此腳本後,訪問處理程序中的 FILES 字典,並查看請求處理程序PID目錄中的 fd 目錄內容,我們看到所有上傳的文件都有打開的文件描述符:
因此,通過使用這種方法,我們可以保證具有特定號碼的文件描述符終將會指向我們上傳的文件。如果我們提交100個上傳文件的請求,可能文件描述符50指向的就是我們的文件!反過來,我們現在需要猜測的唯一值就是PID。
如何實施攻擊利用?
總而言之,為了引用上傳文件以達到攻擊的目的,這是一種大大減少搜索空間的方法,這在許多情況下可以使LFI成為RFI。如果你要使用此方法進行攻擊利用,請考慮以下事項:
-
通過我們對多個Web框架(Django和Flask)的分析發現,當訪問FILE字典時框架會延遲加載文件引用。因此,我們必須定位 訪問FILES字典的 請求處理程序 。一旦請求處理程序訪問了FILES字典,文件描述符將在請求處理期間一直保持打開狀態。
-
默認情況下,其他框架可能會填充這些文件描述符,而這正是我們下一步將要研究的內容。
-
當處理請求體中上傳的文件時,有些框架不區分不同的請求方式,這在一定程度上說明這種攻擊方法不僅限於非冪等的HTTP verbs。
-
PID並不意味著隨機化。無論我們的目標是什麼(Ubuntu上的Apache,Fedora上的Nginx等),如果我們希望將其轉化為漏洞,那麼我們可以創建一個本地設置,並查看與Web服務器和請求處理程序相關聯的PID。一般來說,當我們將服務安裝到*nix時,它們將在機器重新啟動時以類似的順序啟動。由於PID也按順序分配,這意味著我們可以大大減少PID搜索空間。
-
請求處理程序需要訪問 所有要處理的上傳文件 的FILES字典 。這就是說,如果處理程序中的功能期望上傳的文件是PDF,以執行請求處理程序的中代碼,而此時我們也準備上傳一個JAR包文件,那麼隻要同時上傳這兩個文件即可,它們都將被賦予文件描述符。
-
嚐試找到加載文件描述符的請求處理程序,為了我們的代碼審計工作能夠順利進行,我們發現有一個處理程序會逐行處理一個文件的全部內容,所以我們上傳了一個巨大的文件,該巨大的文件中當然也包含了我們想要執行的JAR包文件。
- 請注意,如果您上傳的文件較小,則可能隻讀入內存,並且不會打開任何文件描述符。當對Flask Web框架進行測試時,我們發現1MB以下的文件會被直接加載到內存中,而1MB以上的文件則放在磁盤上。因此,我們需要在JAR包文件中額外填充任何可供攻擊利用的有效載荷。
後續更新
後續我們對多個框架緩慢加載文件描述符這一問題進行了深入的研究和分析。最後我們發現對於Flask和Django來說,並不是FILES 被緩慢加載,而是請求體的內容隻有在被訪問時才會被處理。因此,根據這個結論我們可以輕鬆定位那些訪問HTTP請求體數據的任何請求處理程序。一旦請求處理程序訪問了包含在請求體中的數據,文件描述符就會被填充。
Django框架中訪問請求體數據代碼如下所示:
通過此訪問請求填充的文件描述符如下所示:
Flask Web框架中訪問請求體數據的代碼如下所示:
通過此訪問請求填充的文件描述符如下所示:
本文作者:佚名
來源:51CTO
最後更新:2017-11-03 15:04:15