今天在BUUctf上做了此題,正好想找機會加深一下對phar相關知識的理解,
一打開是個登陸界面,簡單試了下好像沒法注入,而且可以注冊,并不一定要強行登錄什么,不妨注冊一下,
此處我為了簡單填的kendo: kendo,
然后就會跳轉回登錄頁面,我們輸入賬號密碼登錄,進入主頁,是一個可以上傳文件的版面,
按常理是只讓上傳圖片類型,這里我們簡單對服務端的上傳過濾做個簡單測試,看看他的過濾有多嚴格,
這里我們把一個php文件的后綴改為jpg之后上傳,也沒做什么別的處理,
發現上傳成功,看來這個題目比較友好,
接下來我們點擊下載,
通過抓包可以看到,這個filename直接裸露在我們面前,促使我們去考慮文件任意下載,但是服務端是否對filename做了過濾,還有待嘗試,
這里需要注意一個細節,按照慣例和經驗,我們上傳的文件是放在 網站主目錄/sandbox/hash 目錄下的,所以要想下載php文件必須跳轉到上級目錄,
既然這樣,我們可以再點一下刪除,這里的delete.php一定也存在任意文件刪除漏洞。如此我們可以順藤摸瓜,下載下來這個網站的所有php文件,這里我們已知的除了index.php,還有已經遇見的upload.php, register.php, login.php, download.php, delete.php,再加上這些php文件中include的class.php,我們都下載下來。
在本地搭建了這樣一個環境,既然有源碼,接下來就是審計的問題。
應該先看的就是config.php、class.php或function.php這類文件,對于此題來說我們先看class.php,此處先是和數據庫建立連接,而后分別是User類,FileList類,File類,
我們可以發現,這里和數據庫交互的語句全部進行了參數綁定,也就是說,SQL注入我們可以不想了,應該考慮別的知識點,
在download.php里,我們發現了一個暗示,雖然filename我們可控,導致任意了任意下載,但是它不讓我們下載文件名里有flag的文件,暗示我們本題中要讀取文件名里有'flag'的文件,
講真,這個提示即使作為暗示,也是比較晦澀的(我也是看了大佬的wp才知道是'/flag.txt',或許大佬們有更高大上的操作,望不吝賜教),只能在后面嘗試網站根目錄下有沒有flag.txt(php),或者逐級讀取直到根目錄下有沒有flag.txt(php),
向下走,我們在FileList類中發現了一個有意思的函數,這是一個魔術方法,
這個方法在這里很突兀,內容也很強硬、直接、粗暴,應該來講,它的出現就是提醒我們,該考慮使用phar了。
phar文件是php的壓縮文件,它可以把多個文件歸檔到同一個文件中,而且不經過解壓就能被 php 訪問并執行,phar://與file:// ,php://等類似,也是一種流包裝器。至于深層次的知識點,網上有很多大佬寫的非常詳細,有的甚至深入到底層代碼,貼出鏈接:
關于phar的格式:https://blog.csdn.net/u011474028/article/details/54973571
關于phar://的利用:https://xz.aliyun.com/t/2715
所以我就不再班門弄斧,只對幾個小問題簡單談下自己淺顯的理解。
一是為什么phar協議讀取phar文件會觸發反序列化操作,
先說一下phar文件結構:
1、stub
一個供phar擴展用于識別的標志,格式為xxx<?php xxx; __HALT_COMPILER();?>,注意此處必須以__HALT_COMPILER();?>結尾,但前面的內容沒有限制,也就是說我們可以在前面輕易偽造一個圖片文件的頭如GIF98a來繞過一些上傳限制;
2、manifest
phar文件本質上是一種壓縮文件,每個被壓縮文件的權限、屬性等信息都放在這部分。另外,這部分還會以序列化的形式存儲用戶自定義的meta-data,這就是觸發反序列化的點,當文件操作函數通過phar://偽協議解析phar文件時就會將數據反序列化。至于為什么會這樣,有大佬深入了底層源代碼進行分析,我也不懂。
3、contents
被壓縮文件的內容,不是主要的點。
4、signature
簽名,放在文件末尾,不用我們操心
二是為什么使用phar協議讀取改為其他后綴名的phar文件也能觸發反序列化,
底層的原因我不知道,我只能舉個例子進行類比:
我們先寫一個1.php,內容為<?php phpinfo();然后改名為1.jpg
在index.php里面include它,
結果我們發現成功讀取了phpinfo
我們在php.net上可以看到,
就算說包含一個圖片文件沒毛病,運行一個圖片文件多少有些不妥,所以我猜測,include會將被包含文件作為php文件運行,類比來看,將demo.phar改名為demo.jpg之后,phar://協議也會將demo.jpg作為phar文件處理,所以我們也可以通過phar://demo.jpg正常訪問demo.phar。雖然是類比,不具有很強的說服力,但php的文件函數都是以流的形式讀取文件,這些多少都是相通的,從代碼邏輯上本就應該具有較高的相似性。
三是為什么加上phar://就能以phar文件的格式讀取文件,類似的,為什么print(file_get_contents("php://input"))就能打印出post的內容,
我太菜了,不懂底層實現,這里還是舉例子做類比,在我們使用Python讀取文件的時候,寫的是with open('1.txt', 'r') as f:,那么問題來了,'1.txt'不過是一個字符串而已,為什么能能讀取出1.txt的內容呢,類似的例子在PHP里也有,比如include "1.php",最終也是將1.php包含并運行,而'1.php'也不過是個字符串而已,我們echo '1.php'也好,echo"php://input"也好,都不會有任何作用。
我個人的理解是,'1.php'本身是個字符串,'php://input'本身也是字符串,但是include、file_get_contents都是進行的文件流操作,會把字符串的實際意義和文件流關聯起來,使其不再是簡單的字符串。個人認為這樣理解問題不大,至于正確與否,日后一定會補上。
四是舉個實際應用的例子,雖然別的大佬寫的很多了,但我總覺得自己不寫一下不完整,
先是一個index.php,里面聲明了兩個類,
而后在demo.php里通過控制參數來生成可利用的phar文件(需要配置php.ini的phar.readonly = Off后重啟Server),
最后在demo1.php里file_get_contents一下,
雖然有個警告(也不知啥原因,有知道的同志望不吝賜教),但代碼還是執行成功了。
這只是一個很簡單的例子,實戰價值不大,畢竟現實情況下,每個類都是在服務端寫好的,服務端不傻,不能保證我們每次都能遇到里面直接有eval($s)這樣對選手十分友好的類,也很難保證直接在__destruct里面就能觸發相應函數,可能需要去尋找其他魔術方法,畢竟非魔術方法我們是幾乎沒可能直接調用的。比如本題中就沒有任何代碼執行或命令執行語句可供使用,迫使我們只能去想文件讀取。但總的來說,基本思路就是這樣:上傳phar文件,利用類中的可利用的方法,找到服務端文件操作函數并以phar://協議讀取phar文件。
有大佬總結的利用條件:
1)phar文件要能夠上傳至服務器
2)要有可用的魔術方法為跳板
3)文件操作函數的參數可控,且:、/、phar等特殊字符沒有被過濾
對于本題而言,第一條滿足,第二條有一個魔術方法__call()和FileList類、User類的__destruct(),恐怕想不利用它們也不行,第三條后半部分沒問題,前半部分則需要我們找一找。
既曰文件操作函數,就應該在本題的File類(至多也在FileList類)的方法中尋找,畢竟整個題目基本上都是在面向對象的基礎上編程,對文件的操作也都是對File類的對象的操作,
我們看到,open()方法調用了file_exists()和is_dir()函數(注意name方法里的basename函數不算),size()方法調用了filesize()函數,delete()方法調用了unlink()函數,close()方法file_get_contents()函數。
我們前面提到了,本題要讀取/flag.txt文件,故剛剛列舉的這些函數中,雖然文件操作函數不少,可以用來觸發反序列化,對讀取文件有用的只有close()方法中的file_get_contents()函數這一個,所以我們可以對它分析,
這個時候,如果想不到__call()方法和__destruct()方法,基本上就可以放棄了,在phar題目里,魔術方法一般來講是必須要用的,
這里我們看到,FileList的__call()方法語義簡單,就是遍歷files數組,對每一個file變量執行一次$func,然后將結果存進$results數組,
接下來的__destruct函數會將FileList對象的funcs變量和results數組中的內容以HTML表格的形式輸出在index.php上(我們可以看到,index.php里創建了一個FileList對象,在腳本執行完畢后觸發__destruct,則會輸出該用戶目錄下的文件信息),
User對象的__destruct()方法,
無非就是 腳本執行完畢后,執行$db的close()的方法(來關閉數據庫連接),但話說回來,沒有括號里的話,這句話依然成立,而且這個'close'與File類中的close()方法同名。所以,當db的值為一個FileList對象時,User對象析構之時,會觸發FileList->close(),但FileList里沒有這個方法,于是調用_call函數,進而執行file_get_contents($filename),讀取了文件內容。整個鏈的結構也很簡單清晰:在我們控制$db為一個FileList對象的情況下,$user->__destruct() => $db->close() => $db->__call('close') => $file->close() => $results=file_get_contents($filename) => FileList->__destruct()輸出$result。
接下來,我們開始著手構造POP鏈,
這里的類都是簡化了寫的,類的成員由屬性和方法構成,序列化一個對象將會保存對象的所有變量,但不會保存對象的方法,只會保存類的名字。因此,反序列化的主要危害在于我們可以控制對象的變量來改變程序執行流程。在這個過程中,我們無法調用對象的普通方法,故我們這里只能利用可以調用的魔術方法。
上傳后,在刪除文件時抓包,修改filename
即可得到flag.
這里要注意一個細節:
ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 這個函數執行后,我們通過Web只能訪問當前目錄、/etc和/tmp三個目錄,所以只能在delete.php中利用payload,而不是download.php,否則訪問不到沙箱內的上傳目錄。