🚨客人只是想換一張客製化圖片,你的網站卻整個被搬空?Advanced Product Fields Extended 漏洞全解析
🕳️ 客人只是想換一張客製化圖片,你的網站卻整個被搬空?Advanced Product Fields Extended 漏洞全解析(CVE-2026-81789/CVSS 8.6)
🔥 懶人包:
WooCommerce 熱門的商品客製化欄位外掛「Advanced Product Fields Extended for WooCommerce」被抓到一個不用登入就能觸發的路徑遍歷漏洞(CVE-2026-81789,CVSS 8.6 高風險)。攻擊者完全不需要帳號密碼,只要在檔案路徑參數裡偷偷塞進幾個「../」,伺服器就會乖乖照辦,把它指定的任何檔案刪掉——而駭客最愛鎖定的目標,正是整座網站的命脈:wp-config.php。目前這個漏洞還沒被美國 CISA 列進「已知遭利用清單」,但正因為門檻低到幾乎不需要任何技巧,非常適合被自動化掃描機器人大量掃射。如果你的商店有裝這款外掛,版本號落在 3.1.6(含)以下,真心建議今天就處理,別等到前台變成一片空白才發現出事 🧯
內容目錄
🧵 一張「換圖」小要求,怎麼會變成整間店被搬空?
先講個你我都熟悉的情境:開一間網路商店,難免會遇到客人想買刻字杯、客製化衣服、印有自己照片的抱枕。為了讓客人上傳自己的照片或選項,很多店家都會裝一款叫「Advanced Product Fields Extended for WooCommerce」的外掛,讓商品頁多出各種可以自由填寫、上傳檔案的欄位——這就像是在櫃檯放了一疊「客製化訂購單」,客人填好、附上圖檔,訂單就會被收進後台的收件匣裡等著處理 📮
為了保持收件匣整潔,系統會搭配一台自動碎紙機:客人如果反悔換了張圖、或是重新上傳檔案,舊圖就會被自動送進碎紙機銷毀。問題出在這台碎紙機的控制面板完全沒上鎖——任何路人,不用出示會員證、不用登入任何帳號,只要在紙條上寫「請沿著『../../』這條路走出收件室,穿過幾條走廊,把店長辦公室保險箱裡那份『營業登記證』拿出來碎掉」,碎紙機二話不說就照做。而那份「營業登記證」,對應到 WordPress 網站身上,就是決定整座網站怎麼連上資料庫、怎麼運作的核心設定檔 wp-config.php 📜
這種讓程式誤判「往上跳出安全資料夾」的手法,在資安圈有個正式名字:路徑遍歷(Path Traversal),對應的通用弱點編號是 CWE-22。說穿了問題很簡單:外掛在處理「刪除舊上傳檔案」這個動作時,直接把客人前端傳來的檔名或路徑參數,原封不動接去拼成實際的檔案路徑,再呼叫底層的刪除函式,完全沒有先檢查這串路徑是不是真的還乖乖待在它該待的暫存資料夾裡 🕳️
🧊 冷知識:「回到上一層」的符號,居然比 WordPress 老快 60 歲
用「..」代表「跳回上一層目錄」的寫法,最早出現在 1960 年代 MIT 打造的早期分時作業系統裡,後來被 Unix、MS-DOS 一路沿用到今天的 Linux 世界。這個原本是為了讓工程師少打幾個字的貼心設計,卻在網頁應用程式時代變成資安界的老頑疾——只要開發者忘記檢查使用者傳進來的路徑字串有沒有夾帶這兩個小圓點,程式就有可能被「哄騙」著往外跳,摸到原本根本不該碰到的系統核心檔案 🕰️
⏱️ 從被發現到公開,中間到底發生了什麼事
把時間軸攤開來看,這次通報與修補的節奏其實走得不算慢:
| 日期 | 事件節點 | 詳細內容 |
|---|---|---|
| 2026-03-27 | 官方悄悄補上安全強化 | 原廠 StudioWombat 釋出 3.1.7 版本,更新日誌僅低調記載「加固檔案上傳與刪除流程之安全性」,當時並未點名任何 CVE 編號 |
| 2026-04-19 | 資安研究員正式通報 | 研究員 João Pedro S Alcântara(暱稱 Kinorth)透過 Patchstack 弱點回報管道,正式提交這個路徑遍歷漏洞的技術細節 |
| 2026-08-27 | CVE 編號正式保留 | Patchstack 向 CVE 體系申請並保留編號 CVE-2026-81789 |
| 2026-09-09~10 | 正式對外公開 | Patchstack 與 NVD 同步公開完整漏洞資訊,CVSS 3.1 評為 8.6 高風險 |
有個地方蠻有意思的:官方修補版本(3.1.7)的釋出時間,其實比研究員正式通報的時間還早了將近一個月 🟡 合理推論:比較可能的情況是,原廠當時是在做例行安全強化時「順手」補上了這個洞,並不知道這正好對應到一個尚未公開的高風險漏洞;後來研究員各自獨立找到同一個問題並送出報告,兩邊只是剛好撞在了同一次修補上。這種「原廠先修好、漏洞才後補公開」的時序在資安圈其實不算罕見,讀者不用過度解讀成官方隱瞞了什麼 🍍
✅ 已確認事實:截至目前,這個漏洞尚未被收錄進美國 CISA 的「已知遭利用漏洞(KEV)」清單,威脅情資平台估算的 EPSS(近期遭利用機率)分數也偏低,代表目前還沒有觀測到大規模、武器化的自動攻擊浪潮。🟡 合理推論:但因為這個漏洞完全不需要登入、也不需要誘騙任何人點擊,觸發門檻低到近乎零,非常適合被掃描機器人批次掃射,安全圈普遍認為這種「低門檻、自動化」的漏洞遲早會被收進常態掃描腳本裡,不能因為現在還沒爆發就掉以輕心 🚨
😨 講白了,這串攻擊到底能對我的商店幹嘛?
跟大部分「要先登入才危險」的漏洞不太一樣,這次完全不吃「帳號密碼」這一套。我們照事實的確定程度,一層一層拆給你看:
- 🏮 ✅ 已確認事實:不用登入,就能命令伺服器刪檔案。攻擊者不需要你網站任何一個帳號,只要能連上外掛暴露出來的處理端點,夾帶包含「跳出目錄」符號的路徑參數送出請求,伺服器上任何網頁伺服器行程有權限刪除的檔案,都可能被一併清空。CVSS 向量裡的 A:H(可用性衝擊:高)講的正是這件事。
- 🏮 🟡 合理推論:九成攻擊會直接瞄準 wp-config.php。這個檔案一消失,網站立刻失去跟資料庫連線的依據,前台會瞬間跳出「建立資料庫連線時發生錯誤」,整間店直接打烊,客人連結帳頁都進不去。
- 🏮 🔴 假設情境:檔案一消失,WordPress 反而主動幫你「開門」。WordPress 的核心邏輯其實很單純:只要根目錄裡找不到 wp-config.php,程式就會認定「這是一座剛蓋好、還沒裝潢的新房子」,主動秀出公開的安裝導引畫面。如果攻擊者在檔案被刪除後幾秒內搶先連上這個安裝畫面,填入自己準備好的外部資料庫位址與管理員密碼,等於直接把整座商店的鑰匙換成自己的,還能進一步接觸到過去累積的顧客訂單與個資。
💡 碎碎念:CVSS 8.6 分數看起來很嚇人,但仔細拆開向量會發現它「不偷資料、也不改資料」,機密性(C)與完整性(I)欄位都是 N,真正被打到滿分的只有可用性(A:H)。翻成白話就是:駭客不會偷看你的訂單內容,但可以讓你的商店整個「找不到」。對電商來說,這種痛其實一點都不比資料外洩輕——貨還在倉庫、訂單資料還躺在資料庫裡,但客人打開網站卻以為你已經倒店關門了 🥟
🎭 先別急著慌,來看看你屬於哪一種情境
如果你已經開始緊張,先深呼吸——不同身分該做的事其實差很多,來對照看看你最接近哪一種 🧋
🙋 我只是負責上架商品、顧客服的小編
如果你平常的工作是寫商品描述、回覆客服訊息,完全不碰程式碼跟主機後台,那你要做的事其實很單純:確認版本、按下更新、必要時先停用外掛。不用理解後面技術深潛篇的細節,直接跳到下面「五分鐘無痛自救指南」照著做即可,更新這個動作本身完全免費,唯一的成本是你的十分鐘 💯
👨💻 我自己架站,手上也管著好幾個客戶的 WooCommerce
如果你同時顧著好幾間店,光靠滑鼠一個一個點後台太沒效率。用 WP-CLI 批次盤點所有站台的外掛版本會實際很多,後面的技術深潛篇會直接給你可以貼上就用的排查指令,建議拉到那一段 👇
🏢 我的網站有在收單、有客戶付款資訊
如果你的商店牽涉到會員個資或線上金流,這件事就不只是「更新一個外掛」這麼簡單——服務中斷幾個小時,對電商來說直接等於實質營收損失,而不只是流量掉了。除了更新之外,建議同步啟動內部的應變流程,把稽核日誌留存下來,之後不管是要跟保險公司對帳、還是跟主機商核對,都用得上 🩺
🩵 荷包試算:停機一小時,比升級外掛貴得多
升級這款外掛本身不需要額外付費(如果你手上的是官方訂閱制的授權版本,License Key 通常已經涵蓋更新)。真正花錢的地方是「拖著不處理」的代價:中小型電商網站若因此停擺半天到一天,光是流失的訂單與客訴處理成本,往往就遠遠超過任何一款外掛的年費。這筆帳怎麼算,都是現在花十分鐘更新比較划算 💸
🚨 這個情境最容易被忽略:那個「當初辦活動裝完,就再也沒點開過」的欄位外掛
一間台灣的手工皮件小店,兩年前辦父親節活動時,為了讓客人能選皮帶顏色、刻上姓名縮寫,裝了這款客製化欄位外掛。活動結束後商品頁不再需要客製功能,店主也沒特別注意過外掛版本,覺得「反正它只是安安靜靜掛在那邊,應該不會怎樣」。問題是只要外掛還處於「啟用」狀態,就算完全沒人在用它的欄位功能,攻擊者依然能透過它暴露出來的處理端點觸發刪除動作——地雷不會因為你忘記它而消失,只會安靜地等在那裡 🆘
🚥 畫重點:「很久沒用」不等於「沒有風險」,只要外掛還在啟用清單裡,攻擊者就有機可乘——這也是這類漏洞最容易被忽略的地方 🙅♂️
🛟 五分鐘無痛自救指南(電腦小白也能跟著做)
不管你屬於上面哪一種情境,這一段建議每個人都先跑一遍——不用懂程式,跟著畫面點就好 🤏
- ㊀ 先去哪裡確認版本?
登入你的 WordPress 後台,點左側選單的「外掛(Plugins)」,再點「已安裝的外掛(Installed Plugins)」。在整串清單裡找到名字叫 Advanced Product Fields 或帶有 Extended 字樣的那一項,它旁邊或下方會標示目前的版本號碼 🔢 - ㊁ 看到版本號之後,怎麼判斷自己安不安全?
如果版本號是 3.1.6 或更舊(例如 3.1.5、3.1.0 等),代表你目前正暴露在風險裡;如果已經顯示 3.1.7 或更新,可以先鬆一口氣。判斷標準就是這一個數字 ㊙️ - ㊂ 需不需要馬上更新?
需要,而且優先程度排在今天所有代辦事項的第一位。直接在同一個畫面點「立即更新(Update Now)」按鈕,等它跑完,重新整理頁面確認版本號已經變成 3.1.7 以上即可 💪 - ㊃ 按了更新按鈕卻沒反應,或是根本不敢按怎麼辦?
如果你擔心更新會讓商品頁的自訂欄位跑版、或是這款外掛屬於需要授權金鑰的商用版本、按了半天沒動靜,不需要自己硬著頭皮排除,直接把這篇報告轉傳給你的網站維護工程師、外包商,或是主機代管服務商,請他們優先處理這項更新,同時順便檢查有沒有已經被入侵的跡象。找人幫忙是很正常的事,不用不好意思開口 🙇 - ㊄ 如果因為相容性問題暫時真的沒辦法更新,該怎麼辦?
既然這個漏洞一定要透過外掛暴露出來的處理端點才會被觸發,只要你把外掛直接點「停用(Deactivate)」,攻擊者能利用的路徑就完全沒有機會被觸發。等未來確認相容性沒問題,再評估是否更新後重新啟用 🤔 - ㊅ 如果你在後台完全找不到更新按鈕,或不熟悉這整套流程呢?
請立刻聯絡你的網站建置商、維護團隊或代管主機客服。跟他們溝通時,直接提供「CVE-2026-81789」這個編號,專業技術人員就能立刻理解問題的嚴重性並協助處置。在沒有完整備份的情況下,請不要自己貿然進入主機底層刪除或移動任何檔案❗
🫂 新手求助:完全看不懂「後台」「路徑」這些詞怎麼辦?
完全沒關係,這篇報告本來就不是要你變成資安專家。如果你連「已安裝的外掛」這個選單都找不到,最快的方式就是把整篇報告網址直接轉傳給你的主機商客服或網站外包商,跟他們說「我這個外掛有嚴重漏洞,麻煩幫我更新並檢查」,這件事本來就不該是每個網站主自己一個人扛 🤗
🤿 拆開來看:這串攻擊的程式碼底層到底哪裡壞掉了
接下來這段是給想知道「為什麼會這樣」的人看的——如果你只想知道怎麼補洞,上面那段其實已經夠用了。但如果你也好奇這種等級的漏洞底層長什麼樣子,我們繼續往下拆 ⚔️
| 評估指標 | 具體資訊 |
|---|---|
| 外掛名稱 | Advanced Product Fields Extended for WooCommerce |
| 開發廠商 | StudioWombat |
| CVE 編號 | CVE-2026-81789 |
| CVSS 評分 | 8.6(High) |
| CVSS 向量 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H |
| 受影響版本 | <= 3.1.6 |
| 安全版本 | >= 3.1.7(2026-03-27 釋出) |
| 漏洞類型 | 路徑遍歷(Path Traversal)/CWE-22,導致未授權任意檔案刪除 |
🧑🔧 DevOps 小知識:CVSS 向量裡的 S:C 比 PR:N 更值得留意
多數人看到 PR:N(不需權限)就知道要緊張,但這條向量裡的 S:C(Scope Changed,範圍變更)同樣關鍵:它代表這個漏洞造成的影響,不會被侷限在外掛自己的資料範圍內,而是能跳出外掛、波及整個 WordPress 核心運作(例如刪掉 wp-config.php 這種核心層級檔案)。防禦時不能只看「要不要登入」,範圍是否會外溢,往往才是決定實際傷害有多大的關鍵欄位 🦣
問題出在外掛處理「移除已上傳客製化檔案」這個功能時,對前端傳來的檔案路徑參數缺乏足夠的驗證。正常、安全的寫法,應該要先用 basename() 把任何目錄跳脫符號通通去掉,再用 realpath() 換算出這個檔案實際指向的絕對路徑,最後確認這條路徑的開頭仍然落在允許存取的暫存資料夾(例如 wp-content/uploads/wapf/)內,才准許執行刪除。但根據目前公開的技術摘要研判,<= 3.1.6 版本並沒有完整做到這一層檢查,導致當攻擊者在檔案路徑參數裡夾帶「../../」這類目錄回溯序列時,底層負責刪除檔案的函式會直接「照單全收」,一路跳出暫存目錄,摸到伺服器上任何具備寫入權限的檔案並將其抹除 🔍
🧊 冷知識:WordPress 的 Nonce 其實不是密碼學意義上的「一次性」
在密碼學裡,Nonce 指的是「只使用一次的隨機數」。但 WordPress 內建的 Nonce 機制實際上是個帶有 12 到 24 小時效期的雜湊權杖,針對同一位使用者、同一個動作,在效期內產生的值其實是固定不變的。當網站開啟整頁快取時,這些 Nonce 常常因為被一起快取住而失效,導致正常顧客反而上傳不了檔案。這種「為了解決快取相容性問題,乾脆放寬身分驗證」的權衡,正是許多外掛未授權漏洞背後最常見的根本溫床 📔
🎯 從「刪一個檔案」到「奪走整座商店」,中間只隔三步
- 🏮 第一步:定位目標。攻擊者不需要事先知道任何帳號密碼,直接向外掛暴露在外的檔案處理端點發送請求,夾帶包含目錄回溯序列的路徑參數,鎖定的目標通常就是網站根目錄下的 wp-config.php。
- 🏮 第二步:伺服器照單全收。由於路徑未經 basename()/realpath() 這類白名單檢核,伺服器端的直譯器直接把這條「合法看起來、實際上不合法」的路徑交給底層刪除函式執行,關鍵檔案就此消失。
- 🏮 第三步:搶在你之前完成「安裝」。wp-config.php 一旦不存在,WordPress 會誤判成這是一座全新、尚未初始化的網站,主動展示公開的安裝導引畫面。若攻擊者以自動化腳本第一時間搶先完成安裝流程,等於直接把管理權限交到了對方手上。
🧑🔧 DevOps 小知識:為什麼光是「檔案不見了」也能等於「被接管」?
多數人對「刪除檔案」的直覺印象只是造成服務中斷(DoS),但在 WordPress 的世界裡,wp-config.php 消失卻是眾所皆知的「奪權捷徑」。WordPress 核心邏輯非常精簡:只要根目錄找不到這支設定檔,程式就會判定「本站尚未初始化」,進而主動開放公開的安裝介面讓任何連上網站的人完成設定。這也是為什麼防禦這類漏洞的優先順序,永遠要把「保護 wp-config.php」擺在第一位,而不只是單純把它想成一支普通的設定檔 🔑
🕵️♀️ 資安排查 Checklist:DevOps 該怎麼確認自己有沒有已經中招
🤖 重要提醒:以下指令與排查流程由 Gemini 產生、Claude 複查
雖然已盡力對照常見實務經驗與 WP-CLI 官方文件,但仍可能因伺服器環境、權限設定或外掛版本差異而產生疏漏。正式環境(尤其是正在營業中的電商站)的高風險操作——包含更新、停用、刪除檔案、變更權限或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter 🧙
如果你是自己架 VPS、同時管理多間店的 WooCommerce,這一節最重要的不是「背一條神奇迴圈指令」,而是先搞清楚自己到底是哪一種 WordPress 架構。這次的漏洞一旦被觸發,關鍵檔案可能已經消失,也可能只是被掃描但尚未真正得手,因此這一節會依序走過環境判斷 👉 盤點(版本比對)→ Log 日誌排查 👉 後門檢查點 👉 加固檢查五個關卡,每一關都拆成「決策原理 👉 具體指令 👉 預期輸出範例 👉 分支判斷邏輯」四個段落,並且分別涵蓋單一網站、獨立多站、Multisite 三種環境 📜
🔥 一句話先記住:「版本已升級到 3.1.7」代表你已經跨過這道漏洞的修補門檻,但不等於「網站歷史上從來沒有被摸過」。如果舊版本曾經對外公開超過一段時間,版本修補與入侵排查應該視為兩件不同的工作,缺一不可 🧯
🧑🔧 DevOps小常識:先判斷地圖,再決定走哪條路
「一台伺服器上有 10 間 WooCommerce 商店」跟「一個 WordPress Multisite 裡面有 10 個 Site」看起來很像,實際上卻是兩種完全不同的架構。前者應該逐一找到各網站的 wp-config.php;後者才應該使用 wp core is-installed –network 判斷。WP-CLI 官方文件對 Multisite 的判定方式跟一般獨立站是分開處理的,不能混用 🫠
🧭 ㊀ 環境判斷:你到底是在管一間店,還是一整座商場?
決策原理:環境判斷錯誤會直接導致後續盤點、Log 排查、備份、更新全部跑偏——單一站、獨立多站、Multisite 三者的 WP-CLI 參數與資料範圍完全不同,必須先用官方指令確認,而不是憑印象猜 🤔
| 環境 | 典型結構 | 正確思路 | 常用 WP-CLI 定位方式 |
|---|---|---|---|
| 💠 單一網站 | 一個 WordPress 安裝 | 直接在該網站目錄操作 | –path=”$WP_PATH” |
| ❇️ 獨立多站 | 一台 VPS 上有多個完全獨立的 WooCommerce 商店 | 每個 wp-config.php 都代表一個獨立安裝 | –path=”$path” |
| ✴️ Multisite | 一個 WordPress Network,底下有多個 Site | 外掛檔案屬於整個 Network 共用,不是每個 Site 一份 | –network |
💠 單一網站:先確認 WP-CLI 真的站在 WordPress 裡
WP_PATH="/var/www/example.com/public_html"
if [ -f "${WP_PATH}/wp-config.php" ] && wp core is-installed --path="${WP_PATH}"; then
echo "[+] OK:偵測到正常運作的 WordPress 安裝。"
else
echo "[-] ERROR:路徑異常,或 wp-config.php 可能已經不見了(本身就是重要警訊)。"
fi
預期輸出範例:
[+] OK:偵測到正常運作的 WordPress 安裝。
分支判斷邏輯:
- ⭕ 顯示 OK 👉 可以進入盤點階段
- 🟠 顯示 ERROR,且 wp-config.php 確實不存在 👉 這代表你可能已經中招,直接跳到後面「A. 已中招/疑似中招」段落處理,不要繼續往下跑盤點指令(因為沒有 wp-config.php,WP-CLI 根本無法正常運作)
- 🟡 路徑含空格或特殊字元 👉 保留 “${WP_PATH}” 的雙引號,不要自行刪除,避免指令從中被切斷
❇️ 獨立多站:安全地逐站搜尋,不用不安全的迴圈寫法
決策原理:同一主機上有多間獨立商店,不代表是 Multisite。這時應該從各站的 wp-config.php 建立盤點清單,並取得檔案實際擁有者,避免用單一 Linux 帳號跑遍所有網站造成權限錯亂。這裡刻意採用 find -print0 搭配 while read -r -d ” 的寫法,而不是常見但危險的 for dir in $(find …)——後者一旦路徑裡出現空格或特殊字元,迴圈就會被錯誤地切成好幾段,對錯的目錄下重手 🔪
SEARCH_ROOT="/var/www"
find "${SEARCH_ROOT}" -maxdepth 4 -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 core is-installed --path="${path}" 2>/dev/null; then
printf 'WordPress: %s (owner: %s)\n' "${path}" "${owner}"
else
printf '[!] 無法正常載入:%s(owner: %s,wp-config.php 可能已消失或損毀)\n' "${path}" "${owner}"
fi
done
預期輸出範例:
WordPress: /var/www/shop-alpha/public_html (owner: shop-alpha) WordPress: /var/www/shop-beta/public_html (owner: shop-beta) [!] 無法正常載入:/var/www/shop-gamma/public_html(owner: shop-gamma,wp-config.php 可能已消失或損毀)
分支判斷邏輯:
- ⭕ 每個路徑都能通過 👉 建立獨立多站盤點清單,進入下一步
- 🟠 出現「無法正常載入」👉 這一站優先標記為高風險,直接進入「A. 已中招/疑似中招」流程排查,不要繼續套用一般盤點指令
- 🟡 同一個網站被找到多個 wp-config.php 👉 先人工確認哪個才是正式站,避免誤更新到備份或 Staging 副本
✴️ Multisite:確認網路多站架構
WP_PATH="/var/www/network/public_html"
if wp core is-installed --network --path="${WP_PATH}" 2>/dev/null; then
echo "[!] 判定架構為:✴️ Multisite(WordPress 網路多站架構)"
wp site list --fields=blog_id,url --format=table --path="${WP_PATH}"
else
echo "[!] 非 Multisite 架構,或 wp-config.php 已異常,請改用單一網站/獨立多站流程檢查"
fi
分支判斷邏輯:能列出多個 Site 👉 進入 Multisite 流程;如果不確定,可另外用 grep -E “MULTISITE” “${WP_PATH}/wp-config.php” 交叉確認常數是否存在,雙重驗證比較保險 🧭
🔎 ㊁ 盤點(版本比對):先回答「哪些站裝了這款外掛、目前到底是哪一版?」
決策原理:不要把版本號寫死在腳本裡,也不要依賴容易誤判的畫面文字比對,改用 wp plugin get –field=update 直接取得 available/none 這種明確的狀態值,判斷邏輯才不會因為畫面格式改版而失準 📊
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
SLUG="advanced-product-fields-for-woocommerce-extended"
CURRENT_VERSION="$(wp plugin get "${SLUG}" --field=version --path="${WP_PATH}" 2>/dev/null || true)"
UPDATE_STATUS="$(wp plugin get "${SLUG}" --field=update --path="${WP_PATH}" 2>/dev/null || true)"
if [ -z "${CURRENT_VERSION}" ]; then
echo "[-] 未安裝此外掛,本站不受影響。"
elif [ "${UPDATE_STATUS}" = "available" ]; then
echo "[!] 目前版本 ${CURRENT_VERSION},有可用更新,判定為受影響版本,需進入備份與更新流程。"
else
echo "[+] 目前版本 ${CURRENT_VERSION},狀態:${UPDATE_STATUS},暫無可用更新。"
fi
預期輸出範例:
[!] 目前版本 3.1.5,有可用更新,判定為受影響版本,需進入備份與更新流程。
分支判斷邏輯:
- 🔴 3.1.6 或更舊,且狀態為 available 👉 視為受影響,進入第 4 節備份/應急/修補流程
- 🟢 3.1.7 或更新 👉 此漏洞的版本條件已解除,但若網站曾經使用過舊版,仍應繼續往下做 Log 與後門排查
- 🟠 這款外掛屬於需要授權金鑰的商用版本,若 update 欄位顯示不出正確狀態(License 未綁定),WP-CLI 可能無法從原廠拉到更新資訊,這時務必改成登入後台手動確認或直接從官網下載最新安裝包覆蓋
❇️ 獨立多站
SEARCH_ROOT="/var/www"
SLUG="advanced-product-fields-for-woocommerce-extended"
find "${SEARCH_ROOT}" -maxdepth 4 -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "${config}")"
owner="$(stat -c '%U' -- "${config}")"
version="$(sudo -u "${owner}" -- wp plugin get "${SLUG}" --field=version --path="${path}" 2>/dev/null || true)"
if [ -n "${version}" ]; then
status="$(sudo -u "${owner}" -- wp plugin get "${SLUG}" --field=update --path="${path}" 2>/dev/null || true)"
printf 'Site: %s | Owner: %s | Version: %s | Update: %s\n' "${path}" "${owner}" "${version}" "${status}"
fi
done
預期輸出範例:
Site: /var/www/shop-alpha/public_html | Owner: shop-alpha | Version: 3.1.7 | Update: none Site: /var/www/shop-beta/public_html | Owner: shop-beta | Version: 3.1.5 | Update: available
分支判斷邏輯:這份結果就是你的第一張「風險地圖」——shop-beta 應優先列入修補名單;shop-alpha 可以繼續進行歷史排查,確認舊版暴露期間有沒有留下痕跡 🗺️
🚨 注意:上面的 owner 是從檔案實際擁有者取得,而不是硬編碼固定帳號。這對多租戶 VPS 特別重要,因為不同網站可能由不同 Linux 使用者管理;所有路徑變數與檔名一律使用雙引號包覆,避免空格或特殊字元造成指令中斷 ⛓️💥
✴️ Multisite
WP_PATH="/var/www/network/public_html"
SLUG="advanced-product-fields-for-woocommerce-extended"
wp plugin get "${SLUG}" --fields=name,status,version,update,update_version --path="${WP_PATH}"
wp plugin is-active "${SLUG}" --network --path="${WP_PATH}"
echo "Network activated exit code: $?"
分支判斷邏輯:Exit code 0 代表 Network Activated,處理範圍要以整個 Network 為單位;非 0 則代表只在部分 Site 各自啟用,這時可以再用 –url=”$url” 逐一確認各站啟用狀態 🧭
📊 ㊂ Log 日誌排查特徵:找「路徑+時間+結果」的組合
決策原理:路徑遍歷攻擊在存取日誌裡通常帶有明顯特徵(如 ../、%2e%2e),而檔案真的被刪除成功時,錯誤日誌會立刻爆出「找不到檔案」之類的致命錯誤,兩邊要一起比對,才能判斷是「單純被掃描」還是「已經被得手」🎯
💠 單一網站
ACCESS_LOG="/var/log/nginx/access.log"
ERROR_LOG="/var/log/nginx/error.log"
if [ -f "${ACCESS_LOG}" ]; then
grep -Ei "(advanced-product-fields|wapf).*(%2e%2e|\.\./)" "${ACCESS_LOG}" | tail -n 100
else
echo "LOG NOT FOUND: ${ACCESS_LOG},請先確認實際的 Web Server 日誌路徑"
fi
if [ -f "${ERROR_LOG}" ]; then
grep -Ei "(wp-config\.php.*No such file|Failed opening required.*wp-config\.php)" "${ERROR_LOG}" | tail -n 100
fi
預期輸出範例:
198.51.100.24 - - [11/Sep/2026:16:40:12 +0800] "POST /wp-admin/admin-ajax.php?action=wapf_remove_file&file=../../wp-config.php HTTP/1.1" 200 45 2026/09/11 16:40:15 [error] PHP Fatal error: Failed opening required '/var/www/example.com/public_html/wp-config.php'
分支判斷邏輯:
- 🟢 只有零星請求、沒有對應的錯誤日誌 👉 目前沒有直接證據顯示已被引爆,但仍建議繼續往下做後門檢查
- 🔴 存取日誌出現含 ../ 與 wp-config.php 的請求,且回應碼是 200,同時錯誤日誌也出現檔案遺失的致命錯誤 👉 高度懷疑已經中招,直接進入「A. 已中招/疑似中招」流程
❇️ 獨立多站
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 -Ei "(advanced-product-fields|wapf).*(%2e%2e|\.\./)" "${log}" 2>/dev/null || true)"
if [ -n "${matches}" ]; then
printf '\n===== %s =====\n' "${log}"
printf '%s\n' "${matches}" | tail -n 50
fi
done
分支判斷邏輯:某個 Log 出現命中 👉 依檔名或 VirtualHost 設定回推是哪一站,把該站標記為需要進一步鑑識;全部沒有命中 👉 風險降低,但不能證明「絕對沒被摸過」,因為 Log 可能已輪替或遺失 ♻️
✴️ Multisite
LOG_FILE="/var/log/nginx/access.log"
if [ -f "${LOG_FILE}" ]; then
grep -Ei "(advanced-product-fields|wapf).*(%2e%2e|\.\./)" "${LOG_FILE}" | tail -n 100
fi
分支判斷邏輯:找到可疑時間點後,再使用 wp site list –field=url 取得 Network Site 清單,依 Log 中的 Host/URL 判斷哪個 Site 受到影響,不要只看「外掛是否啟用」就下結論 🔗
🕳️ ㊃ 後門檢查點:檔案不見了,不代表事情只有這樣
決策原理:如果系統曾遭觸發安裝導引畫面,標準後門植入點通常包含 wp-content/mu-plugins(具備最高自動執行優先權的隱形外掛目錄)、近期遭到異動的 PHP 檔案,以及資料庫裡多出的未授權管理員帳號。這裡的 SQL 查詢刻意用單引號 heredoc 寫成獨立暫存檔,避免跟 Shell 本身的變數展開規則互相打架 📜
💠 單一網站
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
echo "[*] 檢查最近 72 小時內被異動過的 PHP 檔案..."
find "${WP_PATH}/wp-content" -maxdepth 5 -type f -name "*.php" -mtime -3 -not -path "*/cache/*" -ls
資料庫異常帳號與留言排查(用單引號 heredoc 把 SQL 寫成獨立暫存檔,避免多層跳脫符號互相干擾):
# 使用 heredoc 確保 SQL 不受引號干擾,維持邏輯閉環
WP_PATH="/var/www/example.com/public_html"
QUERY_FILE="$(mktemp)"
cat > "${QUERY_FILE}" <<'EOF'
SELECT option_name, LEFT(option_value, 80) AS preview
FROM wp_options
WHERE autoload = 'yes'
AND (option_value LIKE '%eval(%' OR option_value LIKE '%base64_decode%')
LIMIT 20;
EOF
wp --path="${WP_PATH}" db query < "${QUERY_FILE}"
rm -f -- "${QUERY_FILE}"
wp user list \
--role=administrator \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=table \
--path="${WP_PATH}"
預期輸出範例:
[1] mu-plugins:/var/www/example.com/public_html/wp-content/mu-plugins/sys_cache_bypass.php +------+----------------+---------------------+-------------------------+ | ID | user_login | user_email | user_registered | +------+----------------+---------------------+-------------------------+ | 1 | shop_admin | official@example.com | 2024-02-10 09:00:00 | | 7 | backup_sec | ghost@temp-mail.com | 2026-09-11 16:42:00 | +------+----------------+---------------------+-------------------------+
分支判斷邏輯:
- 🔴 mu-plugins 出現非部署流程建立的 PHP 檔案,或多出一個你不認得的管理員帳號(如上例的 backup_sec)👉 系統已被攻破,單純升級外掛無法復原,直接進入「A. 已中招/疑似中招」流程
- 🟢 檢查結果乾淨無異 👉 系統核心完整,繼續執行加固檢查
❇️ 獨立多站
SEARCH_ROOT="/var/www"
find "${SEARCH_ROOT}" -maxdepth 4 -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 站乾淨就把其他站一起視為乾淨;找到命中的站優先進入事件調查,其餘站繼續走一般盤點流程 🧭
✴️ Multisite
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 user list \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=table \
--path="${WP_PATH}"
分支判斷邏輯:發現可疑 mu-plugins 檔案 👉 把整個 Network 都納入事件調查範圍,因為 mu-plugins 是 Network 共用資源;發現未知帳號後,再進一步確認它屬於哪個 Network/Site 層級的權限,不要看到帳號存在就直接刪除 🧾
🔧 ㊄ 加固檢查:就算修好了,也把第二道門鎖起來
決策原理:這起漏洞的根本教訓,是攻擊鏈完全瞄準了「wp-config.php 不見了就能重新安裝」這個 WordPress 的原生行為,因此加固不能只靠「更新外掛」單一動作。這裡有個容易被忽略的細節:Linux 系統要刪除一個檔案,實際檢查的是該檔案所在的『上一層目錄』有沒有寫入權限,而不是檔案本身的權限——所以把 wp-config.php 的權限調到 440 只是第一步,網站根目錄本身是否還對 Web 伺服器帳號開放寫入,同樣要一併檢查,否則保護可能只是看得到、摸不到 🔒
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
CONFIG_FILE="${WP_PATH}/wp-config.php"
echo "[*] 開始執行目錄與檔案基礎權限收斂..."
find "${WP_PATH}" -type d -exec chmod 755 "{}" \;
find "${WP_PATH}" -type f -exec chmod 644 "{}" \;
if [ -f "${CONFIG_FILE}" ]; then
# 擁有者移交給 root,群組保留給 Web 伺服器帳號,權限限縮為 440(唯讀)
chown root:www-data "${CONFIG_FILE}"
chmod 440 "${CONFIG_FILE}"
echo "[+] wp-config.php 已設定為 root:www-data 440 唯讀保護。"
fi
echo "[*] 檢查網站根目錄擁有者與權限(避免 Web 伺服器帳號仍能寫入這一層目錄)..."
stat -c '擁有者:%U:%G 權限:%a' -- "${WP_PATH}"
echo "[*] 檢查當前 PHP 環境 disable_functions 狀態..."
php -r 'echo "當前 disable_functions: " . (ini_get("disable_functions") ?: "無設定") . "\n";'
預期輸出範例:
[+] wp-config.php 已設定為 root:www-data 440 唯讀保護。 擁有者:deploy:www-data 權限:755 當前 disable_functions: exec,passthru,shell_exec,system,proc_open,popen
分支判斷邏輯:
- ✅ wp-config.php 擁有者為 root、權限為 440,且根目錄擁有者不是 Web 伺服器帳號本身 👉 兩層防護都到位,即使外掛日後再爆出類似漏洞,PHP 行程也沒有系統層級權限可以刪除這支檔案
- 🟡 wp-config.php 已改成 440,但網站根目錄擁有者仍是 www-data、且權限含寫入位元 👉 保護只完成一半,請調整根目錄擁有者或改用更嚴格的權限設定,否則刪除動作理論上依然可能成立
伺服器層防護建議(Nginx 範例,路徑與規則請依實際站點調整,套用前先在 Staging 驗證):
# 攔截查詢字串中夾帶目錄回溯特徵的請求
if ($query_string ~* "(%2e%2e|\.\./)") {
return 403;
}
# 嚴格封鎖直接存取 wp-config.php
location ~* /wp-config\.php {
deny all;
return 404;
}
❇️ 獨立多站
SEARCH_ROOT="/var/www"
find "${SEARCH_ROOT}" -maxdepth 4 -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "${config}")"
if [ -f "${config}" ]; then
chown root:www-data "${config}"
chmod 440 "${config}"
printf '[+] 已加固:%s\n' "${path}"
fi
done
分支判斷邏輯:每個網站獨立判斷,不要拿 A 站的結果代替 B、C 站;任何一站出現例外(例如 Web 伺服器帳號與檔案擁有者相同、無法切割權限),就先暫停該站的自動化排程,避免可疑檔案被排程覆寫掉 ⏸️
✴️ Multisite
WP_PATH="/var/www/network/public_html"
chown root:www-data "${WP_PATH}/wp-config.php"
chmod 440 "${WP_PATH}/wp-config.php"
wp core verify-checksums --path="${WP_PATH}"
wp plugin verify-checksums --all --path="${WP_PATH}" || echo "[-] 部分商用付費外掛沒有公開的雜湊資料庫可供比對"
🚨 Checksum 驗證不是完整的入侵鑑識。它只能證明「WordPress.org 有公開雜湊值的檔案」未被竄改,這款外掛屬於商用付費版本,很可能沒有公開雜湊資料庫可供比對。即使 Checksum 全數通過,也不能保證整站沒有後門——mu-plugins、uploads、資料庫 option 都必須配合前面的排查步驟一起檢查 🔐
📋 第五節速查表:三種架構的排查地圖
| 階段 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| ① 環境判斷 | wp core is-installed | find -print0 逐站 wp-config.php | wp core is-installed –network |
| ② 盤點(版本) | wp plugin get –field=update | 逐站 –path 動態 owner | Network 外掛+–network |
| ③ Log 排查 | 單一 Access Log | 逐 VirtualHost Log | 共用 Log+URL 比對 |
| ④ 後門檢查 | mu-plugins+wp_options+帳號 | 逐站 mu-plugins | Network mu-plugins+Site 帳號 |
| ⑤ 加固檢查 | 440 唯讀+根目錄權限+WAF | 逐站加固 | Network 加固一次 |
🔥 核心原則:「批次」不代表「把所有東西塞進一條 Bash 迴圈」。真正安全的批次排查,是先判斷環境,再逐層縮小範圍,且務必用 find -print0 搭配 while read -r -d ”,而不是危險的 for x in $(find …),否則路徑一旦出現空格,指令很容易對錯的目錄下手 🎯
🤔 讀到快喘不過氣?常見焦慮 QA 大解密
看到這邊,如果是行銷小編或老闆,可能已經覺得頭昏眼花了。別緊張,整理了幾個大家最常問的焦慮點,幫你壓壓驚:
- 🏮 Q:按了更新,商品頁的客製化欄位設定會不會全部跑掉?
A:這款外掛主要負責前台的欄位邏輯,不涉及核心版面渲染,單純更新讓版面跑掉的機率不高。但保險起見,動手前先備份資料庫絕對是不變的真理喔 💾 - 🏮 Q:外包廠商說「這款外掛我們沒特別在用,不用管它」,我該聽他的嗎?
A:千萬別信這句話!漏洞最狡猾的地方在於「只要外掛還啟用著」,就算你沒在用它的客製化欄位,駭客一樣能把地雷塞進去。請堅定地要求廠商協助升級 💣
🛠️ 修不了就先擋:短期應急與正式修補三部曲
前一節解決的是「怎麼判斷」,這一節則進入真正的維運 Runbook。無論你屬於哪一種情境,強制流程都是同一套:盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證,每個階段都必須完成,才能進入下一階段 🧹
| 階段 | 目的 | 核心動作 |
|---|---|---|
| ① 判斷環境 | 避免操作錯網站 | 單站/獨立多站/Multisite |
| ② 盤點 | 知道誰受影響 | 版本、外掛狀態、Site Inventory |
| ③ 備份 | 保留回復點 | DB+wp-content+wp-config.php 本身 |
| ④ Dry-run | 先看更新會做什麼 | –dry-run |
| ⑤ 更新 | 關閉漏洞 | 升級至 3.1.7 或更新版本 |
| ⑥ 驗證 | 確認修補真的完成 | 版本+Checksum+功能+Log |
🚨 畫重點!千萬別踩雷:這次的備份階段有一個非常容易被忽略的地方——大部分人習慣只打包 wp-content 資料夾,但 wp-config.php 根本不在 wp-content 裡面!如果只備份 wp-content,等到真的發生「A. 已中招」情境、wp-config.php 被攻擊者刪掉時,你手上根本沒有任何一份可以直接拿來復原的設定檔備份。下面的備份指令會特別把這支檔案獨立備份出來,請不要略過這一段 🛟
🚨 ㊀ A. 已中招/疑似中招:先止血再修補
決策原理:一旦第五節排查出現明確跡象(wp-config.php 消失、mu-plugins 可疑檔案、帳號異常),優先目標是「切斷攻擊鏈+保留證據+恢復核心檔案」,而不是急著更新外掛——更新只是後續步驟。這裡有個關鍵順序:wp-config.php 若已消失,所有 WP-CLI 指令都無法運作,因此必須先從備份把這支檔案復原回來,才有辦法繼續執行後續的帳號清理與 Salt 刷新動作 🧯
⚠️ 安全優先提醒:即將執行具破壞性或不可逆之系統變更操作(刪除使用者、覆寫設定檔),執行前請務必再三核對目標路徑!
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
BACKUP_DIR="$HOME/wp-security-backup"
EVIDENCE_DIR="$HOME/wp-forensics-$(date +%Y%m%d_%H%M%S)"
## ❶ 立即隔離:阻斷外部流量進入網站與安裝介面
mkdir -p "${EVIDENCE_DIR}"
cat > "${WP_PATH}/.htaccess" <<'EOF'
Order Deny,Allow
Deny from all
Allow from 127.0.0.1
EOF
## ❷ 緊急數位取證:打包日誌與 mu-plugins(wp-config.php 已消失時,資料庫仍可透過主機商後台或既有連線資訊直接匯出)
cp -r /var/log/nginx "${EVIDENCE_DIR}/nginx_logs" 2>/dev/null || \
cp -r /var/log/apache2 "${EVIDENCE_DIR}/apache_logs" 2>/dev/null || true
cp -r "${WP_PATH}/wp-content/mu-plugins" "${EVIDENCE_DIR}/mu_plugins_dump" 2>/dev/null || true
## ❸ 檢查核心設定檔是否還在,不在就從備份復原(這一步是後續所有 WP-CLI 指令能否運作的關鍵)
if [ ! -f "${WP_PATH}/wp-config.php" ]; then
echo "[!] wp-config.php 已消失,正在尋找最近一份備份進行復原..."
LATEST_CONFIG_BACKUP="$(find "${BACKUP_DIR}" -maxdepth 1 -type f -name "wp-config_*.php" -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -n 1 | cut -d' ' -f2-)"
if [ -n "${LATEST_CONFIG_BACKUP}" ]; then
cp -- "${LATEST_CONFIG_BACKUP}" "${WP_PATH}/wp-config.php"
echo "[+] 已從 ${LATEST_CONFIG_BACKUP} 復原 wp-config.php。"
else
echo "[-] 找不到任何獨立備份的 wp-config.php,請改用主機商後台的檔案救援功能,或聯絡代管團隊協助重建(不建議直接執行 wp core config 重新產生,可能導致資料庫連線資訊錯誤)。"
fi
fi
## ❹ wp-config.php 確認存在後,強制刷新所有安全金鑰鹽值,讓現存的登入 Session 與 Cookie 全數失效
if [ -f "${WP_PATH}/wp-config.php" ]; then
wp config shuffle-salts --path="${WP_PATH}"
fi
## ❺ 檢查管理員清單,清理不明帳號(範例以 ID: 7 為可疑帳號,將文章歸屬轉移給 ID: 1)
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered --path="${WP_PATH}"
wp user delete 7 --reassign=1 --yes --path="${WP_PATH}" || true
## ❻ 強制停用漏洞外掛
wp plugin deactivate advanced-product-fields-for-woocommerce-extended --path="${WP_PATH}"
預期輸出範例:
[!] wp-config.php 已消失,正在尋找最近一份備份進行復原... [+] 已從 /home/admin/wp-security-backup/wp-config_pre_update_20260911-093000.php 復原 wp-config.php。 Success: Shuffled the salt keys. Success: Removed user 7. Plugin 'advanced-product-fields-for-woocommerce-extended' deactivated.
分支判斷邏輯:
- ✅ wp-config.php 復原成功且 Salts 刷新完成 👉 繼續往下清理帳號、進入正式修補流程
- 🔴 找不到任何 wp-config.php 備份 👉 這時千萬不要自己隨意用 wp core config 重建,因為資料庫帳密、表前綴等資訊若填錯,可能造成二次損壞,務必先聯絡熟悉該站架構的工程師或主機商協助救援
- 🟡 資料庫確認遭惡意竄改 👉 以最接近事發時間點之前的一份資料庫備份進行 Rollback,並同步變更資料庫連線密碼
❇️ 獨立多站
INCIDENT_PATH="/var/www/shop-beta/public_html"
BACKUP_DIR="$HOME/wp-security-backup"
if [ -f "${INCIDENT_PATH}/wp-config.php" ]; then
INCIDENT_OWNER="$(stat -c '%U' -- "${INCIDENT_PATH}/wp-config.php")"
else
INCIDENT_OWNER="$(stat -c '%U' -- "${INCIDENT_PATH}")"
echo "[!] wp-config.php 缺失,正在嘗試從備份復原..."
LATEST_CONFIG_BACKUP="$(find "${BACKUP_DIR}" -maxdepth 1 -type f -name "shop-beta_wp-config_*.php" -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -n 1 | cut -d' ' -f2-)"
if [ -n "${LATEST_CONFIG_BACKUP}" ]; then
cp -- "${LATEST_CONFIG_BACKUP}" "${INCIDENT_PATH}/wp-config.php"
chown "${INCIDENT_OWNER}:${INCIDENT_OWNER}" "${INCIDENT_PATH}/wp-config.php"
fi
fi
if [ -f "${INCIDENT_PATH}/wp-config.php" ]; then
sudo -u "${INCIDENT_OWNER}" -- wp config shuffle-salts --path="${INCIDENT_PATH}"
sudo -u "${INCIDENT_OWNER}" -- wp plugin deactivate advanced-product-fields-for-woocommerce-extended --path="${INCIDENT_PATH}"
else
echo "[-] 復原失敗,請立即聯絡專業團隊協助救援,不要在這種狀態下繼續執行其他 WP-CLI 指令。"
fi
分支判斷邏輯:確認路徑與擁有者正確 👉 對這一站套用單一網站流程中的證據保存與復原指令;不要把整個 /var/www 當成單一網站處理,也不要因為同一台 VPS 其他站看起來正常,就跳過對它們的排查 🧭
✴️ Multisite
WP_PATH="/var/www/network/public_html"
if [ -f "${WP_PATH}/wp-config.php" ]; then
wp config shuffle-salts --path="${WP_PATH}"
wp plugin deactivate advanced-product-fields-for-woocommerce-extended --network --path="${WP_PATH}"
wp user list --fields=ID,user_login,user_email,user_registered --path="${WP_PATH}"
else
echo "[!] Multisite 共用的 wp-config.php 已消失,影響範圍是整個 Network,請優先從最近備份復原後再繼續。"
fi
分支判斷邏輯:由於 Multisite 共用同一支 wp-config.php,一旦消失代表整個 Network 全數受影響;復原後務必逐一檢查各 Site 的管理員帳號是否有異常,不要遺漏權限層級較低但仍具備發文能力的帳號 🔍
🧯 ㊁ B. 短期應急:尚未確認中招,但現在還不能立刻更新
🚨 短期應急 ≠ 正式修補。停用外掛、伺服器層防護規則都屬於緩衝措施,目標是縮短攻擊面暴露時間,真正的修補仍然是安裝 3.1.7 或更新版本 🧱
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
wp plugin deactivate advanced-product-fields-for-woocommerce-extended --path="${WP_PATH}"
分支判斷邏輯:
- 🟢 客製化欄位功能暫時可以停用 👉 保留停用狀態,盡快排入正式修補窗口
- 🟠 商店正在檔期活動中,客製化欄位是核心賣點不能停 👉 改採伺服器層防護規則(見前一節加固檢查的 Nginx/Apache 範例),並儘速安排升級
❇️ 獨立多站
INCIDENT_PATH="/var/www/shop-beta/public_html"
INCIDENT_OWNER="$(stat -c '%U' -- "${INCIDENT_PATH}/wp-config.php")"
sudo -u "${INCIDENT_OWNER}" -- wp plugin deactivate advanced-product-fields-for-woocommerce-extended --path="${INCIDENT_PATH}"
分支判斷邏輯:這個指令只影響單一站點,不應連其他站一起停掉;如果要批次處理多個高風險站,建議先跑前面盤點迴圈篩出清單,再針對清單逐一停用 📋
✴️ Multisite
WP_PATH="/var/www/network/public_html"
if wp plugin is-active advanced-product-fields-for-woocommerce-extended --network --path="${WP_PATH}"; then
echo "外掛為 Network Activated,停用時需視為整個 Network 影響範圍。"
wp plugin deactivate advanced-product-fields-for-woocommerce-extended --network --path="${WP_PATH}"
else
echo "外掛僅在部分 Site 個別啟用,可改用 --url 逐站停用。"
fi
✅ 為什麼停用就能擋?整條攻擊鏈完全依賴外掛暴露出來的處理端點才能觸發,停用外掛能百分之百截斷觸發路徑,是目前最乾淨、風險最低的短期緩解手段,但不能取代正式升級 🙅
㊂ C. 正式修補:強制流程「判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證」
🩵 荷包試算:這幾道防線要花多少錢?
伺服器層設定與停用外掛本質上都是免費操作,只要有 SSH 權限就能自己動手。真正會花到錢的地方,通常是「已經中招之後」——請資安顧問做鑑識、清除後門的工時,或是商店因故障而流失的訂單金額,這筆帳,怎麼算都是現在就花時間走完這套 Runbook 比較划算 💸
① 判斷環境(再次確認)
回到前面「㊀ 環境判斷」重新執行判斷,確認自己屬於哪一種架構後再繼續,不要在還沒確認架構前就執行下面任何批次指令。
② 盤點(Inventory)
沿用前面「㊁ 盤點」的三段指令,先確認哪些站真正安裝了受影響版本,把結果列成清單,不要在第一個迴圈裡直接更新。
③ 備份(Backup,含獨立備份 wp-config.php)
# 💠 單一網站
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-CLI 匯出資料庫完整備份
wp --path="${WP_PATH}" db export "${BACKUP_DIR}/site_pre_update_${STAMP}.sql"
## ❷ 透過 tar 封裝 wp-content 資料夾
tar -czf "${BACKUP_DIR}/site_wp-content_pre_update_${STAMP}.tar.gz" -C "${WP_PATH}" "wp-content"
## ❸ ⚠️ 額外獨立備份 wp-config.php 與 .htaccess ── 這兩支檔案不在 wp-content 內,
## 上面的壓縮檔不會包含它們,但正是這次漏洞最容易被鎖定的目標,
## 沒有這一步,「A. 已中招」流程就沒有真正能復原的依據
cp -- "${WP_PATH}/wp-config.php" "${BACKUP_DIR}/wp-config_pre_update_${STAMP}.php"
if [ -f "${WP_PATH}/.htaccess" ]; then
cp -- "${WP_PATH}/.htaccess" "${BACKUP_DIR}/htaccess_pre_update_${STAMP}.bak"
fi
echo "[+] 備份完成,涵蓋資料庫、wp-content 與 wp-config.php 三份獨立檔案。"
# ❇️ 獨立多站(逐站獨立備份,含各站 wp-config.php)
SEARCH_ROOT="/var/www"
BACKUP_DIR="$HOME/wp-security-backup"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "${BACKUP_DIR}"
find "${SEARCH_ROOT}" -maxdepth 4 -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}")"
echo "=== 備份站點:${path}(owner: ${owner})==="
sudo -u "${owner}" -- wp --path="${path}" db export "${BACKUP_DIR}/${site_name}_db_${STAMP}.sql"
tar -czf "${BACKUP_DIR}/${site_name}_wp-content_${STAMP}.tar.gz" -C "${path}" "wp-content"
cp -- "${config}" "${BACKUP_DIR}/${site_name}_wp-config_${STAMP}.php"
done
# ✴️ Multisite(Network 共用資料庫與設定檔,只需備份一次)
WP_PATH="/var/www/network/public_html"
BACKUP_DIR="$HOME/wp-network-backup"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "${BACKUP_DIR}"
wp --path="${WP_PATH}" db export "${BACKUP_DIR}/network_alldb_${STAMP}.sql"
tar -czf "${BACKUP_DIR}/network_wp-content_${STAMP}.tar.gz" -C "${WP_PATH}" "wp-content"
cp -- "${WP_PATH}/wp-config.php" "${BACKUP_DIR}/network_wp-config_${STAMP}.php"
echo "⚠️ Multisite 注意:上述 db export 是整個網路共用的總資料庫,包含所有子站資料表,非單一 Site。"
預期檔案結構範例:
/home/admin/wp-security-backup/ ├── site_pre_update_20260911-093000.sql ├── site_wp-content_pre_update_20260911-093000.tar.gz └── wp-config_pre_update_20260911-093000.php
分支判斷邏輯:三份檔案(DB、wp-content、wp-config.php)都成功產出 👉 進 Dry-run;任一備份失敗 👉 停止,不進入正式更新,先排除磁碟空間或權限問題 🛑
④ Dry-run(正式修改前先預覽)
# 💠 單一網站
WP_PATH="/var/www/example.com/public_html"
wp plugin update advanced-product-fields-for-woocommerce-extended --dry-run --path="${WP_PATH}"
# ❇️ 獨立多站
SEARCH_ROOT="/var/www"
find "${SEARCH_ROOT}" -maxdepth 4 -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "${config}")"
owner="$(stat -c '%U' -- "${config}")"
printf '\n===== %s =====\n' "${path}"
sudo -u "${owner}" -- wp plugin update advanced-product-fields-for-woocommerce-extended --dry-run --path="${path}"
done
# ✴️ Multisite(外掛檔案共用,只需 Dry-run 一次)
WP_PATH="/var/www/network/public_html"
wp plugin update advanced-product-fields-for-woocommerce-extended --dry-run --path="${WP_PATH}"
預期輸出範例:
Available update from version 3.1.5 to version 3.1.7
分支判斷邏輯:
- 若顯示 new_version 為 3.1.7(或更高)👉 更新來源無誤,進入正式更新
- 若顯示 No plugin updates available,但版本仍 <= 3.1.6 👉 這是商用外掛常見狀況,代表 License Key 可能未綁定,WP-CLI 無法直接從原廠拉取,必須手動登入 StudioWombat 官方會員中心下載最新安裝包覆蓋上傳
⑤ 更新(確認備份與 Dry-run 都 OK 後才真正修改)
# 💠 單一網站(嚴格遵循 deactivate 👉 update 👉 activate 閉環)
WP_PATH="/var/www/example.com/public_html"
wp plugin deactivate advanced-product-fields-for-woocommerce-extended --path="${WP_PATH}"
wp plugin update advanced-product-fields-for-woocommerce-extended --path="${WP_PATH}"
wp plugin activate advanced-product-fields-for-woocommerce-extended --path="${WP_PATH}"
wp plugin get advanced-product-fields-for-woocommerce-extended --fields=name,status,version --path="${WP_PATH}"
# ❇️ 獨立多站(穩健串行逐站更新,禁止一次性平行迴圈)
SEARCH_ROOT="/var/www"
find "${SEARCH_ROOT}" -maxdepth 4 -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "${config}")"
owner="$(stat -c '%U' -- "${config}")"
echo "=========================================================="
echo "[*] 開始執行站點更新程序:${path}(身分:${owner})"
sudo -u "${owner}" -- wp plugin deactivate advanced-product-fields-for-woocommerce-extended --path="${path}"
sudo -u "${owner}" -- wp plugin update advanced-product-fields-for-woocommerce-extended --path="${path}"
sudo -u "${owner}" -- wp plugin activate advanced-product-fields-for-woocommerce-extended --path="${path}"
echo "[*] 緩衝等待 3 秒,保護系統資源平衡..."
sleep 3
done
# ✴️ Multisite(外掛檔案只需更新一次,接著全網啟用)
WP_PATH="/var/www/network/public_html"
wp plugin update advanced-product-fields-for-woocommerce-extended --path="${WP_PATH}"
wp plugin activate advanced-product-fields-for-woocommerce-extended --network --path="${WP_PATH}"
🚨 危險做法對比:千萬不要用 for dir in /var/www/*; do wp plugin update … & done 這種無間隔並行寫法同時更新所有站點。一旦新版本出現任何語法衝突,所有營業中的商店會在同一秒集體癱瘓,瞬間暴增的資料庫連線也可能直接衝垮主機記憶體。正確做法永遠是逐站完成備份 👉 更新 👉 驗證後,再進行下一站 ⚠️
⑥ 驗證(更新成功 ≠ 工作完成)
正式更新後至少完成以下四層驗證:
WP_PATH="/var/www/example.com/public_html"
# ① 版本驗證
wp plugin get advanced-product-fields-for-woocommerce-extended --fields=name,status,version --path="${WP_PATH}"
# ② WordPress Core Checksum(若曾疑似入侵,建議一併執行)
wp core verify-checksums --path="${WP_PATH}"
# ③ 前台功能探針
curl -sI "https://example.com/shop/" | grep -E "HTTP/[123].*200"
# ④ 最後一輪 Log/mu-plugins 複查
grep -Ei "(advanced-product-fields|wapf).*(%2e%2e|\.\./)" "/var/log/nginx/access.log" | tail -n 50
find "${WP_PATH}/wp-content/mu-plugins" -maxdepth 2 -type f -iname "*.php" -print
分支判斷邏輯:更新後仍持續出現可疑請求,或 mu-plugins 出現新的可疑檔案 👉 不要把它當成「已修好所以沒事」,應回到前面「A. 已中招」流程重新處理,而不是單純重新更新一次外掛就結案 🔁
🔥 一句話總結:真正安全的批次維運,是先 Inventory,再 Backup(連 wp-config.php 都要獨立備份),再 Dry-run,確認無誤後才逐批 Update,最後 Verify。這套判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證的閉環,換成下一次別的外掛、別的 CVE,依然可以直接套用 🦸
🏆 事件綜合評估(10 分制,數字越高代表風險或應對品質越正向)
| 評估維度 | 分數 | 一句話評語 |
|---|---|---|
| 漏洞嚴重程度(越高代表風險越大) | 8.6 | 不用登入就能刪檔,還能連帶奪走整座商店的管理權限 |
| 官方應變速度 | 7 | 修補版本其實早於正式通報就已釋出,只是公告時未特別點名 |
| 一般站長自救可行性 | 9 | 點更新按鈕就能解決,操作門檻不高但急迫度極高 |
| DevOps 技術文件完整度 | 8 | 攻擊成因、排查腳本與三種架構應對方式齊全,方便直接落地部署 |
| 🏆 加權綜合建議 | 立即行動 | 最終建議:【今天內完成更新,別等到前台變空白才處理】 |
🔥 這起事件說到底其實提醒了所有經營網路商店的人一件事:漏洞不一定都躲在複雜的付款金流或會員系統裡,有時候反而藏在一個「客人拿來換張圖」的小功能背後。客製化欄位外掛裝完就晾在一邊,本身並不是問題;真正的問題是,只要它還「啟用」著,攻擊者就有機可乘。與其等哪天商店前台突然變成一片空白才發現不對勁,不如現在就花五分鐘,打開後台看一眼版本號碼 🔢









