PHP-Audit-Labs題解之Day1-4
前言
大家好,我們是紅日安全-程式碼審計小組。最近我們小組正在做一個PHP程式碼審計的專案,供大家學習交流,我們給這個專案起了一個名字叫 PHP-Audit-Labs 。我們已經發表的系列文章如下:
[[紅日安全]程式碼審計Day1 - in_array函式缺陷]( ofollow,noindex" target="_blank">https://xz.aliyun.com/t/2451)
[[紅日安全]程式碼審計Day2 - filter_var函式缺陷]( https://xz.aliyun.com/t/2457)
[[紅日安全]程式碼審計Day3 - 例項化任意物件漏洞]( https://xz.aliyun.com/t/2459)
[[紅日安全]程式碼審計Day4 - strpos使用不當引發漏洞]( https://xz.aliyun.com/t/2467)
在每篇文章的最後,我們都留了一道CTF題目,供大家練習。下面是 Day1-Day4 的題解:
Day1題解:(By 七月火)
題目如下:
實際上這道題目考察的是 in_array 繞過和不能使用拼接函式的 updatexml 注入,我們先來看一下 in_array 的繞過。在下圖第11~13行處,程式把使用者的ID值儲存在 $whitelist** 陣列中,然後將使用者傳入的 **id** 引數先經過stop_hack函式過濾,然後再用 **in_array** 來判斷使用者傳入的 **id** 引數是否在 **$whitelist 陣列中。這裡 in_array 函式沒有使用強匹配,所以是可以繞過的,例如: id=1' 是可以成功繞過 in_array 函式的。
然後在說說 updatexml 注入,這題的注入場景也是在真實環境中遇到的。當 updatexml 中存在特殊字元或字母時,會出現報錯,報錯資訊為特殊字元、字母及之後的內容,也就是說如果我們想要查詢的資料是數字開頭,例如 7701HongRi ,那麼查詢結果只會顯示 HongRi 。所以我們會看到很多 updatexml 注入的 payload 是長這樣的 and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1) ,在所要查詢的資料前面憑藉一個特殊符號(這裡的 0x7e 為符號 '~' )。
回到題目,我們看一下 stop_hack 函式過濾了什麼。可以發現該方法過濾了字串拼接函式(下圖第2行),所以我們就要用其他方法來繞過。
我們直接來看一下本題的 payload :
http://localhost/index.php?id=4 and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))
實際上,繞過的思路還是將特殊字元或字母拼接在我們想要的資料的前面,讓資料的第一個字元為字母或符號即可,這裡給出我以前寫的分析 文章 ,供大家參考學習。
Day2題解:(By 七月火)
題目如下:
這道CTF題目,實際上考察的是 filter_var 函式的繞過與遠端命令執行。在題目 第6行 ,程式使用 exec 函式來執行 curl 命令,這就很容易讓人聯絡到命令執行。所以我們看看用於拼接命令的 $site_info['host']** 從何而來。在題目 **第2-4行** ,可以看到 **$site_info 變數是從使用者傳來的 url 引數經過 filter_var 和 parse_url 兩個函式過濾而來。之後,又規定當 url 引數的值以 sec-redclub.com 結尾時,才會執行 exec 函式。
所以讓我們先來繞過 filter_var 的 FILTER_VALIDATE_URL 過濾器,這裡提供幾個繞過方法,如下:
http://localhost/index.php?url=http://[email protected] http://localhost/index.php?url=http://demo.com&sec-redclub.com http://localhost/index.php?url=http://demo.com?sec-redclub.com http://localhost/index.php?url=http://demo.com/sec-redclub.com http://localhost/index.php?url=demo://demo.com,sec-redclub.com http://localhost/index.php?url=demo://demo.com:80;sec-redclub.com:80/ http://localhost/index.php?url=http://demo.com#sec-redclub.com PS:最後一個payload的#符號,請換成對應的url編碼 %23
接著要繞過 parse_url 函式,並且滿足 $site_info['host'] 的值以 sec-redclub.com 結尾,payload如下:
http://localhost/index.php?url=demo://%22;ls;%23;sec-redclub.com:80/
當我們直接用 cat f1agi3hEre.php 命令的時候,過不了 filter_var 函式檢測,因為包含空格,具體payload如下:
http://localhost/index.php?url=demo://%22;cat%20f1agi3hEre.php;%23;sec-redclub.com:80/
所以我們可以換成 cat<f1agi3hEre.php 命令,即可成功獲取flag:
關於 filter_var 函式繞過更多的細節,大家可以參考這篇文章: SSRF技巧之如何繞過filter_var( ) ,關於命令執行繞過技巧,大家可以參考這篇文章: 淺談CTF中命令執行與繞過的小技巧 。
Day3題解:(By 七月火)
題目如下:
這道題目考察的是例項化漏洞結合XXE漏洞。我們在上圖第18行處可以看到使用了 class_exists 函式來判斷類是否存在,如果不存在的話,就會呼叫程式中的 __autoload 函式,但是這裡沒有 __autoload 函式,而是用 spl_autoload_register 註冊了一個類似 __autoload 作用的函式,即這裡輸出404資訊。
我們這裡直接利用PHP的內建類,先用 GlobIterator 類搜尋 flag檔案 名字,來看一下PHP手冊對 GlobIterator 類的 建構函式的定義:
public GlobIterator::__construct ( string $pattern
[, int $flags
= FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO ] )
第一個引數為要搜尋的檔名,第二個引數為選擇檔案的哪個資訊作為鍵名,這裡我選擇用 FilesystemIterator::CURRENT_AS_FILEINFO ,其對應的常量值為0,你可以在 這裡 找到這些常量的值,所以最終搜尋檔案的 payload 如下:
http://localhost/CTF/index.php?name=GlobIterator¶m=./*.php¶m2=0
我們將會發現flag的檔名為 f1agi3hEre.php ,接下來我們使用內建類 SimpleXMLElement 讀取 f1agi3hEre.php 檔案的內容,,這裡我們要結合使用PHP流的使用,因為當檔案中存在: < > & ' " 這5個符號時,會導致XML檔案解析錯誤,所以我們這裡利用PHP檔案流,將要讀取的檔案內容經過 base64編碼 後輸出即可,具體payload如下:
http://localhost/CTF/index.php?name=SimpleXMLElement¶m=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/CTF/f1agi3hEre.php">]><x>%26xxe;</x>¶m2=2
上面payload中的param2=2,實際上這裡2對應的模式是 LIBXML_NOENT ,具體可以參考 這裡 。
Day4題解:(By 七月火)
本次題目為QCTF 2018中的一道題目,由於程式碼太多,這裡就不貼出原圖片。題目的場景為:一個彩票系統,每位使用者初始情況下有20$,由使用者輸入一個7位數,系統也會隨機生成一個7位數。然後逐位數字進行比較,位數相同的個數越多,獎勵的前也越多。當你的錢足夠買flag的時候,系統就會給你flag。
我們來看一下後臺程式碼是如何進行比較的,比較程式碼在 buy.php 檔案中:
在上圖中看到表單的部分( 程式碼4-8行 ),呼叫了 js/buy.js 檔案,應該是用來處理上面的表單的,我們具體看一下 js 程式碼:
在 第10行 處看到,程式將表單資料以 json 格式提交到伺服器端,提交頁面為 api.php ,我們轉到該檔案看看。
這裡主要是對數字進行比較,注意 第13行 用的是 == 操作符對資料進行比較,這裡會引發安全問題。因為使用者的資料是以 json 格式傳上來的,如果我們傳一個數組,裡面包含7個 true 元素,這樣在比較的時候也是能相等的。因為 == 運算子只會判斷兩邊資料的值是否相等,並不會判斷資料的型別。而語言定義,除了 0、false、null 以外均為 true ,所以使用 true 和數字進行比較,返回的值肯定是 **true 。只要後臺生成的隨機數沒有數字0,我們傳入的payload就能繞過每位數字的比較。我們傳送幾次payload後,就可以買到flag了。
在看官方WP的時候,還發現另外一種解法,也是一種不錯的思路。
另外比賽過程中發現有的選手用了暴力重複註冊然後買彩票的方法。考慮了一下這種方法花費的時間並不比直接審計程式碼短,為了給廣大彩民一點希望,可以留作一種備選的非預期解,就沒有改題加驗證碼或者提高flag價格。
總結
我們的專案會慢慢完善,如果大家喜歡可以關注 PHP-Audit-Labs 。大家若是有什麼更好的解法,可以在文章底下留言,祝大家玩的愉快!