)
1. 這題的“坑”其實(shí)埋在 PHP 的語言設(shè)計(jì)里“Make PHP Great Again” 這個(gè)標(biāo)題我第一次看到的時(shí)候第一反應(yīng)是這又是個(gè)用梗的比賽題。wmctf2020 里面這類命名不少主辦方喜歡把一個(gè)看起來輕松的名字扣在一道并不輕松的代碼審計(jì)題上。等真正打開環(huán)境才發(fā)現(xiàn)核心考點(diǎn)不是某個(gè)“神奇的繞過姿勢”而是 PHP 這門語言在過去十幾年里一直被人反復(fù)討論的結(jié)構(gòu)性特性類型轉(zhuǎn)換、魔術(shù)方法、反序列化、動態(tài)調(diào)用。先說結(jié)論這道題所考察的核心場景基本可以收斂為“PHP 反序列化 正則過濾繞過”的組合。當(dāng)然網(wǎng)上能找到的原題源碼版本并不完全統(tǒng)一不同隊(duì)伍賽后還原出來也有細(xì)節(jié)出入。我這里更想講的是按主流寫法還原出的等價(jià)最小用例以及從這個(gè)最小用例延伸開去的排查方法、修復(fù)思路和工程防御手段。換句話說就算你沒有親自參加過這幾場比賽只要把下面這套分析走通以后再遇到任何帶unserialize的代碼你都能快速建立自己的判斷框架。適合看這篇文章的人我覺得有三類第一類是剛開始打 CTF 的選手尤其是對 PHP 題型還沒形成體系的新人第二類是很早就把 PHP 寫成業(yè)務(wù)代碼、但很少做安全審計(jì)的開發(fā)第三類是正準(zhǔn)備梳理面試知識的人因?yàn)?PHP 反序列化、弱類型比較、文件上傳校驗(yàn)這些點(diǎn)幾乎是面試?yán)锢@不開的“基礎(chǔ)知識”。我自己復(fù)盤的時(shí)候最大的感受是很多人把時(shí)間花在了記“payload”上卻沒有先花時(shí)間理解 PHP 為什么會出現(xiàn)這些“奇奇怪怪”的行為。一旦理解了你會發(fā)現(xiàn)所謂的繞過并不是“運(yùn)氣好”而是 PHP 在解析輸入時(shí)的設(shè)計(jì)本就和很多樸素直覺不一致。這就是為什么我覺得“Make PHP Great Again”是個(gè)很妙的題目名它與其說是在喊口號不如說是在提醒你把 PHP 當(dāng)年那些讓你皺眉頭的東西一個(gè)一個(gè)重新?lián)炱饋碚J(rèn)真讀一遍。1.1 為什么 PHP 的考題總是圍繞語言特性寫 Java、Go 或者 Rust 的 CTF 題出題人更多是把考點(diǎn)放在框架漏洞、加密協(xié)議、邏輯漏洞上因?yàn)檎Z言本身幫你擋掉了很多誤用。PHP 不是這樣。PHP 的口號是“為 Web 而生”它的函數(shù)庫極其龐大類型系統(tǒng)又非常寬松加上$_GET、$_POST、$_FILES這些超全局變量直接把用戶輸入鋪在你眼前天然就容易寫出“使用者只需要傳入一個(gè)參數(shù)剩下的交給語言自動處理”的代碼。這種寬松帶來了開發(fā)效率也帶來了很多邊界情況。最典型的就是比較運(yùn)算符和在 PHP 里的行為差異幾乎可以撐起一門單獨(dú)的安全課。字符串1e3在參與比較的時(shí)候會被當(dāng)作數(shù)字處理某些以0e開頭的字符串在哈希比較里又會被當(dāng)成科學(xué)計(jì)數(shù)法結(jié)果數(shù)值恒為 0于是兩個(gè)完全不同的哈希值就能被判定為相等。在 CTF 里這叫“魔法哈?!痹谡鎸?shí)業(yè)務(wù)里如果用它去做簽名校驗(yàn)后果不亞于直接把驗(yàn)證邏輯刪掉。再看反序列化。PHP 提供serialize()和unserialize()是為了方便把對象狀態(tài)存到文件或傳進(jìn) session這本是好意??蓡栴}在于反序列化不只是簡單恢復(fù)數(shù)據(jù)它會在對象被創(chuàng)建時(shí)自動調(diào)用__wakeup在對象被銷毀時(shí)自動調(diào)用__destruct。如果這些魔術(shù)方法里存在危險(xiǎn)操作比如執(zhí)行命令、讀寫文件、調(diào)用方法攻擊者只要精心構(gòu)造一個(gè)序列化字符串就能在不讓程序“主動執(zhí)行攻擊代碼”的情況下完成利用。放在業(yè)務(wù)流程里這就是典型的“數(shù)據(jù)通道被當(dāng)成了代碼通道”。所以我復(fù)盤這道題時(shí)最大的收獲不是學(xué)會了一條繞過正則的路徑而是建立了一個(gè)習(xí)慣看到 PHP 代碼時(shí)先看這個(gè)變量是從哪來的下一個(gè)動作會不會觸發(fā)類型轉(zhuǎn)換、方法回調(diào)或?qū)ο笊芷阢^子。這個(gè)習(xí)慣比背一百條 payload 都值錢。2. 常見 PHP 題面里的五類攻擊入口如果把近幾年的 CTF PHP 題拉一個(gè)清單你會發(fā)現(xiàn)考來考去基本離不開下面幾個(gè)入口。我把它們整理成一個(gè)對照表方便你快速定位一道題大概在考什么也方便你以后審計(jì)代碼時(shí)按圖索驥。類型常見成因CTF 里高頻入口對應(yīng)到真實(shí)項(xiàng)目弱類型比較和混用字符串自動轉(zhuǎn)數(shù)字md5碰撞、0e開頭哈希、數(shù)組與字符串直接比較簽名/口令校驗(yàn)、支付金額判斷反序列化魔術(shù)方法自動調(diào)用serialize/unserialize直接作用于用戶輸入可控參數(shù)進(jìn)入unserialize()配合__destruct/__wakeup觸發(fā)危險(xiǎn)方法把外部傳入的 JSON/緩存數(shù)據(jù)交給不安全的反序列化函數(shù)變量覆蓋與動態(tài)回調(diào)extract()、parse_str()、變量函數(shù)、call_user_func()覆蓋$config、$page、$whitelist等關(guān)鍵變量后包含文件或執(zhí)行方法框架里通過用戶參數(shù)決定調(diào)用哪個(gè)方法、加載哪個(gè)模板文件包含與文件上傳include/require路徑可控上傳后綴和內(nèi)容校驗(yàn)不嚴(yán)LFI本地文件包含、RFI遠(yuǎn)程文件包含、phar://觸發(fā)反序列化、上傳偽裝文件頭像上傳功能只校驗(yàn)了 MIME或上傳目錄放在 Web 根目錄下危險(xiǎn)函數(shù)直接把用戶輸入傳給eval、assert、system、exec、shell_exec一句話木馬、代碼執(zhí)行、命令執(zhí)行后臺為了讓業(yè)務(wù)人員“靈活配置”把用戶規(guī)則交給了eval執(zhí)行這張表看起來是在分析 CTF但每一項(xiàng)都能在真實(shí)項(xiàng)目里找到對應(yīng)版本。我見過不少團(tuán)隊(duì)把反序列化當(dāng)成“緩存技術(shù)”來用用unserialize處理 Redis 里的用戶數(shù)據(jù)卻忘了那些數(shù)據(jù)從哪來也見過上傳組件只查了Content-Type導(dǎo)致攻擊者把一段 PHP 代碼包裝成普通圖片就通過了校驗(yàn)。CTF 題只是把這些問題濃縮在一個(gè)短期場景里核心邏輯和所有業(yè)務(wù)代碼是同一個(gè)道理。2.1 為什么“危險(xiǎn)函數(shù)”防不勝防eval、assert、system、exec、shell_exec這類函數(shù)本身沒有錯(cuò)問題在于“誰給它們傳參數(shù)”。在 PHP 里assert在歷史版本中可以直接把字符串當(dāng)成代碼執(zhí)行很多人會用assert來做簡單的條件判斷但一旦參數(shù)可控就變成了一個(gè)和eval等價(jià)的執(zhí)行點(diǎn)。更麻煩的是有些函數(shù)乍一看不危險(xiǎn)組合起來就危險(xiǎn)call_user_func只是“調(diào)用一個(gè)函數(shù)”但當(dāng)函數(shù)名和參數(shù)都可控時(shí)你就能調(diào)用system并傳進(jìn)任意命令。所以審計(jì)時(shí)我一般會先做一個(gè)“數(shù)據(jù)流追蹤”找出所有能進(jìn)入函數(shù)的變量路徑重點(diǎn)看$_GET、$_POST、$_COOKIE、$_FILES、$_SERVER以及從數(shù)據(jù)庫、緩存、消息隊(duì)列里讀出來的內(nèi)容。很多隊(duì)伍在打 CTF 時(shí)會忽略“二次輸入”的位置比如反序列化數(shù)據(jù)不是直接從請求參數(shù)進(jìn)入的而是先寫進(jìn) session 文件下一次請求再被加載。這種間接路徑往往比直接傳參更難防御也更值得在實(shí)戰(zhàn)中關(guān)注。3. 從反序列化到正則繞過一處處還原處理場景現(xiàn)在進(jìn)入這道題最核心的部分反序列化過濾的繞過。當(dāng)年這道題被討論得很多其中一個(gè)關(guān)鍵點(diǎn)是不少選手手上有現(xiàn)成的 payload卻匹配不上正則然后就開始懷疑是不是類的名字不對、屬性名不對。實(shí)際上問題往往出在 PHP 的unserialize對序列化格式的“過度寬容”上。3.1 先看懂 PHP 序列化字符串的結(jié)構(gòu)PHP 的序列化格式并不神秘一個(gè)對象可以表示成下面這段樣子O:4:Flag:1:{s:3:cmd;s:2:id;}這段字符串拆開來看是這樣的O表示對象類型4是類名的字節(jié)長度Flag是類名1是對象屬性的數(shù)量s:3:cmd是屬性名s表示字符串3是屬性名字節(jié)長度s:2:id是屬性值。很多人第一次手寫序列化字符串時(shí)會栽在“屬性名”上。比如 PHP 類里的屬性名如果是cmd序列化生成的是s:3:cmd不能寫多也不能寫少。如果你處理的是中文還要注意字符串長度按字節(jié)算而不是按字符算一個(gè)中文在 UTF-8 編碼下通常是 3 個(gè)字節(jié)所以序列化里面會出現(xiàn)s:6:你好這種寫法。這道題之所以讓不少人頭疼就是因?yàn)樗麄儗@個(gè)格式的構(gòu)造不夠熟一旦遇到正則攔截又漏掉了這個(gè)符號的存在。3.2 過濾O:的漏洞點(diǎn)加號繞過我再畫一個(gè)等價(jià)的最小環(huán)境。假設(shè)目標(biāo)代碼長這樣?php class Flag { public $cmd; public function __destruct() { system($this-cmd); } } $data $_GET[data]; if (preg_match(/^O:\d/, $data)) { die(blocked); } unserialize($data);這段代碼的邏輯很簡單如果傳入?yún)?shù)以O(shè):加數(shù)字開頭就攔截否則直接反序列化。乍一看正常的序列化字符串確實(shí)都以O(shè):4:之類開頭正則/^O:\d/應(yīng)該能擋住絕大多數(shù)對象輸入。但 PHP 的序列化解析器在識別對象類型標(biāo)記時(shí)非常寬容。你在O和數(shù)字之間插入一個(gè)解析器會照單全收。于是前面那個(gè)正常 payload 可以改寫成O:4:Flag:1:{s:3:cmd;s:2:id;}正則里的意思是O后面必須直接跟冒號、冒號后面必須是數(shù)字。O:4中:的后面是不是數(shù)字所以preg_match(/^O:\d/, O:4:Flag:...)匹配失敗。程序以為自己攔住了實(shí)際上unserialize依然把它當(dāng)成一個(gè)合法的對象去解析。你可以本地跑一段最簡單的驗(yàn)證代碼?php var_dump(unserialize(O:4:Flag:1:{s:3:cmd;s:2:id;}));只要Flag類存在并能順利包含進(jìn)來你就會看到一個(gè)Flag對象被創(chuàng)建。接下來腳本結(jié)束__destruct被觸發(fā)system(id)被執(zhí)行命令結(jié)果直接輸出到頁面。我這里用一個(gè)完全可復(fù)現(xiàn)的小代碼片段來說明原理一點(diǎn)也不依賴當(dāng)時(shí)題目環(huán)境里的文件名、路由和額外配置。這就是反序列化類題目里最常用的“正則繞過”思路過濾條件只針對了“看起來正常”的序列化開頭卻沒有去約束 PHP 解析器對加號的容忍度。這類問題在真實(shí)代碼里也很常見比如做輸入校驗(yàn)時(shí)只攔截了字符串O:卻沒有考慮大小寫、空白、加號、URL 編碼、Unicode 等價(jià)變形這些“同一語義不同寫法”的情況。3.3__wakeup繞過數(shù)字不一致帶來的對象生命周期差異如果你把過濾器改得更嚴(yán)格一些比如已經(jīng)能攔住O:4那么攻擊者還可以考慮另一條路繞過__wakeup方法。__wakeup和__destruct是對象生命周期里的一進(jìn)一出。反序列化創(chuàng)建對象后會先執(zhí)行__wakeup而__destruct會在對象被銷毀時(shí)執(zhí)行。如果一個(gè)類在__wakeup里強(qiáng)制把cmd屬性改掉攻擊者等于拿到了一個(gè)“沒有攻擊能力的對象”。不過在 PHP 7.4 之前的版本里存在一個(gè)反序列化特性當(dāng)序列化字符串中聲明的屬性個(gè)數(shù)大于對象實(shí)際的屬性個(gè)數(shù)時(shí)__wakeup不會被調(diào)用。我按這個(gè)邏輯設(shè)計(jì)一份代碼?php class Flag { public $cmd; public function __wakeup() { $this-cmd echo test; } public function __destruct() { system($this-cmd); } }正常反序列化一個(gè)擁有cmdid的對象__wakeup會把cmd改掉。但如果你提交的是O:4:Flag:2:{s:3:cmd;s:2:id;}注意這里屬性個(gè)數(shù)寫的是2而對象實(shí)際上只有一個(gè)cmd屬性。在 PHP 7.4 之前的版本里這個(gè)不一致會導(dǎo)致__wakeup被跳過對象直接進(jìn)入后續(xù)生命周期。等到腳本結(jié)束__destruct觸發(fā)system(id)照樣執(zhí)行。這個(gè)繞過并不是新東西它對應(yīng) CVE-2016-7124PHP 官方在后續(xù)版本里做了修復(fù)。但在比賽里主辦方經(jīng)常把容器版本固定在 PHP 7.3.x為的就是讓選手有機(jī)會使用這個(gè)繞過??吹竭@里你應(yīng)該能理解為什么我一直強(qiáng)調(diào)“看完代碼還要看環(huán)境版本”一個(gè)在 PHP 8.0 里完全無效的 payload在 PHP 7.3 里可能就是唯一解。3.4 本地用 Docker 復(fù)現(xiàn)比紙上談兵有效得多很多人打 CTF 時(shí)只在腦子里推邏輯一旦 payload 不生效就開始亂試。我的做法是本地快速拉起一個(gè)等價(jià)環(huán)境用 Docker 最省事。例如你手頭有一個(gè)包含上述反序列化代碼的目錄叫php-lab可以這樣跑docker run -d -p 8080:80 -v $PWD/php-lab:/var/www/html php:7.3-apache然后打開http://127.0.0.1:8080/index.php?data...直接觀察輸出。這種方式的好處是能快速驗(yàn)證“加號是否被接受”“__wakeup是否被繞過”“不同 PHP 版本的差異”這些關(guān)鍵結(jié)論。比賽中環(huán)境版本一般能通過響應(yīng)頭、報(bào)錯(cuò)信息或題目描述推測出來本地 Docker 鏡像版本和容器版本保持一致能少走很多彎路。我當(dāng)時(shí)也是這么復(fù)驗(yàn)的先跑一個(gè) php:7.3 容器把上面兩段代碼分別放進(jìn)去再用不同的 payload 去測試。結(jié)果很直觀O:4能過正則O:4:Flag:2能跳過__wakeup。這些結(jié)論如果沒有實(shí)際環(huán)境驗(yàn)證你很難確定它在邊界情況下是否成立。4. 反過來想這套題放到真實(shí)項(xiàng)目中該怎么修CTF 題目的價(jià)值不只在于“打進(jìn)去”還在于“修回去”。把一個(gè)反序列化繞過考到極致最終要回答的問題仍然是真實(shí)項(xiàng)目里我應(yīng)該怎么避免自己掛在同一個(gè)點(diǎn)上4.1 能不用unserialize就盡量不用對絕大多數(shù) Web 項(xiàng)目來說存儲結(jié)構(gòu)化數(shù)據(jù)首選json_encodejson_decode。JSON 格式里沒有“類”的概念危險(xiǎn)魔術(shù)方法就不會被自動觸發(fā)。如果數(shù)據(jù)并不需要某一個(gè)具體類的實(shí)例完全沒必要把整個(gè)對象序列化進(jìn)去。如果確實(shí)需要反序列化PHP 7.0 以后提供allowed_classes參數(shù)可以白名單化允許創(chuàng)建的類$obj unserialize($data, [allowed_classes [UserProfile, CartItem]]);這樣即使攻擊者把序列化數(shù)據(jù)改成O:4:Flag:...反序列化器也會拒絕創(chuàng)建不在白名單里的類。這是我在審計(jì)時(shí)最推薦的最低成本加固方案。4.2 給魔術(shù)方法里的危險(xiǎn)動作“上鎖”即使你用了allowed_classes也沒法保證被允許的類自身沒有問題。審計(jì)時(shí)重點(diǎn)檢查這些類的__wakeup、__destruct、__toString、__call、__get、__set等方法看看它們內(nèi)部是否操作了命令執(zhí)行、文件讀寫、方法回調(diào)、數(shù)據(jù)庫訪問等敏感動作。如果有盡量拆出去不要在反序列化自動觸發(fā)的邏輯里做這些事。以__destruct為例對象銷毀時(shí)機(jī)不可控你很難判斷它發(fā)生在請求哪個(gè)階段更不能假設(shè)“用戶輸入過了過濾所以這里一定安全”。比較好的設(shè)計(jì)是讓這些魔法方法只做“數(shù)據(jù)清理”不要做“數(shù)據(jù)執(zhí)行”。執(zhí)行操作放在顯式調(diào)用的業(yè)務(wù)方法里并單獨(dú)做權(quán)限校驗(yàn)。4.3 把錯(cuò)誤信息關(guān)進(jìn)日志里很多 PHP 項(xiàng)目在出問題時(shí)會直接把錯(cuò)誤打到頁面上這對調(diào)試很方便但在生產(chǎn)環(huán)境等于把內(nèi)部信息免費(fèi)送給攻擊者。一個(gè)小小Warning就能暴露文件路徑、數(shù)據(jù)庫表名、框架版本。你可以這樣設(shè)置error_reporting(E_ALL); ini_set(display_errors, 0); ini_set(log_errors, 1); ini_set(error_log, /var/log/php_errors.log);開發(fā)環(huán)境可以開display_errors生產(chǎn)環(huán)境就必須關(guān)掉。能看到的錯(cuò)誤越少攻擊者做指紋探測和信息收集的難度就越高。這個(gè)點(diǎn)雖然是基礎(chǔ)但在真實(shí)項(xiàng)目里仍然有大量遺漏。每次審計(jì) PHP 代碼時(shí)我都習(xí)慣先看錯(cuò)誤處理配置因?yàn)橐粋€(gè)過度“友善”的報(bào)錯(cuò)頁面往往已經(jīng)幫攻擊者省掉一半的工作量。4.4 文件上傳的幾道閘門都要關(guān)緊搜索的時(shí)候我注意到很多人關(guān)心的另一個(gè)點(diǎn)是“php 上傳漏洞”。這類問題和反序列化一樣本質(zhì)是“把用戶輸入當(dāng)成可信內(nèi)容”。文件上傳至少要過四道閘門擴(kuò)展名白名單不要只攔危險(xiǎn)后綴文件內(nèi)容頭部校驗(yàn)比如判斷圖片的真實(shí)簽名存儲目錄放在 Web 根目錄外部不要給上傳目錄配置 PHP 執(zhí)行權(quán)限文件名隨機(jī)化不要使用用戶提供的文件名。如果是在 nginx 環(huán)境里可以在上傳目錄的 location 里寫死拒絕執(zhí)行 PHPlocation ^~ /uploads/ { location ~ \.php$ { deny all; } }這道配置的意思是上傳目錄下所有以.php結(jié)尾的請求直接拒絕不管文件是原本就叫這個(gè)名字還是通過雙擴(kuò)展名、大小寫變體繞過來的。在 Windows 下還要額外注意x.php.、x.php%20、x.php::$DATA這類文件系統(tǒng)解析差異字符串校驗(yàn)只是第一層真正可靠的還是“目錄里沒有腳本執(zhí)行能力”。4.5 輸出側(cè)的防御也不能忘我看了搜索熱度里還有“php跨域jsonp”“php div彈窗”這類詞。其實(shí)它們都指向輸出側(cè)的同一個(gè)問題數(shù)據(jù)進(jìn)入 HTML、JavaScript、JSONP 回調(diào)時(shí)如果沒有正確編碼就會帶來 XSS。JSONP 在過去的年代很常見但它的回調(diào)函數(shù)名通常由前端或用戶傳入如果服務(wù)端沒做白名單就會變成一個(gè)反射型 XSS 出口?,F(xiàn)在更推薦的做法是能不用 JSONP 就不用用 CORS 白名單域名代替如果必須支持 JSONP回調(diào)名只允許[A-Za-z_][A-Za-z0-9_]*其他一律拒絕。HTML 輸出時(shí)不要偷懶該轉(zhuǎn)義的字段用htmlspecialchars($value, ENT_QUOTES, UTF-8)轉(zhuǎn)義。聽起來像是在講基礎(chǔ)課但真實(shí)問題往往就是從一個(gè)未轉(zhuǎn)義的字段開始一路變成賬號被盜。5. 工程自查清單別等被打了才回頭審計(jì)復(fù)盤一道 CTF 題不是終點(diǎn)我更愿意把里面出現(xiàn)的每一個(gè)考點(diǎn)翻譯成工程自查問題。下面這個(gè)清單是我每次幫團(tuán)隊(duì)做 PHP 代碼頭審計(jì)時(shí)都會過一遍的你可以直接拿來當(dāng)模板。風(fēng)險(xiǎn)點(diǎn)自查問題修復(fù)建議輸入來源哪些函數(shù)直接使用了$_GET、$_POST、$_COOKIE、$_FILES或請求頭入口處統(tǒng)一做參數(shù)白名單/類型校驗(yàn)反序列化項(xiàng)目里有沒有對外部數(shù)據(jù)調(diào)用unserialize()用了什么過濾改用 JSON必須用時(shí)加allowed_classes白名單魔術(shù)方法所有類里__wakeup、__destruct、__toString等方法是否涉及敏感操作把執(zhí)行邏輯從魔術(shù)方法中拆出危險(xiǎn)函數(shù)代碼里有多少處eval、assert、system、exec、shell_exec、call_user_func能用白名單方法列表替代就替代參數(shù)嚴(yán)格約束比較邏輯登錄、驗(yàn)簽、金額判斷用的是還是涉及哈希和數(shù)字判斷一律用SQL 查詢用的是 PDO 預(yù)處理還是字符串拼接全量切到 prepared statement文件上傳上傳目錄是否可執(zhí)行腳本文件名是否可控白名單 隨機(jī)文件名 目錄禁止執(zhí)行 PHP文件包含include/require的文件名是否由用戶控制用映射表或內(nèi)置路由禁止直接拼接路徑錯(cuò)誤輸出生產(chǎn)環(huán)境是否關(guān)閉了display_errors關(guān)掉頁面報(bào)錯(cuò)只寫日志輸出編碼HTML、JavaScript、JSONP 回調(diào)處是否有轉(zhuǎn)義統(tǒng)一htmlspecialchars 回調(diào)名白名單依賴安全composer 依賴?yán)镉袥]有已知反序列化漏洞用composer audit定期掃描及時(shí)升級版本信息PHP 版本是多少是否還處于社區(qū)支持期內(nèi)升級到受支持版本老版本的坑很難靠代碼補(bǔ)齊這個(gè)清單看著很多實(shí)際執(zhí)行起來并不復(fù)雜。先全局搜索unserialize、eval、system、include、require、extract、assert這些關(guān)鍵字把命中點(diǎn)逐個(gè)確認(rèn)一遍再把所有入口參數(shù)整理成一張表最后用自動化工具跑一輪依賴漏洞掃描。重點(diǎn)是形成習(xí)慣而不是真的等到出了事故再返工?!癕ake PHP Great Again”這個(gè)題目本身我后來反復(fù)復(fù)盤過好多次。第一次做的時(shí)候滿腦子都在想怎么把正則繞過去第二次做我開始琢磨為什么 PHP 解析器會對這么寬容第三次做我真正關(guān)心的已經(jīng)變成了“如果這是我自己寫的代碼我應(yīng)該在哪里攔一手”。這個(gè)轉(zhuǎn)變恰好對應(yīng)代碼審計(jì)的三個(gè)層次先看到能打再理解為什么能打最后想明白怎么防。如果你正卡在第一步不用著急回去搭一個(gè)本地環(huán)境把序列化字符串一行一行拆開看就會突然發(fā)現(xiàn)這些題并沒有想象中那么玄幻。