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