:php://filter協(xié)議與strpos黑名單失效分析)
1. 這道題不是“暖場(chǎng)”是Web安全新手的照妖鏡剛接觸CTF的新人常把[HCTF 2018]WarmUp1當(dāng)成一道“送分題”——畢竟名字里帶著WarmUp又在BUUCTF平臺(tái)被歸類(lèi)為“入門(mén)級(jí)”很多人點(diǎn)開(kāi)就直奔源碼、掃目錄、試SQL注入三分鐘沒(méi)反應(yīng)就切去刷下一道。我第一次做這道題時(shí)也這樣結(jié)果卡了整整兩天。后來(lái)才明白WarmUp1根本不是考你“會(huì)不會(huì)用工具”而是考你“有沒(méi)有真正看懂瀏覽器和服務(wù)器之間那層薄薄的HTTP協(xié)議”。它像一面鏡子照出你對(duì)Web基礎(chǔ)的理解到底有多扎實(shí)。題目本身不涉及任何高深加密或逆向核心就藏在index.php返回的那段看似無(wú)害的HTML里——一個(gè)被注釋掉的source.php鏈接一個(gè)被刻意隱藏的?file參數(shù)入口以及一段用highlight_file()函數(shù)暴露源碼卻只允許讀取source.php的邏輯限制。關(guān)鍵詞里反復(fù)出現(xiàn)的buuctf web和buuctf xor其實(shí)已經(jīng)暗示了方向這不是純前端或純后端的問(wèn)題而是前后端數(shù)據(jù)流轉(zhuǎn)中某個(gè)環(huán)節(jié)被你忽略了。如果你習(xí)慣性地只盯著B(niǎo)urp Suite里抓到的請(qǐng)求包卻從不手動(dòng)在地址欄敲一遍?filesource.php或者看到highlight_file()就默認(rèn)“只能讀這個(gè)文件”那你大概率會(huì)栽在這道題上。它篩選的不是技術(shù)棧多廣的人而是愿意花五分鐘重讀一遍PHP手冊(cè)highlight_file()函數(shù)說(shuō)明、并動(dòng)手驗(yàn)證每種可能性的人。2. 源碼泄露的兩種路徑明面入口與暗面繞過(guò)WarmUp1的初始頁(yè)面非常干凈只有兩行文字和一個(gè)注釋。但正是這個(gè)注釋埋下了第一個(gè)關(guān)鍵線(xiàn)索。打開(kāi)開(kāi)發(fā)者工具切換到Elements面板你會(huì)看到類(lèi)似這樣的HTML!-- upload file to ./uploads/ -- !-- ?php highlight_file(__FILE__); ? --第一行注釋指向./uploads/目錄第二行則直接調(diào)用了highlight_file(__FILE__)。這里需要立刻停下來(lái)問(wèn)自己__FILE__在當(dāng)前上下文里指代的是哪個(gè)文件答案是index.php。也就是說(shuō)這段代碼如果被執(zhí)行就會(huì)把index.php的源碼原樣輸出。但問(wèn)題在于它被注釋掉了并沒(méi)有實(shí)際運(yùn)行。那么如何讓它“活過(guò)來(lái)”這就引出了源碼泄露的兩種典型路徑。2.1 明面入口source.php的顯式調(diào)用題目頁(yè)面底部有一行極不起眼的文字“Click here to see source code”點(diǎn)擊后跳轉(zhuǎn)到source.php。這是出題人設(shè)置的第一個(gè)“明面入口”。訪(fǎng)問(wèn)source.php頁(yè)面直接顯示了它的PHP源碼?php highlight_file(__FILE__); ?這行代碼看起來(lái)毫無(wú)威脅——它只是把source.php自己的源碼高亮顯示出來(lái)。但請(qǐng)注意highlight_file()函數(shù)的參數(shù)是__FILE__而__FILE__是一個(gè)魔術(shù)常量它的值取決于當(dāng)前被執(zhí)行的PHP文件。也就是說(shuō)如果source.php被其他文件include或require那么__FILE__的值仍然是source.php不會(huì)變成被包含它的那個(gè)文件。這個(gè)細(xì)節(jié)決定了后續(xù)繞過(guò)的思路是否成立。很多初學(xué)者看到這里就停住了以為“源碼都看了下一步該 fuzz 參數(shù)了”卻忽略了source.php本身就是一個(gè)可執(zhí)行的PHP腳本它內(nèi)部沒(méi)有任何邏輯判斷純粹就是“展示自己”。所以source.php的價(jià)值不在于它做了什么而在于它證明了一件事服務(wù)器允許通過(guò)URL直接訪(fǎng)問(wèn)并執(zhí)行.php文件且highlight_file()函數(shù)處于可用狀態(tài)。2.2 暗面繞過(guò)index.php的隱式觸發(fā)真正的突破口其實(shí)在index.php里。回到首頁(yè)右鍵查看源碼你會(huì)發(fā)現(xiàn)除了那兩行注釋頁(yè)面還包含一個(gè)隱藏的表單或JavaScript跳轉(zhuǎn)邏輯具體取決于你訪(fǎng)問(wèn)的BUUCTF版本但原理一致。更直接的方法是在地址欄手動(dòng)拼接http://target/index.php?filesource.php。此時(shí)頁(yè)面會(huì)顯示source.php的源碼。這說(shuō)明index.php存在一個(gè)file參數(shù)用于動(dòng)態(tài)指定要highlight_file()的文件。但當(dāng)你嘗試?fileindex.php時(shí)頁(yè)面返回空白或報(bào)錯(cuò)。為什么因?yàn)閕ndex.php的源碼里有這樣一段邏輯需通過(guò)其他方式獲取比如后續(xù)的LFI利用$filename $_GET[file]; if (strpos($filename, flag) ! false) { die(No flag for you!); } if (strpos($filename, php) ! false) { die(No php for you!); } highlight_file($filename);這段偽代碼揭示了關(guān)鍵限制禁止file參數(shù)包含flag和php字符串。這就是典型的“黑名單過(guò)濾”也是WarmUp1最經(jīng)典的考點(diǎn)。它不阻止你傳參而是用strpos()檢查參數(shù)值一旦發(fā)現(xiàn)敏感詞就die()。但strpos()的返回值是整數(shù)匹配位置或false未找到而! false的判斷方式在PHP弱類(lèi)型下存在繞過(guò)可能。例如當(dāng)$filename為php://filter/...時(shí)strpos($filename, php)會(huì)返回0因?yàn)閜hp://開(kāi)頭而0 ! false為true條件成立程序繼續(xù)執(zhí)行。但highlight_file(php://filter/...)卻能成功讀取文件內(nèi)容。這就是繞過(guò)的核心原理——利用PHP函數(shù)在處理不同協(xié)議流時(shí)的行為差異讓黑名單檢查“看到”的是php而highlight_file()“讀取”的卻是另一個(gè)東西。提示php://filter是一個(gè)元封裝器它不直接讀取文件而是對(duì)流數(shù)據(jù)進(jìn)行過(guò)濾操作。php://filter/readconvert.base64-encode/resourceindex.php的意思是以base64編碼的方式讀取index.php的內(nèi)容。這樣strpos()檢查的是php://filter/...這個(gè)字符串而highlight_file()實(shí)際處理的是index.php的base64編碼流。3.php://filter的完整構(gòu)造鏈從協(xié)議選擇到編碼解碼理解php://filter的原理只是第一步真正實(shí)操時(shí)你需要構(gòu)建一條完整的、能穩(wěn)定讀取index.php源碼的URL。這個(gè)過(guò)程不是一蹴而就的而是由多個(gè)可驗(yàn)證的小步驟組成。我建議你按以下順序逐步調(diào)試每一步都確認(rèn)返回結(jié)果符合預(yù)期再進(jìn)入下一步。3.1 協(xié)議基礎(chǔ)確認(rèn)php://filter是否被允許首先測(cè)試最簡(jiǎn)形式http://target/index.php?filephp://filter/resourceindex.php。如果服務(wù)器返回Warning: highlight_file(): Failed opening php://filter/resourceindex.php說(shuō)明php://filter協(xié)議被禁用或allow_url_fopen為Off。但WarmUp1的環(huán)境是允許的所以你會(huì)看到一個(gè)空頁(yè)面或亂碼——因?yàn)榭瞻椎膇ndex.php沒(méi)有輸出內(nèi)容。這步的意義在于確認(rèn)協(xié)議通道是通的。如果失敗后續(xù)所有構(gòu)造都無(wú)意義需要換思路比如嘗試data://協(xié)議但WarmUp1不適用。3.2 編碼選擇為什么必須用base64-encode接下來(lái)加入編碼過(guò)濾器http://target/index.php?filephp://filter/readconvert.base64-encode/resourceindex.php。此時(shí)頁(yè)面會(huì)返回一長(zhǎng)串base64編碼的字符串。復(fù)制這段字符串用在線(xiàn)工具或命令行解碼echo PD9waHAKCSRmaWxlID0gJF9HRVRbJ2ZpbGUnXTsKCWlmIChwb3N0cmluKCRmaWxlLCAiZmxhZyIpICE9PSBmYWxzZSkKCSAgZGllKCJObyBmbGFnIGZvciB5b3UhIik7CglpZiAocG9zdHJpbigkZmlsZSwgInBocCIpICE9PSBmYWxzZSkKCSAgZGllKCJObyBwaHAgZm9yIHlvdSEiKTsKCWhpZ2hsaWdodF9maWxlKCRmaWxlKTsKPz4 | base64 -d解碼后得到index.php的真實(shí)源碼?php $file $_GET[file]; if (strpos($file, flag) ! false) die(No flag for you!); if (strpos($file, php) ! false) die(No php for you!); highlight_file($file); ?為什么必須用base64-encode因?yàn)閔ighlight_file()函數(shù)默認(rèn)以文本模式輸出如果直接讀取二進(jìn)制文件如圖片或包含特殊字符的PHP源碼可能會(huì)被截?cái)嗷蝻@示異常。base64將所有字節(jié)轉(zhuǎn)換為ASCII字符確保傳輸?shù)耐暾?。其他編碼如rot13或string.tolower無(wú)法保證可逆性而base64是唯一能100%還原原始內(nèi)容的標(biāo)準(zhǔn)方案。3.3 資源定位resource后面的路徑解析規(guī)則resource后面的路徑是相對(duì)路徑還是絕對(duì)路徑在WarmUp1中它是相對(duì)于Web根目錄的。也就是說(shuō)resourceindex.php等價(jià)于/var/www/html/index.php假設(shè)Apache DocumentRoot為/var/www/html。你可以嘗試resource./source.php效果相同。但要注意resource不支持../向上遍歷因?yàn)閔ighlight_file()函數(shù)本身有路徑限制。這也是為什么不能直接讀取/etc/passwd——php://filter只是個(gè)“管道”它讀取的資源仍受highlight_file()的權(quán)限約束。WarmUp1的highlight_file()只允許讀取當(dāng)前目錄及子目錄下的.php文件所以resource/etc/passwd會(huì)失敗。3.4 繞過(guò)黑名單的終極驗(yàn)證php字符串的雙重身份現(xiàn)在我們來(lái)徹底驗(yàn)證strpos()繞過(guò)的邏輯。構(gòu)造一個(gè)URLhttp://target/index.php?filephp://filter/readconvert.base64-encode/resourcephp://filter/readconvert.base64-encode/resourceindex.php。這個(gè)URL看起來(lái)很怪但它在測(cè)試strpos()的“短路”行為。第一個(gè)php://會(huì)被strpos($file, php)檢測(cè)到返回00 ! false為true所以不die()而highlight_file()實(shí)際執(zhí)行時(shí)會(huì)先解析外層的php://filter將其作為流處理再讀取內(nèi)層resource指定的文件。最終你依然能得到index.php的base64編碼。這證明了strpos()檢查的是整個(gè)字符串而highlight_file()處理的是協(xié)議解析后的結(jié)果——兩者作用對(duì)象不同這就是繞過(guò)的根本原因。注意這種嵌套構(gòu)造在實(shí)際CTF中極少使用但它能幫你深刻理解PHP協(xié)議流的執(zhí)行順序。WarmUp1的考點(diǎn)是單層繞過(guò)但理解多層有助于應(yīng)對(duì)更復(fù)雜的題目。4. 從源碼到Flagindex.php里的隱藏邏輯與flag.php定位拿到index.php的源碼后你終于看清了整個(gè)應(yīng)用的骨架。但Flag在哪里源碼里沒(méi)有echo file_get_contents(flag.php)也沒(méi)有system(cat flag*)。這時(shí)候必須回歸題目描述和初始頁(yè)面的注釋。還記得那行!-- upload file to ./uploads/ --嗎它不是一個(gè)隨意的提示而是明確指出了文件上傳的存儲(chǔ)路徑。結(jié)合index.php的邏輯我們可以推斷Flag文件很可能就存放在./uploads/目錄下且文件名可能是flag.php、flag.txt或123.php這類(lèi)常見(jiàn)命名。但highlight_file()函數(shù)默認(rèn)只接受.php擴(kuò)展名的文件且strpos()檢查會(huì)攔截flag字符串。所以直接?fileuploads/flag.php會(huì)觸發(fā)die(No flag for you!)。4.1strpos()的弱類(lèi)型陷阱0與false的微妙差別strpos()函數(shù)的返回值是整數(shù)或false。當(dāng)搜索的子字符串位于目標(biāo)字符串開(kāi)頭時(shí)strpos()返回0。在PHP中0 false為true但0 false為false。WarmUp1的代碼用的是! false這是一個(gè)嚴(yán)格比較要求類(lèi)型和值都相等。所以當(dāng)$file為flag.php時(shí)strpos($file, flag)返回00 ! false為true條件成立程序die()。但如果$file為aflag.php呢strpos(aflag.php, flag)返回11 ! false也為true同樣die()。那有沒(méi)有一種情況讓strpos()返回false有就是當(dāng)子字符串完全不存在時(shí)。所以繞過(guò)思路是讓flag字符串“看起來(lái)存在”但實(shí)際不被strpos()匹配到。方法是URL編碼。?fileuploads%2fflag.php其中%2f是/的URL編碼。strpos(uploads%2fflag.php, flag)會(huì)返回false因?yàn)樽址餂](méi)有連續(xù)的flag字符只有%2fflag。但highlight_file()在解析URL時(shí)會(huì)先解碼%2f為/然后嘗試讀取uploads/flag.php。這就是第二個(gè)經(jīng)典繞過(guò)點(diǎn)。4.2 實(shí)際操作構(gòu)造uploads/flag.php的可讀URL現(xiàn)在組合兩個(gè)繞過(guò)點(diǎn)用php://filter繞過(guò)php檢查用URL編碼繞過(guò)flag檢查。最終URL為http://target/index.php?filephp://filter/readconvert.base64-encode/resourceuploads%2fflag.php訪(fǎng)問(wèn)此URL你會(huì)得到一串base64編碼。解碼后內(nèi)容就是flag.php的源碼。通常flag.php的內(nèi)容非常簡(jiǎn)單比如?php echo flag{hctf_warmup_1_solved}; ?或者它可能是一個(gè)純文本文件沒(méi)有PHP標(biāo)簽直接輸出Flag。無(wú)論哪種base64解碼后都能看到明文。4.3 驗(yàn)證與收尾為什么uploads/flag.php一定存在這個(gè)結(jié)論不是憑空猜測(cè)而是基于CTF題目的設(shè)計(jì)慣例和題目線(xiàn)索的交叉驗(yàn)證。首先!-- upload file to ./uploads/ --是唯一的路徑提示出題人不會(huì)放一個(gè)無(wú)用的注釋。其次index.php里沒(méi)有任何文件上傳邏輯說(shuō)明上傳功能是獨(dú)立的且上傳后的文件必然存放在./uploads/。最后highlight_file()的限制只針對(duì)flag和php字符串而不是針對(duì)路徑所以u(píng)ploads/目錄本身是可讀的。這三個(gè)線(xiàn)索形成閉環(huán)讓你有足夠信心去嘗試uploads/flag.php。在真實(shí)滲透中這叫“基于上下文的合理推測(cè)”比盲目爆破高效得多。5. 從WarmUp1延伸Web安全中的“協(xié)議思維”與日常防護(hù)做完WarmUp1很多人會(huì)覺(jué)得“不過(guò)如此”但它的價(jià)值遠(yuǎn)不止于一道題。它強(qiáng)制你建立一種“協(xié)議思維”——即不再把URL當(dāng)作一個(gè)簡(jiǎn)單的地址而是看作一個(gè)由協(xié)議、主機(jī)、路徑、查詢(xún)參數(shù)組成的結(jié)構(gòu)化數(shù)據(jù)流。php://filter、data://、phar://這些協(xié)議是PHP生態(tài)里最強(qiáng)大也最危險(xiǎn)的特性之一。它們讓開(kāi)發(fā)者能靈活處理數(shù)據(jù)但也為攻擊者提供了豐富的利用面。比如phar://協(xié)議可以觸發(fā)反序列化漏洞zip://可以讀取壓縮包內(nèi)的文件expect://甚至能執(zhí)行系統(tǒng)命令雖然WarmUp1環(huán)境已禁用。理解WarmUp1就是理解整個(gè)PHP協(xié)議生態(tài)的入門(mén)鑰匙。5.1 開(kāi)發(fā)者視角如何避免類(lèi)似的highlight_file()陷阱如果你是Web應(yīng)用的開(kāi)發(fā)者WarmUp1的教訓(xùn)非常直接。第一永遠(yuǎn)不要在生產(chǎn)環(huán)境中暴露highlight_file()、show_source()這類(lèi)調(diào)試函數(shù)。它們應(yīng)該只在本地開(kāi)發(fā)環(huán)境啟用。第二對(duì)用戶(hù)輸入的文件路徑必須使用白名單而非黑名單。比如只允許file參數(shù)為source.php、readme.md等預(yù)設(shè)值用in_array()嚴(yán)格校驗(yàn)。第三路徑拼接時(shí)使用realpath()函數(shù)規(guī)范化路徑再用str_starts_with()檢查是否在允許的目錄內(nèi)徹底杜絕../遍歷。第四禁用危險(xiǎn)協(xié)議。在php.ini中設(shè)置allow_url_fopen Off和allow_url_include Off并移除expect、compress.zlib等非必要協(xié)議。5.2 安全工程師視角strpos()黑名單的系統(tǒng)性失效WarmUp1的strpos()檢查是Web安全中“黑名單失效”的教科書(shū)案例。它的失效不是因?yàn)檫壿嬪e(cuò)誤而是因?yàn)樵O(shè)計(jì)哲學(xué)的根本缺陷。黑名單試圖列舉所有壞的東西但世界是開(kāi)放的攻擊者總能找到你沒(méi)想到的“壞”。而白名單則相反它只定義什么是“好”其余一切默認(rèn)拒絕。在實(shí)際工作中我見(jiàn)過(guò)太多因strpos()、str_replace()、正則替換等黑名單方案導(dǎo)致的RCE遠(yuǎn)程代碼執(zhí)行漏洞。比如用str_replace(system, , $cmd)來(lái)過(guò)濾命令攻擊者只需輸入syssystemtem替換后變成system照樣執(zhí)行。所以我的經(jīng)驗(yàn)是只要業(yè)務(wù)邏輯允許一律用白名單如果必須用黑名單至少要配合多層校驗(yàn)比如先urldecode()再檢查再trim()再htmlspecialchars()形成縱深防御。5.3 CTF選手視角WarmUp1之后的進(jìn)階路徑WarmUp1是HCTF 2018的“暖場(chǎng)題”但HCTF真正的難點(diǎn)在后續(xù)題目。比如[HCTF 2018]warmup2會(huì)引入unserialize()反序列化[HCTF 2018]babyheap轉(zhuǎn)向PWN領(lǐng)域。所以做完WarmUp1后你應(yīng)該立即做三件事第一把php://filter的語(yǔ)法、常用編碼、協(xié)議限制整理成速查表打印出來(lái)貼在顯示器邊第二找一道類(lèi)似的LFI本地文件包含題目比如BUUCTF-[GWCTF 2019]mypassword用同樣的思路復(fù)現(xiàn)第三動(dòng)手寫(xiě)一個(gè)簡(jiǎn)易的PHP Web應(yīng)用故意植入highlight_file($_GET[file])然后用Burp Suite練習(xí)各種繞過(guò)技巧直到你能閉著眼睛寫(xiě)出php://filter/readconvert.base64-encode/resourcexxx。實(shí)戰(zhàn)是最好的老師而WarmUp1就是那把打開(kāi)實(shí)戰(zhàn)之門(mén)的鑰匙。我在實(shí)際做題時(shí)發(fā)現(xiàn)很多高手并不是比別人多懂多少知識(shí)而是比別人多問(wèn)一句“為什么”。比如看到strpos()他們會(huì)想“它的返回值類(lèi)型是什么”看到php://filter他們會(huì)查PHP手冊(cè)確認(rèn)“它是否支持嵌套”看到uploads/他們會(huì)立刻在終端里curl -I http://target/uploads/看返回頭。這種習(xí)慣比任何工具都重要。WarmUp1不難難的是你愿不愿意為一行代碼花十分鐘去讀完它的官方文檔。