戰(zhàn):Web_php_include偽協(xié)議繞過全解析)
第一次在攻防世界刷Web題的人大概率會(huì)撞上Web_php_include這道題。它不要求你懂多高深的內(nèi)網(wǎng)滲透也不涉及復(fù)雜的密碼學(xué)核心就考一件事你對PHP文件包含和幾個(gè)偽協(xié)議到底熟不熟。這篇WP我按“胎教”標(biāo)準(zhǔn)來寫哪怕你現(xiàn)在只會(huì)用瀏覽器、連Burp都沒裝過跟著一步步做也能把flag拿到。代碼我會(huì)掰開揉碎為什么能繞過、為什么這樣構(gòu)造payload我會(huì)講清楚而不是甩一個(gè)最終payload就完事。練習(xí)環(huán)境提示這篇WP里的所有操作都基于CTF競賽靶場目標(biāo)是你自己賬號下的題目環(huán)境。網(wǎng)絡(luò)安全技術(shù)請只用在自己有授權(quán)的目標(biāo)上別拿去對公網(wǎng)網(wǎng)站做任何測試。1. 先動(dòng)手試環(huán)境長什么樣這題在考什么1.1 打開頁面就能看到的東西把題目環(huán)境打開頁面沒有復(fù)雜的前端交互一進(jìn)來就是一大段高亮顯示的PHP代碼。很多新手看到這一步就懵了“怎么把源碼直接放出來了”其實(shí)這恰恰是題目在放水。代碼大概是這樣的?php show_source(__FILE__); echo br/; $page $_GET[page]; if (isset($page)) { if (strstr($page, php)) { echo no; } else { include($page); } } else { echo please input page parameter; } ?第一行show_source(__FILE__)作用就是把當(dāng)前文件源碼以高亮形式輸出到頁面上。也就是說題目故意讓你看到完整源碼。做CTF題的第一步永遠(yuǎn)是看源碼這一行直接把這一步省了。繼續(xù)往下看$page $_GET[page];是在接收URL里的page參數(shù)。這里的邏輯也很簡單如果沒有傳page參數(shù)就提示“please input page parameter”如果傳了就判斷$page里面有沒有php這個(gè)字符串有就輸出no沒有就include($page)??吹竭@里考點(diǎn)已經(jīng)浮出來了include一個(gè)用戶可控的變量這是非常典型的文件包含漏洞模型。1.2 一行一行讀PHP代碼漏洞點(diǎn)在哪里新手容易在strstr這個(gè)函數(shù)上卡住我先把它講明白。strstr($page, php)做的事情是在$page這個(gè)字符串里查找有沒有php這三個(gè)字符如果有返回從php開始到結(jié)尾的字符串如果沒有返回false。這里把它放到if條件里意思就是只要$page里面出現(xiàn)了小寫php直接被攔下頁面回顯一個(gè)“no”。也就是說題目本意是想阻止你用php://開頭的偽協(xié)議。因?yàn)閜hp://filter、php://input這類地址天然就帶php一多半的常規(guī)解法會(huì)被這個(gè)過濾擋住。同理include這邊是漏洞點(diǎn)。include($page)會(huì)把$page當(dāng)作文件路徑去包含。如果$page是本地路徑比如/etc/passwd它會(huì)把系統(tǒng)文件內(nèi)容讀出來如果$page是一個(gè)URL或偽協(xié)議它就會(huì)走PHP的流包裝器。我建議先做一個(gè)最簡單的本地包含測試在URL后面拼上?page/etc/passwd如果頁面上出現(xiàn)了root:x:0:0:root:...這一類內(nèi)容說明本地文件包含已經(jīng)成立。這時(shí)就可以正式進(jìn)入下一步思考怎么繞過php過濾把這個(gè)include變成能拿flag的武器。1.3 第一次嘗試為什么會(huì)失敗新手的第一反應(yīng)通常是構(gòu)造常規(guī)payload比如?pagephp://filter/readconvert.base64-encode/resourceflag.php提交后頁面果斷回了一個(gè)“no”。原因就是strstr($page, php)檢測到了php://里的php直接攔下。有人會(huì)想把php://換成PHP://行不行這就是關(guān)鍵突破口我后面單獨(dú)講。但現(xiàn)在先冷靜分析一下就算大小寫換成PHP://這個(gè)payload還有沒有別的問題有。resourceflag.php這個(gè)文件名末尾也有php三個(gè)字符。也就是說strstr會(huì)連文件名一起檢查。只要最終URL里出現(xiàn)小寫php都會(huì)被攔。這個(gè)坑很多題解都沒提但它恰恰決定了這道題不能靠“大小寫filter讀flag.php”一步到位。所以真正要解決的是怎么在URL參數(shù)里徹底不出現(xiàn)php這三個(gè)字符又仍然能達(dá)到“包含并執(zhí)行PHP代碼”的效果。2. 不懂偽協(xié)議這題就是死路三個(gè)協(xié)議一次講清2.1 include怎么會(huì)和協(xié)議扯上關(guān)系很多沒接觸過PHP流包裝器的人會(huì)有個(gè)疑問include平時(shí)不是用來包含文件的嗎為什么還能塞一個(gè)php://、data://這種看起來不像路徑的東西因?yàn)樵赑HP里include支持“URL包裝器”。你可以把php://filter、php://input、data://當(dāng)成一種特殊格式的“文件路徑”。當(dāng)PHP看到://它會(huì)調(diào)對應(yīng)的協(xié)議處理器來打開數(shù)據(jù)流。include一個(gè)數(shù)據(jù)流時(shí)如果流里的內(nèi)容是PHP代碼PHP會(huì)在當(dāng)前上下文中執(zhí)行這些代碼如果是普通文本就直接輸出。這個(gè)機(jī)制非常厲害。普通文件包含最多讓你讀到文本有了偽協(xié)議直接可以把“包含一個(gè)文件”變成“執(zhí)行一段任意代碼”也就是從LFI往RCE升級。搭配題目的過濾條件我們要找的協(xié)議是在自身名字和參數(shù)里都不出現(xiàn)小寫php或者能把php藏起來的方案。2.2 php://filter讀源碼的“透視鏡”php://filter是最常用的讀文件協(xié)議之一它的標(biāo)準(zhǔn)格式是php://filter/readconvert.base64-encode/resource目標(biāo)文件比如我要讀當(dāng)前目錄下的flag.php源碼理論上應(yīng)該寫php://filter/readconvert.base64-encode/resourceflag.php它會(huì)先把flag.php內(nèi)容經(jīng)過base64_encode轉(zhuǎn)換再把結(jié)果作為數(shù)據(jù)流交給include輸出。為什么要轉(zhuǎn)成base64因?yàn)閕nclude直接讀PHP文件時(shí)會(huì)把里面的PHP代碼當(dāng)成代碼執(zhí)行你最終看不到源碼而把內(nèi)容base64編碼后PHP解釋器會(huì)把它當(dāng)成普通字符串原原本本顯示出來。拿到base64字符串后再自己解碼就能看到flag.php的原始內(nèi)容。但在這個(gè)題目里php://filter這個(gè)串本身就帶php直接用會(huì)被攔。要繞過就得利用PHP對協(xié)議名稱大小寫不敏感的特性把它改寫成PHP://filter。2.3 php://input把請求體當(dāng)文件讀php://input是一個(gè)只讀流負(fù)責(zé)讀取請求體。也就是說如果URL指向php://input而POST請求體里放了一段PHP代碼include就會(huì)去執(zhí)行這段代碼。使用方式很好理解POST /?pagephp://input HTTP/1.1 Host: 目標(biāo)環(huán)境 ?php system(cat flag.php); ?不過同樣php://input字符串里有小寫php又會(huì)被攔。于是又得用大小寫改寫比如PHP://input。注意POST請求體里的代碼不受strstr檢查影響因?yàn)檫^濾只檢查URL參數(shù)$page。這個(gè)方案的好處是代碼不用base64編碼邏輯清晰適合在Burp或curl里操作。2.4 data://自帶代碼執(zhí)行的新鮮玩法data://是第三個(gè)重要協(xié)議它允許你把一段數(shù)據(jù)直接寫進(jìn)URL里。常見用法有兩種data://text/plain,明文代碼 data://text/plain;base64,base64編碼后的代碼第一種方式很容易翻車因?yàn)槊魑拇a里一旦出現(xiàn)?php這種片段strstr($page, php)會(huì)直接攔下。比如data://text/plain,?php system(ls);?這段URL里含有?php正好包含小寫php必然被攔截。所以要用base64編碼版本把代碼整個(gè)藏起來data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOw這樣URL里就沒有php字樣了可以繞過過濾。這是這道題目前最優(yōu)雅的解法也是網(wǎng)上一票題解的主推方案。2.5 三個(gè)協(xié)議怎么選先看這張表協(xié)議是否受allow_url_include影響本題目中能否繞過strstr過濾用途php://filter否改寫為PHP://filter可繞過但不能在resource中出現(xiàn)小寫php文件名讀源碼php://input是改寫為PHP://input可繞過執(zhí)行任意代碼data://text/plain;base64是base64內(nèi)容不出現(xiàn)php即可直接繞過執(zhí)行任意代碼這里的allow_url_include是PHP配置項(xiàng)決定include能不能包含遠(yuǎn)程URL和部分偽協(xié)議。php://filter屬于內(nèi)置流不受它控制data://和php://input則需要它處于開啟狀態(tài)。所以在CTF環(huán)境里如果data://方案不生效優(yōu)先檢查是不是配置限制。3. 第一招用大小寫繞過PHP://filter看源碼3.1 大小寫繞過到底為什么成立strstr($page, php)是區(qū)分大小寫的。它只認(rèn)小寫的p-h-p三個(gè)字符換成PHP、Php、pHp它都不認(rèn)識(shí)。PHP的流包裝器名稱則相反官方手冊里明確了它不區(qū)分大小寫。也就是說php://filter和PHP://filter對PHP來說是同一個(gè)協(xié)議都能正常解析。于是繞過邏輯就通了strstr區(qū)分大小寫的特性配PHP協(xié)議大小寫不敏感的特性我們把所有php改成PHP過濾就失效了。但這里必須再強(qiáng)調(diào)一次strstr是對整個(gè)$page做檢查不是只檢查協(xié)議前綴。后面跟的resourceflag.php里的小寫php一樣會(huì)被發(fā)現(xiàn)。如果只改協(xié)議部分提交?pagePHP://filter/readconvert.base64-encode/resourceflag.php大概率仍然回顯“no”因?yàn)閒lag.php里藏著小寫php。這就是我在1.3埋下的坑。3.2 實(shí)操把index.php源碼安全讀出來既然不能直接讀flag.php那第一步先讀一個(gè)名字里沒有php的文件最合理的目標(biāo)就是當(dāng)前頁面文件。有些題目環(huán)境里文件叫index.php等等這名字也有php。這就尷尬了index.php照樣有小寫php讀它也會(huì)被攔。那要讀什么才能繞開可以讀info.php嗎也有php。只要文件后綴是.php文件名里必帶php。所以結(jié)論是在這道題的過濾規(guī)則下PHP://filter讀不出任何.php文件。這步操作的唯一價(jià)值是幫我們驗(yàn)證大小寫繞過的思路行不行得通。怎么驗(yàn)證讀一個(gè)非PHP文件比如/etc/passwd用代碼?pagePHP://filter/readconvert.base64-encode/resource/etc/passwd如果返回一段base64解碼出來是root:x:0:0:root:...就證明大小寫繞過成功協(xié)議解析正常。3.3 為什么這條路沒法直接讀flag.php把原因再拆開說一遍strstr($page, php)查的是整個(gè)$page值flag.php文件名的.php后綴躲不掉。只要目標(biāo)是.php結(jié)尾文件大小寫方案就會(huì)二次撞墻。有同學(xué)會(huì)想那我把它再編碼一次比如把flag.php寫成flag%2Ephp%2E是URL編碼的點(diǎn)。問題是PHP拿到$page時(shí)URL解碼已經(jīng)完成%2E會(huì)還原成.最終$page里還是會(huì)出現(xiàn)flag.php。換成fl%61g.php同理解碼后照樣有php。過濾發(fā)生在參數(shù)值解碼之后所以這種二次編碼逃不掉。既然讀源碼這條路被文件名后綴卡死那更聰明的思路是不讀文件直接讓include執(zhí)行代碼。這就引出第四部分的兩種拿flag方式。4. 真正解題兩種姿勢把flag拿下來4.1 姿勢一data://base64一條URL執(zhí)行命令先想清楚目標(biāo)拿到flag最常見的方式就是執(zhí)行命令ls看當(dāng)前目錄有哪些文件然后cat flag.php把內(nèi)容打出來。但命令里只要出現(xiàn)flag.php這個(gè)文件名整個(gè)payload就又包含php了。解決方案很巧妙把整段PHP代碼做成base64塞進(jìn)data://協(xié)議里。strstr看到的是base64字符集字母、數(shù)字、、/、里面沒有小寫php所以過濾直接繞過。第一步在本地把代碼轉(zhuǎn)成base64?php system(ls); ?我在本地命令行執(zhí)行echo -n ?php system(ls); ? | base64得到PD9waHAgc3lzdGVtKCJscyIpOw如果你是Windows環(huán)境或不想用命令行也可以用Python見5.5節(jié)。然后把這段base64拼進(jìn)URL?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCJscyIpOw我截圖般地描述一下結(jié)果頁面上會(huì)直接輸出當(dāng)前目錄的文件列表其中能看到類似flag.php、index.php這樣的文件。到了這里RCE已經(jīng)成功。第二步讀flag。把命令換成cat flag.php但要避免URL里出現(xiàn)php所以繼續(xù)用base64?php system(cat flag.php); ?對應(yīng)的base64為PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg最終payload?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg提交后頁面會(huì)打印出flag.php的源碼內(nèi)容里面通常直接寫著flag{...}。如果頁面只輸出了一部分可以順手用cat flag.php | base64再打一次避免特殊字符顯示問題。4.2 姿勢二PHP://input配合POST body另外一個(gè)思路是用PHP://input。URL里寫PHP://input大小寫繞過了strstr檢查然后在POST請求體里塞PHP代碼。用curl來演示最直觀。假設(shè)目標(biāo)環(huán)境地址是http://目標(biāo)環(huán)境/命令如下curl -X POST -d ?php system(cat flag.php); ? \ http://目標(biāo)環(huán)境/?pagePHP://input這里-d指定POST數(shù)據(jù)PHP://input會(huì)讓include直接讀取剛上傳的請求體也就是?php system(cat flag.php); ?這段代碼。PHP解釋器會(huì)在包含時(shí)執(zhí)行它頁面自然回顯flag。如果手邊有Burp Suite也可以攔截請求后手動(dòng)改成POST在消息體里寫代碼效果一樣。這個(gè)方案最大的優(yōu)點(diǎn)是POST body不參與strstr($page, php)的判斷代碼怎么寫都行完全不受“不能出現(xiàn)php字符”的制約。缺點(diǎn)是操作步驟比data://多一步要用curl或Burp不能像data://那樣直接在瀏覽器地址欄完成。4.3 兩種方式怎么選更適合什么場景對比項(xiàng)data://base64PHP://input是否需要額外工具不需要瀏覽器地址欄直接打需要curl/Burp等能發(fā)POST的工具代碼是否需要編碼必須先base64編碼不用明文直接寫URL里是否會(huì)出現(xiàn)phpbase64內(nèi)容可控不會(huì)出現(xiàn)只寫PHP://input沒有小寫php適合場景快速驗(yàn)證、答題、用hackbar需要跑較復(fù)雜PHP代碼、多行腳本時(shí)我個(gè)人的使用習(xí)慣是答題時(shí)優(yōu)先data://base64快、穩(wěn)、不用裝任何東西調(diào)試復(fù)雜代碼時(shí)優(yōu)先PHP://input代碼可讀性高不用反復(fù)編解碼。兩道經(jīng)典payload打完這道題的flag已經(jīng)到手了。5. 新手易踩的坑實(shí)測排查記錄5.1 base64里的號莫名其妙變成了空格這是我見過最多新手翻車的地方。base64編碼結(jié)果完全有可能包含字符而URL查詢參數(shù)里會(huì)被解析成空格。一旦base64字符串中的變成空格解碼出來的內(nèi)容就是錯(cuò)的include要么報(bào)錯(cuò)要么執(zhí)行不了。舉個(gè)例子對?php system(id);?做base64結(jié)果是PD9waHAgc3lzdGVtKCdpZCcpOz8最后就是一個(gè)。直接放進(jìn)URL?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8PHP拿到的是PD9waHAgc3lzdGVtKCdpZCcpOz8末尾變成空格base64解碼失敗或者解出殘缺內(nèi)容。解決辦法是把編碼成%2B?pagedata://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8%2B更穩(wěn)妥的做法是拿到base64后整段做一次URL編碼再放進(jìn)地址欄。用Burp的話直接把請求發(fā)給Repeater選中base64部分按一下自動(dòng)URL編碼很方便。5.2 訪問flag.php返回空白以為題目壞了如果你直接用瀏覽器訪問http://目標(biāo)環(huán)境/flag.php大概率看到一片空白或者只有一行空內(nèi)容。這不是靶場壞了而是flag.php里的內(nèi)容本身是PHP代碼?php $flag flag{test_flag}; ?它被PHP解釋器執(zhí)行了但沒有輸出任何可見內(nèi)容。$flag這個(gè)變量藏在內(nèi)存里頁面上什么都不顯示。所以要想看到flag必須借助php://filter這類“讀源碼”手段或者執(zhí)行cat命令把文件原文打出來而不是直接訪問.php文件。這個(gè)認(rèn)知能讓你少走很多彎路。5.3 include報(bào)錯(cuò)“failed to open stream”怎么辦如果提交data://或PHP://input后報(bào)錯(cuò)Warning: include(): URL file-access is disabled in the server configuration或者failed to open stream: no suitable wrapper could be found基本可以判定環(huán)境把a(bǔ)llow_url_include關(guān)掉了。data://和php://input都依賴這個(gè)配置項(xiàng)關(guān)掉就廢了。CTF平臺(tái)一般會(huì)為這題開這個(gè)選項(xiàng)但如果碰到變種題就必須換思路。php://filter不受allow_url_include影響所以“讀源碼”這條路通常還是通的。如果data://不能RCE優(yōu)先嘗試用php://filter讀源碼把能讀的文件都讀一遍flag很可能就藏在源碼注釋或配置文件里。再不行就要考慮日志包含、Session文件包含這些進(jìn)階手法。5.4 過濾函數(shù)里還有哪些類似的小坑這道題的過濾函數(shù)是strstr它區(qū)分大小寫給了我們繞過的空間。但如果是strpos呢strpos($page, php)一樣區(qū)分大小寫可以用PHP://繞過。如果過濾條件改成stripos($page, php)它不區(qū)分大小寫大小寫招就失效了。還有一種經(jīng)典坑有人自己寫過濾時(shí)用strpos($page, php) false來判斷“不含php”。PHP里strpos在php出現(xiàn)在字符串開頭時(shí)返回整數(shù)0而0 false在弱比較下是成立的于是直接把合法的php://給放過或者攔錯(cuò)方向。幸好這題用的是strstr沒有這個(gè)坑但你在以后審計(jì)別人代碼時(shí)遇到strpos的和!一定要多留個(gè)心眼。5.5 本地秒算base64的三種方式答題時(shí)反復(fù)編解碼很常見我常用的三種方式Linux/macOS下直接用管道echo -n ?php system(ls); ? | base64Windows PowerShell下[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(?php system(ls); ?))Python一句搞定跨平臺(tái)python3 -c import base64; print(base64.b64encode(b?php system(\ls\); ?).decode())順手提醒一句別用網(wǎng)頁在線base64工具去轉(zhuǎn)換包含payload的代碼首先容易有數(shù)據(jù)泄露風(fēng)險(xiǎn)其次粘貼復(fù)制也容易出錯(cuò)。本地命令轉(zhuǎn)干凈利落。6. 賽后復(fù)盤下次遇到include題怎么秒6.1 拿到include題先列三件事做完這題我復(fù)盤了一套自己的解題流程再遇到文件包含類題目我會(huì)先列三件事第一確認(rèn)過濾規(guī)則。過濾了哪些關(guān)鍵字、是協(xié)議名還是文件名、是不是大小寫敏感、是不是strstr或stripos。這決定了用大小寫繞過還是編碼繞過。第二判斷可用的偽協(xié)議。當(dāng)前環(huán)境下data://和php://input是否可用不行就看php://filter能不能讀源碼。配置錯(cuò)誤提示本身也是一種信息。第三確定目標(biāo)是讀源碼還是RCE。如果flag寫在PHP文件里且沒有其他出口RCE是最穩(wěn)的如果環(huán)境限制多老老實(shí)實(shí)讀源碼找flag。這三件事列完這道題的解題路徑基本就浮出水面了。6.2 但凡是include題都可能有哪些變種文件包含的變種多到寫不完但常見套路就那么幾類過濾協(xié)議名比如php://、data://可以大小寫繞過也可以換協(xié)議、加編碼。過濾://這種更難纏。但可以試試用協(xié)議名稱前綴變形比如data:text/plain;base64,xxx在部分PHP版本里也能識(shí)別或者退一步走日志包含。限制包含路徑比如必須包含某個(gè)目錄下的文件。這種情況可以試目錄穿越../../../../etc/passwd或者結(jié)合文件上傳包含上傳后的圖片馬。還有一類是把用戶輸入放進(jìn)文件名模板比如include(pages/ . $page . .php)。這種就必須用php://filter配合截?cái)嗬霞记苫蛘呃矛F(xiàn)有文件做二次包含。每次看到變種題都把它和我上面列的三件事對照一遍發(fā)現(xiàn)哪個(gè)身份不對路立刻換思路。6.3 防御視角這個(gè)洞是怎么堵上的賽后從防御角度看題目故意留的漏洞其實(shí)很好堵。第一個(gè)層次include的輸入?yún)?shù)必須做白名單校驗(yàn)比如固定允許pagehome、pageabout其它值直接拒絕。第二個(gè)層次如果確實(shí)要?jiǎng)討B(tài)包含也應(yīng)該限制在本地指定目錄用realpath校驗(yàn)包含路徑是否在范圍內(nèi)杜絕偽協(xié)議和穿越。第三個(gè)層次生產(chǎn)環(huán)境務(wù)必把a(bǔ)llow_url_include置為Off并配合open_basedir限制PHP可訪問的目錄范圍。不過話說回來CTF題目的意義就是故意打開一扇門讓我們看清楚攻擊手法長什么樣。真正到真實(shí)環(huán)境你會(huì)遇到更多組合拳比如過濾、白名單、WAF一起上。正因?yàn)榘袌鼋o了我們安全試錯(cuò)的空間才要在答題之外多想一步這種漏洞是怎么被發(fā)現(xiàn)的、怎么被利用的、換成我是運(yùn)維會(huì)怎么防。帶著這三層視角去刷題收獲會(huì)比單純拿flag大得多。最后說點(diǎn)個(gè)人體會(huì)。我第一次刷這道題時(shí)就是敗在flag.php文件名里的php上當(dāng)時(shí)死活不明白為什么已經(jīng)用了PHP://filter還是被攔后來才意識(shí)到strstr檢查的是整個(gè)參數(shù)值不只是協(xié)議前綴。想通這一點(diǎn)后整個(gè)題的所有解法都串起來了過濾規(guī)則要求URL里不能出現(xiàn)php那我就讓URL里一段php都沒有用PHP://input用data://base64思路一下就通透了。新手做CTF題別急著背payload先搞明白過濾依據(jù)是什么再順著依據(jù)找繞法比自己悶頭試要快得多。這道題就很適合作為你Web文件包含方向的入門第一題建議上手敲一遍卡住了再回來看這篇WP。