你按下還原鍵的那一刻,駭客早就埋好雷了:All-in-One WP Migration 漏洞全解析(CVE-2026-19949/CVSS 8.8)
🔥 懶人包:
全球裝機量超過 500萬 站的備份神器 All-in-One WP Migration and Backup,被抓到一個「不用登入就能埋雷,等你自己按下還原鍵才會爆炸」的二階 SQL 注入漏洞(CVE-2026-19949,CVSS 8.8 高風險)。可怕的地方不在攻擊多複雜,而在於它專挑「網站管理員最沒有戒心」的那個瞬間下手——你以為只是在做例行備份還原,其實正親手把炸彈的引信拉開。目前已有公開的概念驗證程式碼流出,強烈建議所有還在用 7.109 以下 版本的網站,立刻升級到 7.110 以上 ☄️
🧊 冷知識:Trackback 這個功能,其實是二十幾年前的產物
你可能沒聽過「Trackback(引用通知)」這個字,但它其實是 2000 年代初期部落格圈的老古董功能,設計初衷是讓不同部落格之間可以互相「打招呼」:A 部落格引用了 B 部落格的文章,B 站就會自動收到一則通知留言。因為當年網路上還沒有現在這麼多惡意流量,所以設計者從一開始就沒打算加上身分驗證。結果二十幾年後的今天,這個幾乎沒人在用、多數人根本忘記它存在的老功能,反而變成這次攻擊鏈路的第一塊骨牌,還蠻諷刺的 💡
內容目錄
🧐 第一章:為什麼單純的「搬家外掛」會變成駭客的遙控炸彈?
先一起來思考個問題:如果有人能在你完全不知情的狀況下,在你家信箱裡塞一張紙條,然後靜靜等你哪天自己把紙條拿去唸給管家聽——你覺得這算不算入侵?聽起來很荒謬,但這次的漏洞玩的正是這一套心理戰 🕳️
WordPress 網站就像一座 24 小時無人自動化倉儲中心,而 All-in-One WP Migration and Backup 則是負責「盤點日大搬遷」的自動搬運機器人系統。這套機器人平常鎖在管制室裡,只有在你按下「開始盤點(執行備份還原)」的那一刻,才會被賦予最高權限,能自由搬動倉庫裡的每一格貨架,甚至改寫貨架上的標籤內容(對應資料庫還原與網址替換)。倉庫大門外掛著一個「意見箱」,任何路人都能投遞紙條進去,機器人在盤點時會把意見箱裡的紙條一併掃進系統歸檔——這個意見箱,就是 WordPress 內建、預設不需要驗證身分的 Trackback 機制 🏭
攻擊者要做的事情很簡單:先寫好一張看起來人畜無害的紙條,塞進意見箱,然後什麼都不做,靜靜等待。這張紙條會被存進資料庫,安安靜靜地躺著,不會觸發任何警報,也不會讓網站當機或跑出錯誤訊息——這正是「二階(Second-Order)注入」最陰險的地方:第一階段的「投遞」跟第二階段的「引爆」是分開的兩個動作,中間可能隔了好幾天,甚至好幾週 ⌛️
💡 碎碎念:這招其實比較像「臥底」,不是「闖空門」
一般人想像中的駭客攻擊,比較像是硬闖進來、馬上翻箱倒櫃——在留言板或登入框打一串怪字元,網站幾乎立刻中招,這種比較像臨時起意的闖空門。但這次的手法不太一樣,它更接近安插一名臥底:先讓惡意內容偽裝成一則普通留言,安分守己地待在資料庫裡,什麼都不做,也不會觸發任何警報。它會一直潛伏著,直到某天你自己觸發了「備份還原」這個完全不相干的功能,臥底才會突然現身動手。這也是為什麼傳統防火牆很難抓到它——留言被貼上去的那一刻,看起來真的什麼事都沒有 🕵️
真正讓機器人「誤讀紙條」的原因,出在它辨識「這句話結束了沒」的方式有瑕疵。機器人只會回頭檢查紙條上「最後一個字」是不是反斜線,卻不會去算反斜線到底出現了偶數次還是奇數次。攻擊者就利用這個弱點,故意在紙條結尾多留一個反斜線,讓機器人誤判「這句話還沒結束」,於是繼續往下讀,把原本應該被當成單純文字內容的部分,硬生生讀成了可以執行的指令。這就是這次漏洞在程式碼層面出包的根源,後面的技術深潛篇會拆給你看細節 💂♂️
⏱️ 從被抓包到修補,中間到底發生了什麼事
這起事件走的是資安圈相對「標準」的通報流程,但速度算是相當快。我們把時間軸攤開來看:
| 日期 | 事件節點 | 詳細內容 |
|---|---|---|
| 2026-08-14 | 漏洞通報 | 資安研究員 Jack Taylor 透過 Wordfence 的漏洞獎金計畫通報這個二階 SQL 注入漏洞,後續拿到 5,761 美元 的高額獎金 |
| 2026-08-15 | 開發商接獲通知 | 漏洞資訊同步給開發團隊 ServMask |
| 2026-08-20 | 官方修補上線 | ServMask 修好正則表達式邏輯缺陷,正式釋出 7.110 安全版本 |
| 2026-08-25 | CVE 正式公開 | NVD 正式對外公布 CVE-2026-19949,CVSS 評為 8.8 高風險 |
| 2026-08-27 | 技術細節與 PoC 現蹤 | SentinelOne、SOCRadar 等多家資安平台發布深度分析;SOCRadar 偵測到網路上已出現 1 個公開的概念驗證程式碼儲存庫 |
從通報到修補只花了 6 天,這個速度算相當不錯。但真正該讓你在意的其實是後半段——資安圈口中的「概念驗證程式碼(PoC)」,你可以直接把它想成「作案教學影片」外流:原本要摸出這道漏洞的前提是你得懂技術、自己花時間摸索,現在等於有人已經把完整步驟拍成教學影片公開放上網,任何懂一點技術的人,都能照著片子依樣畫葫蘆,不用自己重新研究一次 🔎
✅ 目前已確認的事實是:截至報告撰寫時,美國網路安全與基礎設施安全局(CISA)還沒有把這個漏洞列進「已知遭利用漏洞」(KEV)清單,代表目前還沒觀察到大規模、自動化殭屍網路等級的攻擊。🟡 但合理推論是:既然這支「教學影片」已經在外流傳,針對特定高價值目標(例如流量大的電商站、會員資料庫龐大的網站)的「精準狙擊」風險,其實已經比一週前高出不少,這種情況通常不會等太久才轉變成大規模掃描 🔍
😨 講白了,這串攻擊到底能對我的網站幹嘛?
跟大部分「有帳號密碼保護才安全」的直覺相反,這次的威脅模型完全不吃這一套。我們照事實的確定程度,一層一層拆給你看:
- 🏮 ✅ 已確認事實:不用登入就能「埋雷」。攻擊者完全不需要你網站的任何帳號密碼,也不用是訂閱者或編輯者,只要你的網站對外公開,就能透過內建的 Trackback 機制把惡意內容悄悄存進資料庫。這階段不會讓網站有任何異常,傳統防火牆幾乎抓不到。
- 🏮 ✅ 已確認事實:真正的引爆鍵,握在管理員自己手上。惡意內容躺在資料庫裡不會自己發作,它得等到擁有最高權限的管理員登入後台,主動點下「還原」或「匯入」按鈕時,外掛在處理資料的過程中才會不小心觸發。
- 🏮 🟡 合理推論:外掛的機密金鑰可能被外洩,後門隨之而來。一旦被觸發,惡意指令會逼迫系統把外掛專屬的機密金鑰(ai1wm_secret_key)意外暴露在公開的留言區。攻擊者拿到這把鑰匙後,就能偽裝成合法的還原程序,直接上傳藏有木馬的假備份檔,把後門種進網站底層。
- 🏮 🔴 假設情境:如果主機隔離沒做好,可能不只你一個網站遭殃。最壞的狀況是攻擊者透過後門取得伺服器的遠端程式碼執行能力,不只能竄改首頁、偷走客戶名單和交易紀錄,還能把你的網站變成散播勒索軟體或釣魚郵件的跳板。如果你的主機跟其他網站共用同一台伺服器,攻擊者甚至可能藉此橫向入侵,讓整台主機上的網站集體淪陷。
💡 碎碎念:「遠端程式碼執行」聽起來很硬,其實就是「隔空下指令」
你可以把自己的網站想成一間無人商店,平常只有店主(也就是你)才有鑰匙可以進去上架商品、調整貨架。「遠端程式碼執行」講白了,就是駭客不用踏進店裡、不用拿到你的鑰匙,光是坐在自己家裡打幾行字,就能讓店裡的機器人乖乖照做——上架什麼商品、搬走什麼東西、甚至把整間店的電源開關交出去,全部隔空遙控完成。一旦被拿到這個能力,等於店主跟駭客同時握有鑰匙,只是店主自己完全不知道多了一個人可以進出 🔑
🎭 第二章:先別急著慌,先來看看你屬於哪一種情境
如果你已經開始緊張,先深呼吸——不同身分該做的事其實差很多,來看看你最接近哪一種情境。
🙋 我只是負責發文、管會員的網站小編
如果你平常的工作是寫文章、上架商品、回覆客服,完全不碰程式碼跟後台深層設定,那你要做的事其實很單純:確認版本、點更新、必要時先停用外掛。你不需要理解後面 DevOps 那段技術深潛,直接跳到下面「五分鐘無痛自救指南」照著做就好,這不會花你太多時間 💪
👨💻 我自己架站,手上也管著好幾個客戶的 WordPress
如果你身兼數職,同時顧著好幾個網站,光靠手動一個一個點後台更新太沒效率。這種情況下,用 WP-CLI 批次檢查所有站台的外掛版本會實際很多,後面的技術深潛篇會直接給你可以貼上就用的排查指令,建議拉到那一段 👇
🏢 我的網站有會員資料,或是有在線上收款
如果你的網站牽涉到會員個資或電商交易,這件事就不只是「更新一個外掛」這麼簡單了——它同時牽涉到資料外洩通報責任。一旦被證實資料曾經暴露,企業面對的往往不只是商譽受損,還有主管機關的裁罰。除了更新跟重置密碼之外,建議同步啟動內部的資安事件應變流程,把稽核日誌留存下來,之後不管是要跟保險公司對帳、還是配合調查,都用得上 🩺
🧑🔧 DevOps 小知識:為什麼「只停用外掛」有時候還不夠?
單純停用外掛能百分之百切斷這次「還原觸發」的攻擊鏈路,這點沒問題。但如果你懷疑自己已經中招,光停用是不夠的——因為攻擊者留下的後門檔案(例如塞進 mu-plugins 目錄的木馬)跟外掛本身無關,就算你把外掛整個刪掉,後門依然會繼續運作。停用只能防止「新的入侵」,已經進門的東西,還是得靠後面 Checklist 章節的排查手段才清得掉 🛡️
🚦 這個情境最容易被忽略:那個「裝完就再也沒動過」的搬家外掛
一間台灣的中小企業,三年前把網站從舊主機搬到新主機時,裝了這款外掛做一次性資料轉移,搬完之後就再也沒點開過它的設定頁,也沒特別去注意過版本更新。網站主觀認為「反正搬家早就搬完了,這外掛應該只是躺在那邊沒作用」。問題是外掛只要還處於「啟用」狀態,就算你幾乎不使用,攻擊者依然能透過 Trackback 悄悄埋雷,等哪天你臨時想拿它做個緊急備份,地雷就會在你毫無防備的情況下引爆 🆘
🚥 劃重點:「很久沒用」不等於「沒有風險」,只要外掛還在啟用清單裡,攻擊者就有機可乘——這也是這次漏洞最容易被忽略的地方 🙅♂️
🛟 第三章:五分鐘無痛自救指南(電腦小白也能跟著做)
不管你屬於上面哪一種情境,這一段的步驟建議每個人都先跑一遍——不用懂程式,跟著畫面點就好 🤏
- ㊀ 先去確認版本,去哪裡點?
登入你的 WordPress 後台,點左側選單的「外掛(Plugins)」,再點「已安裝外掛(Installed Plugins)」。在整串清單裡找名字叫 All-in-One WP Migration 或後面帶著 and Backup 字樣的那一項,它右側會標示目前的版本號碼 🔢 - ㊁ 看到版本號之後,怎麼判斷自己安不安全?
如果版本號是 7.109 或更舊(例如 7.108、7.105 等),代表你目前正暴露在風險裡;如果已經顯示 7.110 或更新,那就可以先鬆一口氣。判斷標準就是這一個數字 ㊙️ - ㊂ 需不需要馬上更新?
需要,而且優先程度排在今天所有代辦事項的第一位。直接在同一個畫面點「立即更新(Update Now)」按鈕,等它跑完,重新整理頁面確認版本號已經變成 7.110 以上即可 💪 - ㊃ 如果按了更新按鈕卻沒反應,或是根本不敢按怎麼辦?
如果你擔心更新會讓網站版面跑掉、或是按了半天沒動靜,不需要自己硬著頭皮排除,直接把這篇報告轉傳給你的網站維護工程師、外包商,或是主機代管服務商,請他們優先處理這項更新,同時請他們順便檢查有沒有已經被入侵的跡象。找人幫忙是很正常的事,不用不好意思開口 🙇 - ㊄ 如果因為相容性問題暫時真的沒辦法更新,該怎麼辦?
既然這個漏洞一定要靠「執行還原」才會被引爆,只要你把外掛直接點「停用(Deactivate)」,攻擊者佈下的陷阱就完全沒有機會引爆。等未來真的需要用到備份功能時,再評估是否更新後重新啟用 🤔 - ㊅ 如果你在後台完全找不到更新按鈕,或不熟悉這整套流程呢?
請立刻聯絡你的網站建置商、維護團隊或代管主機客服。跟他們溝通時,直接提供「CVE-2026-19949」這個編號,專業技術人員就能立刻理解問題的嚴重性並協助處置。在沒有完整備份的情況下,請不要自己貿然進入主機底層刪除任何檔案❗
🫂 新手求助:完全看不懂「後台」「金鑰」這些詞怎麼辦?
完全沒關係,這篇報告本來就不是要你變成資安專家。如果你連「已安裝外掛」這個選單都找不到,最快的方式就是把整篇報告網址直接轉傳給你的主機商客服或網站外包商,跟他們說「我這個外掛有嚴重漏洞,麻煩幫我更新並檢查」,多數台灣主機商都有提供這類技術支援管道,這件事本來就不該是每個網站主自己一個人扛 🤗
🤿 第四章:技術深潛篇:這串攻擊的程式碼底層到底哪裡壞掉了
接下來這段是給想知道「為什麼會這樣」的人看的——如果你只想知道怎麼補洞,上面那段其實已經夠用了。但如果你也好奇這種等級的漏洞底層長什麼樣子,我們繼續往下拆 ⚔️
| 評估指標 | 具體資訊 |
|---|---|
| 外掛名稱 | All-in-One WP Migration and Backup |
| 開發廠商 | ServMask, Inc. |
| CVE 編號 | CVE-2026-19949 |
| CVSS 評分 | 8.8(High) |
| CVSS 向量 | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| 受影響版本 | <= 7.109 |
| 安全版本 | >= 7.110(2026-08-20 釋出) |
| 漏洞類型 | 二階 SQL 注入(Second-Order SQLi)/CWE-89 |
🧑🔧 DevOps 小知識:CVSS 向量裡的 PR:L 有點誤導
NVD 官方把這條漏洞的「所需權限」標示為 PR:L(低權限),但根據多家資安機構的深度分析,攻擊者在第一階段「植入 Payload」時其實完全不需要任何身分驗證,權限限制只出現在第二階段——必須等擁有高權限的管理員自己觸發還原動作。所以從防禦者的角度來看,還是應該把邊界防護當成「完全未授權攻擊」在防,不能因為向量上寫著 PR:L 就掉以輕心 ⚙️
問題出在外掛處理資料庫還原時,負責識別 SQL 語句裡「字串字面量」邊界的正則表達式寫得不夠嚴謹。當管理員匯入備份封存檔時,外掛必須把來源網站的網址跟資料表前綴,替換成目標網站的設定,這段搜尋與替換邏輯用的是下面這串正則表達式:
preg_replace_callback( "/'(.*?)(?<!\\\\)'/S" , array ( $this , 'replace_table_values_callback' ), $input );
這串語法的核心是 (?<!\\\\)’,屬於「負向後行斷言」,白話翻譯就是:「找一個單引號,前提是它前面那一個字元絕對不能是反斜線」。開發者的用意很單純:如果引號前面有反斜線,就代表這個引號被跳脫過,是字串內容的一部分,不算真正的結尾。立意良好,但漏洞就藏在「只檢查一個字元」這件事上 🔎
MySQL 的語法規定,如果字串本身真的要以反斜線結尾,必須寫成兩個反斜線再加上結尾引號,看起來會像 \\’。當這串正則表達式掃到 \\’ 時,它只往前看一格,發現是反斜線,就直接誤判「這個引號被跳脫了,還沒結束」,於是繼續貪婪地往後匹配,一路吃到下一個沒有反斜線的引號才停下來。這個過程會把原本應該分開的多個欄位、甚至整段 SQL 結構,一併打包送進替換函式;經過解碼、替換、再跳脫這一連串處理後,反斜線的奇偶數平衡被破壞,字串邊界因此位移,原本安分躺在字面量裡的攻擊代碼,就這樣被「提權」成了可以真正執行的 SQL 指令 🔍
🎯 從埋雷到奪權,攻擊者只用了三個階段
整條攻擊鏈路揉合了免驗證輸入、時間差濫用跟 REST API 濫用,拆開來看其實邏輯相當工整 🕵️♀️
- 🏮 第一階段:無驗證埋雷。攻擊者利用 WordPress 內建、預設不需驗證的 Trackback 機制,對一篇允許 Ping 的文章送出兩筆特製請求。其中一筆的 blog_name 欄位刻意以反斜線結尾,並在網址參數裡藏入 SQL 注入片段。WordPress 核心會照單全收,把這些內容存進 wp_comments 資料表,此時 Payload 完全處於休眠狀態,不會有任何動靜。
- 🏮 第二階段:靠「時間預算」耗盡逼出金鑰。由於資料庫還原耗時很長,外掛設計了「時間預算」機制,把 SQL 執行切成一批一批,避免撞上 PHP 的執行時間上限。攻擊者植入的第一筆惡意留言,被設計成一個超級耗運算的內容,目的是刻意榨乾當下這一批次的時間預算,逼外掛提早收工、執行儲存動作——最致命的是,外掛會在這個當下把驗證還原權限用的機密金鑰寫進 wp_options 資料表,準備給下一批次讀取。緊接著下一批次處理到第二筆惡意留言時,前面提到的正則表達式缺陷正式發作,SQL 邊界破裂,夾帶的惡意查詢被執行,精準地把剛剛寫入的金鑰讀出來,再寫進另一則留言的內容裡,並把那則留言標記成「已核准」狀態。攻擊者只需要對公開的 REST API 端點(例如 /wp-json/wp/v2/comments)發一個 GET 請求,就能輕鬆撈到外洩的金鑰。
- 🏮 第三階段:拿到金鑰後直接接管。手握金鑰的攻擊者,等於已經拿到外掛的最高授權。他們直接向外掛的未授權匯入控制器發送請求,因為金鑰比對通過,系統允許他們上傳一個特製的備份壓縮檔,裡面藏著鎖定 mu-plugins(Must-Use Plugins)目錄的 PHP 後門。
🧑🔧 DevOps 小知識:為什麼駭客特別愛挑 mu-plugins 目錄下手?
你可以把 mu-plugins 目錄想成公司裡「不用打卡就能直接進辦公室」的高階主管——放在這個資料夾底下的 PHP 檔案,不需要在資料庫裡被「啟用」,也不會出現在一般外掛清單裡,只要檔案存在,下一個進站的 HTTP 請求就會自動觸發執行,而且是以 Web 伺服器帳號(例如 www-data)的身分跑。對駭客來說,這裡是最不容易被一般管理員注意到、又能立刻取得執行權的黃金地段 🗝️
🟡 合理推論:由於整條攻擊鏈完全利用 WordPress 合法的 Trackback 跟 REST API 通道,特徵比對型 WAF 很難在第一時間攔截;再加上系統真正被攻陷的時間點,剛好落在「執行還原」這個 IT 團隊警覺性通常最低的時刻,等於進一步拉長了發現的延遲。🔴 假設情境:如果伺服器沒有做好租戶隔離或檔案權限限縮,寫進 mu-plugins 的後門甚至可能進一步橫向讀取 wp-config.php 裡的資料庫密碼,或對內網的快取服務進行未授權存取,引發連鎖崩潰 😥
🕵️♀️ 第五章:資安排查 Checklist:DevOps 該怎麼確認自己有沒有已經中招
🤖 重要提醒:以下指令與排查流程由 Gemini 產生、Claude 複查
雖然已盡力對照常見實務經驗與 WP-CLI 官方文件,但仍可能因伺服器環境、權限設定或外掛版本差異而產生疏漏。正式環境(尤其是生產站)的高風險操作——包含更新、停用、刪除檔案、變更權限或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter 🧙
如果你是自己架 VPS、同時管理多個 WordPress,這一節最重要的不是「背一條神奇迴圈指令」,而是先搞清楚自己到底是哪一種 WordPress 架構。搞錯架構,後面的批次指令就算語法完全正確,也可能處理錯對象;更麻煩的是,這次的漏洞屬於「先埋雷、晚點才引爆」的二階注入,就算外掛版本已經衝到 7.110 以上,也不代表舊版暴露期間完全沒有被摸過,因此這一節會依序走過環境判斷 👉 盤點(版本比對)→ Log 日誌排查 👉 後門檢查點 👉 加固檢查五個關卡,每一關都拆成「決策原理 👉 具體指令 👉 預期輸出範例 👉 分支判斷邏輯」四個段落,並且分別涵蓋單一網站、獨立多站、Multisite 三種環境 📜
🔥 一句話先記住:「版本已升級到 7.110」代表你已經跨過這道漏洞的修補門檻,但不等於「網站歷史上從來沒有被摸過」。如果舊版本曾經對外公開超過一段時間,版本修補與入侵排查應該視為兩件不同的工作,缺一不可 🧯
🧑🔧 DevOps小常識:先判斷地圖,再決定走哪條路
「一台伺服器上有 10 個 WordPress」跟「一個 WordPress Multisite 裡面有 10 個 Site」看起來很像,實際上卻是兩種完全不同的架構。前者應該逐一找到各網站的 wp-config.php;後者才應該使用 wp site list。WP-CLI 官方對 wp site list 的定義就是列出 Multisite installation 中的 Sites,不是一般獨立站 🫠
🧭 ㊀ 環境判斷:你到底是在管一個 WordPress,還是一整台 WordPress 伺服器?
決策原理:環境判斷錯誤會直接導致後續盤點、Log 排查、備份、更新全部跑偏——單一站、獨立多站、Multisite 三者的 WP-CLI 參數與資料範圍完全不同,必須先用官方指令確認,而不是憑印象猜 🤔
| 環境 | 典型結構 | 正確思路 | 常用 WP-CLI 定位方式 |
|---|---|---|---|
| 💠 單一網站 | 一個 WordPress 安裝 | 直接在該網站目錄操作 | –path=”$WP_PATH” |
| ❇️ 獨立多站 | 一台 VPS 上有多個完全獨立的 WordPress | 每個 wp-config.php 都代表一個獨立安裝 | –path=”$path” |
| ✴️ Multisite | 一個 WordPress Network,底下有多個 Site | Plugin 檔案屬於整個 Network,不是每個 Site 一份 | –url=”$url” |
💠 單一網站:先確認 WP-CLI 真的站在 WordPress 裡
決策原理:先確認目前路徑真的能載入 WordPress,再往下查外掛版本,這比直接執行一長串批次指令安全許多 🛡️
💡 新手避坑指南:關於路徑與指令替換
接下來的指令中會出現📂 /var/www/example.com/public_html 這類路徑,以及 www-data 這類使用者。如果你用的是 Cloudways,路徑通常會有 public_html;如果是 cPanel,通常在 /home/你的帳號/public_html;👥 記得替換下方的使用者為真實使用者:www-data:www-data。複製貼上執行前,務必將這些變數替換成你主機的真實環境,且指令中的引號包覆(如 “$WP_PATH”)已經過嚴格測試,請勿自行刪除,避免路徑空格導致指令斷裂喔!
WP_PATH="/var/www/example.com/public_html"
if wp --path="$WP_PATH" core is-installed; then
echo "OK: WordPress installation detected."
else
echo "ERROR: WordPress installation not detected."
fi
預期輸出範例:
OK: WordPress installation detected.
分支判斷邏輯:
- ✔️ 顯示 OK 👉 可以進入盤點階段
- 🟠 顯示 ERROR 👉 先停止,不要把錯誤目錄當成網站處理
- 🟡 如果網站路徑含空格或特殊字元,保留 “$WP_PATH” 的雙引號,不要自行刪除,避免指令從中被切斷
❇️ 獨立多站:先用官方 SVN 邏輯思維找出每一個真正的 WordPress
決策原理:HestiaCP、cPanel 或自行管理 VPS 時,「同一台伺服器有很多 WordPress」不代表 Multisite。這時應該從各站的 wp-config.php 建立 Inventory,並取得檔案實際擁有者,避免用單一 Linux 帳號跑遍所有網站造成權限錯亂 ⚙️
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
owner="$(stat -c '%U' -- "$config")"
if sudo -u "$owner" -- wp --path="$path" core is-installed 2>/dev/null; then
printf 'WordPress: %s (owner: %s)\n' "$path" "$owner"
fi
done
預期輸出範例:
WordPress: /var/www/site-a/public_html (owner: site-a) WordPress: /var/www/site-b/public_html (owner: site-b) WordPress: /var/www/site-c/public_html (owner: site-c)
分支判斷邏輯:
- ✔️ 每個路徑都能通過 👉 代表可以建立獨立多站 Inventory
- 🟠 找到 wp-config.php 卻無法載入 WordPress 👉 先確認 PHP、檔案權限或網站路徑,不要直接更新
- 🟡 同一個網站被找到多個 wp-config.php 👉 先人工確認哪個才是 Production,避免誤更新備份或 Staging 副本
✴️ Multisite:這時才把 Network 裡的 Site 列出來
決策原理:wp site list 是 Multisite 專用指令,WP-CLI 官方文件明確將它定義為列出 Multisite installation 中的 Sites,因此不能拿來掃描一台 VPS 上互不相干的獨立多站 📔
WP_PATH="/var/www/network/public_html"
if wp --path="$WP_PATH" core is-installed; then
wp --path="$WP_PATH" site list \
--fields=blog_id,url \
--format=table
else
echo "ERROR: WordPress installation not detected."
fi
預期輸出範例:
blog_id url 1 https://example.com/ 2 https://shop.example.com/ 3 https://blog.example.com/
分支判斷邏輯:能列出多個 Site 👉 進入 Multisite 流程;只能列出單一網站,並不代表一定是獨立多站,還要搭配 wp-config.php 裡是否存在 MULTISITE 常數一起判斷。若不確定,可直接檢查設定檔:
grep -E "MULTISITE|SUBDOMAIN_INSTALL|DOMAIN_CURRENT_SITE|PATH_CURRENT_SITE" "$WP_PATH/wp-config.php"
🧑🔧 DevOps 小知識:「獨立多站」像同一棟大樓裡有三家公司,各自有自己的門、帳冊與倉庫;Multisite 則比較像同一家公司底下有三個部門,外觀看起來都是「三個網站」,但資料庫、Plugin 檔案與更新邏輯完全不同 🏢
🔎 ㊁ 盤點(版本比對):先回答「哪些站裝了這款外掛、目前到底是哪一版?」
目前已知公開資料把 7.109 以下列入受影響範圍,7.110 為官方修補版本。這一節不建議把版本號寫死在腳本裡,而是讓 WP-CLI 直接跟 WordPress.org 官方資料比對,理由跟後面「加固檢查」那節會用到的 verify-checksums 是同一套邏輯:與其自己 grep PHP 原始碼猜版本,不如信任 WordPress 官方的 metadata 系統,這也是延伸查證原始碼修補前後差異時最安全的做法——用官方 checksum/版本比對機制反推「這個檔案是不是真的變成了修補後的版本」,而不是自己重建攻擊流程 🔍
💠 單一網站:不要自己解析 PHP,讓 WP-CLI 回報版本
決策原理:Plugin Inventory 應以 WordPress 自己認得的 Plugin metadata 為主,這樣才能同時拿到「目前版本」與「官方認定的可更新版本」兩個欄位,而不是只有一個寫死的數字 📊
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version
預期輸出範例:
name: all-in-one-wp-migration status: active version: 7.109 update: available update_version: 7.110
分支判斷邏輯:
- 🔴 7.109 或更舊 👉 視為受影響,進入第 4 節備份/應急/修補流程
- 🟢 7.110 或更新 👉 此漏洞的版本條件已解除,但若網站曾經使用過舊版,仍應繼續往下做 Log 與後門排查
- 🟠 找不到 Plugin 👉 先確認是否已被移除、是否改了資料夾名稱,並確認不是查錯了網站路徑
❇️ 獨立多站:逐站查,不要把不同網站混成一份結果
決策原理:每個獨立 WordPress 都有自己的 Plugin 安裝狀態,因此應逐一使用 –path,並保留 owner 資訊,方便後續備份與更新時沿用同一組憑證 🗂️
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
owner="$(stat -c '%U' -- "$config")"
if ! sudo -u "$owner" -- wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
version="$(sudo -u "$owner" -- wp --path="$path" \
plugin get "all-in-one-wp-migration" \
--field=version 2>/dev/null || true)"
if [ -n "$version" ]; then
printf 'Site: %s | Owner: %s | Version: %s\n' "$path" "$owner" "$version"
fi
done
預期輸出範例:
Site: /var/www/site-a/public_html | Owner: site-a | Version: 7.110 Site: /var/www/site-b/public_html | Owner: site-b | Version: 7.109 Site: /var/www/site-c/public_html | Owner: site-c | Version: 7.85
分支判斷邏輯:這份結果就是你的第一張「風險地圖」——Site B、Site C 應優先列入第 4 節修補名單;Site A 可以繼續進行歷史排查,確認舊版暴露期間有沒有留下痕跡 🗺️
🚨 注意:上面的 owner 是從檔案實際擁有者取得,而不是硬編碼 www-data。這對 HestiaCP、cPanel、DirectAdmin、多租戶 VPS 特別重要,因為不同網站可能由不同 Linux 使用者管理;所有路徑變數與檔名一律使用雙引號包覆,避免空格或特殊字元造成指令中斷 ⛓️💥
✴️ Multisite:Plugin 檔案只盤點一次,再從 Site 層確認狀態
決策原理:Multisite 共用同一套 WordPress Plugin 檔案,因此不能把每個 Site 當成一個獨立 Plugin 安裝來反覆查詢版本,這樣只是在浪費查詢次數,也容易讓人誤以為每個 Site 版本可能不同 🧩
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version
wp --path="$WP_PATH" plugin is-active "all-in-one-wp-migration" --network
echo "Network activated exit code: $?"
預期輸出範例:
name: all-in-one-wp-migration status: active version: 7.109 update: available update_version: 7.110 Network activated exit code: 0
分支判斷邏輯:Exit code 0 代表 Network Activated,處理範圍要以整個 Network 為單位;Exit code 1 則代表只在部分 Site 啟用,這時可以再用 –url=”$url” 逐一確認各站啟用狀態,WP-CLI 官方文件也把 –url 定義為 Multisite 指定目標 Site 的標準方式 🧭
📊 ㊂ Log 日誌排查特徵:不是看到 404 就緊張,而是找「路徑+時間+結果」的組合
決策原理:這條攻擊鏈嚴重依賴 WordPress 內建、合法的 Trackback 與 REST API 通道,特徵比對型 WAF 很難第一時間攔截,因此排查 Log 時真正有價值的不是尋找某個「神奇 IP」,而是確認是否曾經出現對應路徑、HTTP 方法與異常回應碼的組合,並且把時間軸跟後面的檔案與帳號排查對齊 🎯
| 排查目標 | 異常特徵/關鍵字 | 潛在惡意行為分析 |
|---|---|---|
| Trackback 濫用 | /wp-trackback.php 的異常 POST 請求 | 留意來自單一 IP 的大量請求、Payload 長度異常大,或參數中夾帶 UNION SELECT 等 SQL 保留字 |
| REST API 探測 | /wp-json/wp/v2/comments 的連續 GET 請求 | 攻擊者觸發引爆後,會透過此端點撈取被標記合法的留言,藉此提取外洩的機密金鑰 |
| 控制器異常呼叫 | admin-ajax.php?action=ai1wm_import | 若發起者未攜帶合法管理員 Cookie 卻能成功呼叫,極可能是拿外洩金鑰嘗試上傳後門 |
💠 單一網站:先找得到 Log,再做關鍵字與時間範圍搜尋
決策原理:不同主機環境的 Access Log 路徑完全不同,因此不要把 /var/log/nginx/access.log 當成所有伺服器的固定答案,先確認檔案是否存在,找不到反而是重要訊號 🔦
LOG_FILE="/var/log/nginx/access.log"
if [ -f "$LOG_FILE" ]; then
grep -E "wp-trackback\.php|ai1wm_import|/wp-json/wp/v2/comments" "$LOG_FILE" |
tail -n 100
else
echo "LOG NOT FOUND: $LOG_FILE"
fi
預期輸出範例:
LOG NOT FOUND: /var/log/nginx/access.log
這不是失敗,而是提醒你:先找出實際 VirtualHost/Web Server 的 Log 路徑。如果找到 Log,可能會看到類似:
... "POST /wp-trackback.php/123 HTTP/1.1" 200 ... ... "GET /wp-json/wp/v2/comments?post=123 HTTP/1.1" 200 ... ... "POST /wp-admin/admin-ajax.php?action=ai1wm_import HTTP/1.1" 403 ...
分支判斷邏輯:
- 🟢 只有零星 Trackback 紀錄、沒有異常 REST/AJAX 呼叫 👉 目前沒有直接證據顯示已被引爆,但仍建議繼續往下做後門檢查
- 🟡 出現對 /wp-json/wp/v2/comments 的密集 GET 👉 提高事件優先級,接著比對同一時間附近的檔案建立/修改紀錄
- 🔴 ai1wm_import 出現 2xx 回應 👉 高度懷疑已被成功呼叫,進入第 4 節「A. 已中招/疑似中招」流程
❇️ 獨立多站:每一個 VirtualHost 都要對應自己的 Log
決策原理:不要只查其中一站,漏洞如果存在於多個獨立 WordPress,任何一站都有可能成為受影響對象,因此要用迴圈掃過整個 Log 目錄 🧵
LOG_ROOT="/var/log"
find "$LOG_ROOT" -type f \
\( -name "*access*.log" -o -name "*access*.log.*" \) \
-print0 |
while IFS= read -r -d '' log; do
matches="$(grep -E "wp-trackback\.php|ai1wm_import|/wp-json/wp/v2/comments" "$log" 2>/dev/null || true)"
if [ -n "$matches" ]; then
printf '\n===== %s =====\n' "$log"
printf '%s\n' "$matches" | tail -n 50
fi
done
預期輸出範例:只會列出有命中的 Log 檔案,方便你把注意力集中在真的需要調查的站 🎯
分支判斷邏輯:某個 Log 出現命中 👉 依檔名或 VirtualHost 設定回推是哪一站,把該站標記為需要進一步鑑識;全部沒有命中 👉 風險降低,但不能證明「絕對沒被摸過」,因為 Log 可能已輪替、遺失,或該站根本使用其他服務(如 CDN/WAF)記錄請求 ♻️
✴️ Multisite:同一份 Web Server Log,從 URL/時間再回推 Site
決策原理:Multisite 通常共用一個 WordPress installation,甚至共用同一個 VirtualHost,因此 Log 排查要把 Host、URL 與 Site 清單對照,而不是只看 Plugin 有沒有啟用 🧷
LOG_FILE="/var/log/nginx/access.log"
if [ -f "$LOG_FILE" ]; then
grep -E "wp-trackback\.php|ai1wm_import|/wp-json/wp/v2/comments" "$LOG_FILE" |
tail -n 100
fi
分支判斷邏輯:找到可疑時間點後,再使用 wp site list –field=url 取得 Network Site 清單,依 Log 中的 Host/URL 判斷哪個 Site 受到影響;這一步不要只看「Plugin 是不是啟用」,因為 Network 中各 Site 的使用狀態可能不同 🔗
🚨 畫重點:Log 裡出現 ai1wm_import 並不等於「一定成功入侵」;同樣地,完全沒有找到這個字串,也不能保證「完全沒被攻擊」。Log 是證據之一,不是唯一證據 🧯
🕳️ ㊃ 後門檢查點:檔案完整性不能只看外表
決策原理:Checksum 驗證只能確認 WordPress.org 有提供 checksum 的外掛檔案,不能證明整個網站「一定沒有後門」——尤其這次攻擊鏈最後一步,是把 PHP 後門寫進 wp-content/mu-plugins 目錄,這個目錄完全不受一般外掛版本檢查涵蓋,必須額外檢查 mu-plugins、資料庫留言內容、使用者帳號與近期異動檔案 🕵️
💠 單一網站:先查 mu-plugins,再查資料庫留言與帳號
WP_PATH="/var/www/example.com/public_html"
MU_PLUGINS="$WP_PATH/wp-content/mu-plugins"
if [ -d "$MU_PLUGINS" ]; then
find "$MU_PLUGINS" -maxdepth 2 -type f -iname "*.php" -print
else
echo "MU-PLUGINS DIRECTORY NOT FOUND: $MU_PLUGINS"
fi
預期輸出範例:
/var/www/example.com/public_html/wp-content/mu-plugins/【可疑檔案】.php
資料庫留言與異常管理員帳號排查(用 heredoc 把 SQL 寫成獨立暫存檔,避免多層跳脫符號互相干擾):
# 使用 heredoc 確保 SQL 不受引號干擾,維持邏輯閉環
WP_PATH="/var/www/example.com/public_html"
QUERY_FILE="$(mktemp)"
cat > "$QUERY_FILE" <<'EOF'
SELECT comment_ID, comment_author, comment_date, LEFT(comment_content, 80) AS preview
FROM wp_comments
WHERE comment_content LIKE '%UNION SELECT%'
OR comment_content LIKE '%CONCAT(%'
OR CHAR_LENGTH(comment_content) > 500
ORDER BY comment_date DESC
LIMIT 20;
EOF
wp --path="$WP_PATH" db query < "$QUERY_FILE"
rm -f -- "$QUERY_FILE"
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=table
預期輸出範例:
+------------+----------------+---------------------+-------------------------+ | comment_ID | comment_author | comment_date | preview | +------------+----------------+---------------------+-------------------------+ | 4821 | pingback | 2026-07-30 03:14:02 | ...UNION SELECT... | +------------+----------------+---------------------+-------------------------+ ID user_login user_email display_name user_registered 1 admin admin@example.com Site Admin 2026-01-10 08:30:00 7 backupadm ops@example.com Backup Admin 2026-06-20 11:00:00
🧑🔧 DevOps 小知識:這裡刻意用 cat <<‘EOF’(單引號 EOF)把 SQL 內容寫成獨立的暫存檔,而不是把整段 SQL 直接塞進雙引號字串裡,原因是 UNION SELECT、CONCAT( 這類字元如果混在 Shell 雙引號中,很容易跟 Shell 自己的變數展開、跳脫規則打架,heredoc 反而是最不容易寫錯的方式 📜
分支判斷邏輯:
- 🔴 mu-plugins 出現非部署流程建立的 PHP 檔案,尤其檔名為亂數字串 👉 不要直接刪除,先保留檔案時間、權限、雜湊與副本供鑑識,並進入第 4 節「A. 已中招」流程
- 🟡 資料庫留言內容出現大量反斜線或明顯 SQL 保留字 👉 標記該筆留言 ID,之後清理時只針對這幾筆處理,不要整個 wp_comments 資料表清空
- 🟢 找不到可疑 mu-plugins 檔案、留言內容也正常 👉 這一項沒有發現異常,但任何不認識的管理員帳號仍值得進一步確認,「陌生」不等於「惡意」,可能是維運商或既有帳號
❇️ 獨立多站:每站各自檢查,結果要帶上網站路徑
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
owner="$(stat -c '%U' -- "$config")"
mu_plugins="$path/wp-content/mu-plugins"
if [ ! -d "$mu_plugins" ]; then
continue
fi
hits="$(find "$mu_plugins" -maxdepth 2 -type f -iname "*.php" -print)"
if [ -n "$hits" ]; then
printf '\n===== SUSPICIOUS MU-PLUGINS: %s (owner: %s) =====\n' "$path" "$owner"
printf '%s\n' "$hits"
fi
done
分支判斷邏輯:每個站獨立判斷,不要看到 A 站乾淨就把 B、C 站一起視為乾淨;找到命中的站優先進入事件調查,其餘站繼續走一般盤點流程 🧭
✴️ Multisite:mu-plugins 屬於 Network,一份檢查即可,帳號要分 Network/Site 兩層看
WP_PATH="/var/www/network/public_html"
MU_PLUGINS="$WP_PATH/wp-content/mu-plugins"
if [ -d "$MU_PLUGINS" ]; then
find "$MU_PLUGINS" -maxdepth 2 -type f -iname "*.php" -print
fi
wp --path="$WP_PATH" user list \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=table
分支判斷邏輯:發現可疑 mu-plugins 檔案 👉 把整個 Network 都納入事件調查範圍,因為 mu-plugins 是 Network 共用資源;發現未知帳號後,再進一步確認它屬於哪個 Network/Site 層級的權限,不要看到帳號存在就直接刪除 🧾
🚨 Checksum 驗證不是完整的入侵鑑識。
它主要驗證 WordPress.org 能提供 checksum 的 Plugin 檔案;它不能證明整個網站「一定沒有後門」。尤其如果網站已經疑似遭入侵,仍然必須檢查 mu-plugins、uploads、Themes、其他 Plugin、WordPress 使用者與 Web Server Log,這五項缺一不可 🔐
🔧 ㊄ 加固檢查:就算修好了,也把第二道門鎖起來
決策原理:這起漏洞的根本教訓之一,是攻擊鏈完全利用 WordPress 內建、合法的 Trackback 與 REST 通道,因此加固不能只靠「更新外掛」單一動作,還要從伺服器層與檔案完整性兩個角度收斂攻擊面。這裡也是「延伸查證原始碼修補前後差異」最務實的落地方式:不去手動比對攻擊用的正則表達式邏輯,而是讓 WP-CLI 直接向 WordPress.org 官方 SVN 提供的 checksum 資料庫比對,藉此反推「目前這個檔案,是不是真的已經變成官方修補後的版本」🔒
💠 單一網站:外掛 Checksum 驗證(反推是否為官方修補版本)
WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" plugin verify-checksums "all-in-one-wp-migration"
預期輸出範例:
Success: Verified 1 of 1 plugins.
分支判斷邏輯:
- 🟢 Verified 👉 目前安裝的外掛檔案與 WordPress.org 官方版本一致,可信度高
- 🔴 Checksum mismatch 👉 不要忽略,代表檔案內容跟官方版本不同,可能是有人手動修改過外掛檔案(例如被植入額外程式碼),應立即進一步做入侵鑑識
- 🟡 Checksum 無法驗證 👉 不代表一定遭入侵,可能是 WordPress.org 沒有對應版本的 checksum,或版本來源並非官方目錄,這時可改用官方 SVN/GitHub Release 頁面人工比對版本標籤
Core Checksum 也建議一併確認,避免只盯著外掛卻忽略了 WordPress 本體:
wp --path="$WP_PATH" core verify-checksums
❇️ 獨立多站:逐站驗證,結果保留路徑方便比對
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
owner="$(stat -c '%U' -- "$config")"
if ! sudo -u "$owner" -- wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
printf '\n===== CHECKSUM: %s =====\n' "$path"
sudo -u "$owner" -- wp --path="$path" plugin verify-checksums "all-in-one-wp-migration"
done
分支判斷邏輯:每個網站獨立判斷,不要拿 A 站的 Checksum 結果代替 B、C 站;任何一站出現 mismatch,就先暫停該站的自動化排程,避免可疑檔案被排程覆寫掉,干擾後續鑑識 ⏸️
✴️ Multisite:Plugin 檔案共用,Checksum 也只驗證一次
WP_PATH="/var/www/network/public_html" wp --path="$WP_PATH" plugin verify-checksums "all-in-one-wp-migration" wp --path="$WP_PATH" core verify-checksums
分支判斷邏輯:Plugin 與 Core 檔案屬於同一個 WordPress installation,因此不需要對每個 Site URL 重複驗證;驗證通過後,才進入「伺服器層防護規則」與「權限最小化」兩項加固 🧱
伺服器層防護建議(Nginx 範例,路徑與規則請依實際站點調整,套用前先在 Staging 驗證):
location ~* "/wp-trackback\.php" {
deny all;
return 403;
}
📂 記得替換下方的目錄為真實網站目錄:/var/www/
👥 記得替換下方的使用者為真實使用者:admin:admin
檔案權限最小化與危險函數收斂:
WP_PATH="/var/www/example.com/public_html"
find "$WP_PATH" -type d -exec chmod 755 {} \;
find "$WP_PATH" -type f -exec chmod 644 {} \;
chmod 600 "$WP_PATH/wp-config.php"
🧑🔧 DevOps 小知識:建議同步檢查 php.ini 的 disable_functions,考慮禁用 exec、passthru、shell_exec、system、proc_open、popen,這樣即使萬一有後門被植入,實質破壞力也會被收斂不少;另外若業務允許,可在後台「討論」設定關閉「允許來自其他網誌的連結通知」,從源頭減少 Trackback 攻擊面 ⚙️
🫂 新手求助:如果不熟悉 WP-CLI 或 SSH 操作,可以直接參考 WP-CLI 官方文件的 plugin verify-checksums、core verify-checksums 指令頁面,上面有完整的參數說明與範例,或請你的主機商客服協助執行這一節的排查指令 🤗
📋 ㊅ 第 3 節速查表:三種架構的排查地圖
| 階段 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| ① 環境判斷 | wp core is-installed | 逐站 wp-config.php | wp site list |
| ② 盤點(版本) | plugin get | 逐站 –path | Network Plugin+–network |
| ③ Log 排查 | 單一 Access Log | 逐 VirtualHost Log | 共用 Log+URL 比對 |
| ④ 後門檢查 | mu-plugins+留言+帳號 | 逐站 mu-plugins | Network mu-plugins+Site 帳號 |
| ⑤ 加固檢查 | Checksum+權限+WAF | 逐站 Checksum | Network Checksum 一次 |
🔥 核心原則:「批次」不代表「把所有東西塞進一條 Bash 迴圈」。真正安全的批次排查,是先判斷環境,再逐層縮小範圍,尤其多站 VPS 與 Multisite 必須分開處理,否則很容易把正確的 WP-CLI 指令用在錯誤的架構上 🎯
🤔 讀到快喘不過氣?常見焦慮 QA 大解密
看到這邊,如果是行銷小編或老闆,可能已經覺得頭昏眼花了。別緊張,我們整理了幾個大家最常問的焦慮點,幫你壓壓驚:
- 🏮 Q:按了更新,網站版面會不會跑掉或當機?
A:All-in-One WP Migration 是一個相對獨立的「工具型」外掛,不負責前台版面渲染。單純更新這個外掛讓前台跑掉的機率微乎其微。但保險起見,動手前先備份資料庫絕對是不變的真理喔 💾 - 🏮 Q:外包廠商說「這功能我們又沒在用,不用管它」,我該聽他的嗎?
A:千萬別信這句話!漏洞最狡猾的地方在於「只要外掛還啟用著」,就算你沒在用,駭客一樣能把炸彈塞進去。請堅定地要求廠商協助升級 💣
🛠️ 第六章:修不了就先擋:短期應急與正式修補三部曲
前一節解決的是「怎麼判斷」,這一節則進入真正的維運 Runbook。無論你屬於哪一種情境,強制流程都是同一套:盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證,每個階段都必須完成,才能進入下一階段,正式環境升級外掛前,務必先於測試環境備份並驗證,避免網站崩潰 🧹
| 階段 | 目的 | 核心動作 |
|---|---|---|
| ① 判斷環境 | 避免操作錯網站 | 單站/獨立多站/Multisite |
| ② 盤點 | 知道誰受影響 | 版本、Plugin、Site Inventory |
| ③ 備份 | 保留回復點 | DB+必要網站檔案 |
| ④ Dry-run | 先看更新會做什麼 | –dry-run |
| ⑤ 更新 | 關閉漏洞 | 升級至 7.110 或更新版本 |
| ⑥ 驗證 | 確認修補真的完成 | 版本+Checksum+功能+Log |
🔥 這裡最重要的閉環:
判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證。簡言之:不要把「更新成功」當成整件事結束 🙅
🚨 ㊀ A. 已中招/疑似中招:先止血再修補
決策原理:一旦第 3 節排查出現明確跡象(mu-plugins 可疑檔案、留言資料異常、REST/AJAX 出現 2xx 命中),優先目標是「切斷攻擊鏈+保留證據+隔離後門」,而不是立刻更新,更新只是後續步驟;最大的敵人不是漏洞本身,而是在沒有留下證據的情況下直接清理,反而把最有價值的線索一起刪掉 🧯
💠 單一網站:先建立事件證據,再止血
WP_PATH="/var/www/example.com/public_html"
INCIDENT_DIR="$HOME/ai1wm-incident-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$INCIDENT_DIR"
cat > "$INCIDENT_DIR/summary.txt" <<EOF
=== Incident collection ===
$(date -Is)
WP_PATH=$WP_PATH
$(wp --path="$WP_PATH" core version)
$(wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version)
EOF
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=csv \
> "$INCIDENT_DIR/administrators.csv"
find "$WP_PATH/wp-content/mu-plugins" -type f \
-printf '%TY-%Tm-%Td %TH:%TM:%TS %u:%g %m %p\n' \
2>/dev/null \
> "$INCIDENT_DIR/mu-plugins-inventory.txt"
echo "Incident evidence: $INCIDENT_DIR"
預期輸出範例:
Incident evidence: /home/admin/ai1wm-incident-20260904_121500
分支判斷邏輯:證據已建立 👉 接續下方止血動作;如果是企業、電商或會員制網站,同步通知內部資安或維運負責人,並保留這份 Incident Directory 到受保護的位置 📦
止血:隔離可疑 mu-plugins 檔案(先隔離,不要直接刪除)、強制刷新安全性鹽值「Salt」、變更管理員密碼:
WP_PATH="/var/www/example.com/public_html"
QUARANTINE_DIR="$HOME/ai1wm-quarantine-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$QUARANTINE_DIR"
find "$WP_PATH/wp-content/mu-plugins" -type f -iname "*.php" -mtime -30 -print0 |
while IFS= read -r -d '' f; do
printf 'Quarantine candidate: %s\n' "$f"
chmod 000 -- "$f"
mv -- "$f" "$QUARANTINE_DIR/"
done
wp --path="$WP_PATH" config shuffle-salts
wp --path="$WP_PATH" user list --role=administrator --field=user_login |
while IFS= read -r login; do
printf 'Resetting password for: %s\n' "$login"
wp --path="$WP_PATH" user reset-password "$login" --skip-email
done
分支判斷邏輯:隔離與重置動作全部成功 👉 進入清除資料庫殘留內容與正式修補流程;任一步驟失敗(例如檔案權限不足) 👉 先解決權限問題,不要略過這一步直接升級外掛版本,否則同一個入侵路徑可能再次被利用 🔁
❇️ 獨立多站:只隔離有證據的站,避免整台 VPS 一起誤傷
決策原理:獨立多站的最大優勢就是隔離性。某一站疑似遭入侵時,先確認受影響網站,再依主機架構進行站點級隔離,不要因為緊張就把整台 VPS 上所有網站一起停用 🎯
INCIDENT_PATH="/var/www/site-b/public_html"
INCIDENT_OWNER="$(stat -c '%U' -- "$INCIDENT_PATH/wp-config.php")"
if sudo -u "$INCIDENT_OWNER" -- wp --path="$INCIDENT_PATH" core is-installed; then
echo "Incident target confirmed: $INCIDENT_PATH (owner: $INCIDENT_OWNER)"
else
echo "ERROR: target is not a WordPress installation."
fi
分支判斷邏輯:確認路徑與擁有者正確 👉 對這一站套用單一網站流程中的證據保存與隔離指令;不要把整個 /var/www 當成單一網站處理,也不要因為同一台 VPS 其他站看起來正常,就跳過對它們的第 3 節排查 🧭
✴️ Multisite:先把事件視為 Network 級問題
決策原理:Multisite 共用核心、Plugin 與 mu-plugins 目錄,因此某個 Site 出現入侵跡象時,不能只處理那個 Site 就宣稱「完成」,必須把整個 Network 納入事件範圍 🌐
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" site list \
--fields=blog_id,url \
--format=table
wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version
find "$WP_PATH/wp-content/mu-plugins" -type f \
-printf '%TY-%Tm-%Td %TH:%TM:%TS %u:%g %m %p\n' \
2>/dev/null
分支判斷邏輯:任一 Site 出現明確入侵跡象 👉 對整個 Network 執行證據保存、mu-plugins 隔離與鹽值「Salt」刷新;接著再逐一檢查各 Site 的管理員帳號是否有異常,不要遺漏權限層級較低但仍具備發文能力的帳號 🔍
🧯 ㊁ B. 短期應急:尚未確認中招,但現在還不能立刻升級
🚨 短期應急 ≠ 正式修補。
停用 Plugin、伺服器層防護規則都屬於緩衝措施,目標是縮短攻擊面暴露時間,真正的修補仍然是安裝 7.110 或更新版本 🧱
💠 單一網站:Plugin 能停用時,優先降低暴露面
決策原理:如果網站目前根本不需要用到搬家/備份功能,而正式更新必須等維護窗口,停用受影響 Plugin 通常比讓它持續暴露在公網更容易控制,而且可以百分之百截斷「還原觸發」這條攻擊鏈 💪
WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" plugin deactivate "all-in-one-wp-migration"
預期輸出範例:
Plugin 'all-in-one-wp-migration' deactivated. Success: Deactivated 1 of 1 plugins.
分支判斷邏輯:
- 🟢 可以安全停用 👉 保留停用狀態,盡快排入第 4 節「C. 正式修補」窗口
- 🟠 網站核心流程依賴它(例如排程自動備份) 👉 不要在 Production 直接停用,改採下方伺服器層防護規則,並儘速安排升級
❇️ 獨立多站:逐站應急,不要一次停掉整台 VPS 的所有站
INCIDENT_PATH="/var/www/site-b/public_html" INCIDENT_OWNER="$(stat -c '%U' -- "$INCIDENT_PATH/wp-config.php")" sudo -u "$INCIDENT_OWNER" -- wp --path="$INCIDENT_PATH" plugin deactivate "all-in-one-wp-migration"
分支判斷邏輯:這個指令只影響 Site B,不應連 Site A、Site C 一起停掉;如果要批次處理多個高風險站,建議先跑第 3 節的盤點迴圈,把版本落後的站篩出清單,再針對清單逐一停用,而不是無差別對整個 /var/www 下手 📋
✴️ Multisite:先確認 Plugin 是否 Network Activated,再決定停用範圍
WP_PATH="/var/www/network/public_html"
if wp --path="$WP_PATH" plugin is-active "all-in-one-wp-migration" --network; then
echo "Plugin is Network Activated."
else
echo "Plugin is not Network Activated."
fi
預期輸出範例:
Plugin is Network Activated.
分支判斷邏輯:如果是 Network Activated,停用時要把整個 Network 視為影響範圍,不能以為只關閉某個 Site 就能把共用 Plugin 檔案隔離;如果只是部分 Site 各自啟用,才適合用 –url=”$url” 針對個別 Site 停用 🔀
伺服器層防護規則(Nginx 範例,套用前請先在 Staging 驗證,且僅在確認該站使用此外掛時套用):
location ~* "/wp-trackback\.php" {
deny all;
return 403;
}
location ~* "/wp-admin/admin-ajax\.php" {
if ($arg_action = "ai1wm_import") {
return 403;
}
}
✅ 為什麼停用就能擋?
整條攻擊鏈嚴重依賴「還原流程」才會引爆第二階,停用外掛能百分之百截斷觸發鏈路,是目前最乾淨、風險最低的短期緩解手段,但不能取代正式升級 🏎️
㊂ C. 正式修補:強制流程「判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證」
🩵 荷包試算:這幾道防線要花多少錢?
伺服器層設定與停用外掛本質上都是免費操作,只要有 SSH 權限就能自己動手。真正會花到錢的地方,通常是「已經中招之後」——請資安顧問做鑑識、清除後門的工時,或是網站被列入安全警示黑名單之後重建流量的成本,這筆帳,怎麼算都是現在就花時間走完這套 Runbook 比較划算 💸
以下流程同時涵蓋單一網站、獨立多站、Multisite 三種環境,每一階段都必須完成才能進入下一階段,正式環境升級外掛前,務必先於測試環境備份並驗證,避免網站崩潰 🧪
① 判斷環境(再次確認)
回到第 3 節「㊀ 環境判斷」執行判斷,確認自己屬於哪一種架構後再繼續,不要在還沒確認架構前就執行下面任何批次指令。
② 盤點(Inventory)
沿用第 3 節「㊁ 盤點(版本比對)」的三段指令,先確認哪些站真正安裝了受影響版本,把結果列成清單,不要在第一個迴圈裡直接更新。
③ 備份(Backup)
決策原理:外掛更新主要改的是 Plugin 檔案,但網站內容、設定與媒體檔案仍可能需要回復,因此至少要有資料庫備份,並依既有維運政策保留必要檔案,備份檔案統一集中放在 ~/wp-security-backup 💾
💠 單一網站:
WP_PATH="/var/www/example.com/public_html"
BACKUP_DIR="$HOME/wp-security-backup"
STAMP="$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
wp --path="$WP_PATH" db export \
"$BACKUP_DIR/example-com_pre_ai1wm_update_${STAMP}.sql"
tar -czf \
"$BACKUP_DIR/example-com_wp-content_pre_ai1wm_update_${STAMP}.tar.gz" \
-C "$WP_PATH" \
"wp-content"
printf 'Backup completed:\n%s\n' "$BACKUP_DIR"
預期輸出範例:
Success: Exported to '/home/admin/wp-security-backup/example-com_pre_ai1wm_update_20260904_123000.sql'. Backup completed: /home/admin/wp-security-backup
分支判斷邏輯:SQL 與檔案備份都存在 👉 進 Dry-run;任一備份失敗 👉 停止,不進入正式更新,先排除磁碟空間或權限問題 🛑
❇️ 獨立多站:每一個 WordPress 各自一份資料庫備份
SEARCH_ROOT="/var/www"
BACKUP_ROOT="$HOME/wp-security-backup"
STAMP="$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_ROOT"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
owner="$(stat -c '%U' -- "$config")"
site_name="$(basename -- "$path")"
db_file="$BACKUP_ROOT/${site_name}_pre_ai1wm_update_${STAMP}.sql"
if ! sudo -u "$owner" -- wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
echo "=== Backup: $path ==="
sudo -u "$owner" -- wp --path="$path" db export "$db_file"
printf 'Database backup: %s\n' "$db_file"
done
預期輸出範例:
/home/admin/wp-security-backup/site-a_pre_ai1wm_update_20260904_124000.sql /home/admin/wp-security-backup/site-b_pre_ai1wm_update_20260904_124000.sql /home/admin/wp-security-backup/site-c_pre_ai1wm_update_20260904_124000.sql
分支判斷邏輯:某一站備份失敗,就把該站標記為未完成,不要因為其他站成功就直接進入批次更新 ⛔
✴️ Multisite:Network Database 不要重複 dump N 次
WP_PATH="/var/www/network/public_html"
BACKUP_DIR="$HOME/wp-security-backup"
STAMP="$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
wp --path="$WP_PATH" db export \
"$BACKUP_DIR/multisite_pre_ai1wm_update_${STAMP}.sql"
tar -czf \
"$BACKUP_DIR/multisite_wp-content_pre_ai1wm_update_${STAMP}.tar.gz" \
-C "$WP_PATH" \
"wp-content"
🚨 不要在 Multisite 的每個 URL 上重複執行整個 wp db export。
這很可能只是把同一個 Network Database 重複 dump 多次,既浪費空間,也沒有增加實質備份價值。WP-CLI 的 wp db export 是使用 wp-config.php 中的 DB credentials 呼叫 mysqldump,對 Network 執行時預設匯出的就是整個 installation 使用的資料庫 🗄️
④ Dry-run(正式修改前先預覽)
決策原理:WP-CLI 官方的 wp plugin update 原生支援 –dry-run,用途就是預覽哪些 Plugin 會被更新,而不真正執行更新,這是進入正式更新前最後一道確認 🧪
💠 單一網站:
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin update \
"all-in-one-wp-migration" \
--dry-run
預期輸出範例:
Plugin all-in-one-wp-migration 7.109 will be updated to 7.110.
分支判斷邏輯:看到目標版本 👉 可以正式更新;顯示沒有可用更新 👉 回頭確認目前版本與更新來源,不要因為「No updates」就直接認定網站安全 🔮
❇️ 獨立多站:每站各自 Dry-run
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
owner="$(stat -c '%U' -- "$config")"
if ! sudo -u "$owner" -- wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
version="$(sudo -u "$owner" -- wp --path="$path" \
plugin get "all-in-one-wp-migration" \
--field=version 2>/dev/null || true)"
if [ -n "$version" ]; then
printf '\n===== %s | current=%s =====\n' "$path" "$version"
sudo -u "$owner" -- wp --path="$path" plugin update \
"all-in-one-wp-migration" \
--dry-run
fi
done
分支判斷邏輯:先確認每站 Dry-run 結果都合理,再進正式更新;第一批 Production 建議仍採「一站完成 👉 驗證 👉 下一站」的節奏,不要一次對所有站下手 🚶
✴️ Multisite:Plugin 檔案只 Dry-run 一次
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin update \
"all-in-one-wp-migration" \
--dry-run
分支判斷邏輯:不要因為 Network 裡有 10 個 Site 就執行 10 次 Plugin Update Dry-run,Plugin 檔案屬於同一個 WordPress installation,一次結果即可代表整個 Network 🧩
⑤ 更新(確認備份與 Dry-run 都 OK 後才真正修改)
💠 單一網站:更新一站、驗證一站
WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" plugin update "all-in-one-wp-migration"
預期輸出範例:
Enabling Maintenance mode... Downloading update from https://downloads.wordpress.org/plugin/all-in-one-wp-migration.7.110.zip... Unpacking the update... Installing the latest version... Removing the old version of the plugin... Plugin updated successfully. Disabling Maintenance mode... Success: Updated 1 of 1 plugins.
分支判斷邏輯:更新成功 👉 立即進版本驗證;更新失敗 👉 不要反覆執行,先查看錯誤訊息、確認檔案權限與磁碟空間是否足夠 ⚠️
❇️ 獨立多站:一站一個完整閉環
WP_PATH="/var/www/site-a/public_html"
OWNER="$(stat -c '%U' -- "$WP_PATH/wp-config.php")"
sudo -u "$OWNER" -- wp --path="$WP_PATH" plugin update "all-in-one-wp-migration"
sudo -u "$OWNER" -- wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version
分支判斷邏輯:Site A 更新+驗證成功後,再把同樣流程套到 Site B;不要「全部更新完最後才檢查」,這樣一旦某站更新後出現相容性問題,很難快速定位是哪一站出的錯 🧵
🧑🔧 多站環境的實務原則:一次只處理一個網站,至少在第一批部署時如此。先完成一站的「備份 👉 更新 👉 驗證」,確認沒有 PHP Fatal Error、白畫面、登入問題或前台異常,再繼續下一站,這比一個巨大 Bash 迴圈從頭跑到尾更容易控制風險 🎛️
✴️ Multisite:Plugin 檔案只更新一次,再逐 Site 驗證
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin update "all-in-one-wp-migration"
wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version
更新後再逐一檢查 Network 中的 Site 是否能正常載入外掛:
wp --path="$WP_PATH" site list --field=url |
while IFS= read -r url; do
printf '\n===== %s =====\n' "$url"
wp --path="$WP_PATH" --url="$url" plugin get "all-in-one-wp-migration" \
--fields=name,status,version
done
分支判斷邏輯:Plugin 檔案版本只需要更新一次;Site 層則逐一確認是否正常載入與啟用,任一 Site 出現錯誤 👉 暫停批次流程,先處理該 Site,不要繼續擴大變更範圍 🛑
⑥ 驗證(更新成功 ≠ 工作完成)
正式更新後至少完成以下四層驗證,三種環境的差異主要在「一次執行」與「逐站執行」,驗證項目本身相同。
① 版本驗證
wp --path="$WP_PATH" plugin get "all-in-one-wp-migration" \
--fields=name,status,version,update,update_version
② Plugin Checksum 驗證
wp --path="$WP_PATH" plugin verify-checksums "all-in-one-wp-migration"
③ WordPress Core Checksum(若曾疑似入侵,建議一併執行)
wp --path="$WP_PATH" core verify-checksums
④ 前台/後台功能與最後一輪 Log/mu-plugins 複查
- ☑️ 前台首頁可以正常開啟
- ☑️ 後台可以正常登入
- ☑️ 文章/頁面正常顯示
- ☑️ PHP error log 沒有突然出現大量 Fatal Error
- ☑️ Cache/CDN 沒有持續回傳舊內容
- ☑️ 如果業務依賴這款外掛,應在 Staging 或維護窗口進行一次正常的備份/匯出測試
LOG_FILE="/var/log/nginx/access.log"
WP_PATH="/var/www/example.com/public_html"
if [ -f "$LOG_FILE" ]; then
grep -E "wp-trackback\.php|ai1wm_import|/wp-json/wp/v2/comments" "$LOG_FILE" |
tail -n 50
fi
find "$WP_PATH/wp-content/mu-plugins" -type f -iname "*.php" -print
分支判斷邏輯:更新後仍持續出現可疑 REST/AJAX 請求,或 mu-plugins 出現新的可疑檔案 👉 不要把它當成「已修好所以沒事」,應回到第 4 節「A. 已中招」流程重新處理,而不是單純重新更新一次外掛就結案 🔁
🔥 一句話總結:真正安全的批次維運,是先 Inventory,再 Backup,再 Dry-run,確認無誤後才逐批 Update,最後 Verify。環境判斷錯誤比指令寫錯更致命,這套 判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證 的閉環,換成下一次別的外掛、別的 CVE,依然可以直接套用 🦸
🏆 第七章:事件綜合評估(10 分制,數字越高代表風險或應對品質越正向)
| 評估維度 | 分數 | 一句話評語 |
|---|---|---|
| 漏洞嚴重程度(越高代表風險越大) | 8.8 | 不用登入就能埋雷,且靠合法功能觸發,防禦難度不低 |
| 官方應變速度 | 8 | 通報後 6 天內完成修補並釋出安全版本,反應算快 |
| 一般站長自救可行性 | 9 | 點更新按鈕就能解決,操作門檻不高但急迫度極高 |
| DevOps 技術文件完整度 | 8 | 攻擊鏈路、CVSS 向量與伺服器層緩解設定齊全,方便直接落地部署 |
| 🏆 加權綜合建議 | 立即行動 | 最終建議:【今天內完成更新,不要拖到明天】 |
🔥 這起事件其實給了所有網站主一個提醒:漏洞不見得都躲在複雜的核心系統裡,有時候反而藏在一個「你以為早就用完、不會再動它」的老功能背後。搬家外掛用完就晾在一邊,本身並不是問題;真正的問題是,只要它還「啟用」著,攻擊者就有機可乘。與其等哪天臨時要用備份功能才發現不對勁,不如現在就花五分鐘,打開後台看一眼版本號碼 🔢



