- 逆向行駛 - https://520.be/ -

顧客結帳時偷偷把自己升級成「網站老闆」:WooCommerce 外掛高危漏洞全解析(CVE-2026-15369/CVSS 9.8)

🔥 懶人包:
WooCommerce 熱門加購外掛 Custom User Registration Fields for WooCommerce(開發商 Addify,WooCommerce Marketplace 認證外掛,官方標榜擁有超過 1.2 萬用戶)被爆出重大漏洞 CVE-2026-15369,CVSS 評分 9.8(近乎滿分)。只要外掛的「使用者角色選擇」功能是開著的,任何一個路人在結帳頁面填完假資料按下送出,就能把自己升級成網站最高權限的 管理員(Administrator)——全程不用登入、不用密碼、不用騙你點任何連結。目前官方已釋出 2.2.4 修補版本,強烈建議所有 WooCommerce 商店主立刻更新,若暫時無法更新,也有一個「一鍵關掉」的緊急自保設定 🛑

🌐 前往 WooCommerce Marketplace 確認外掛版本 [1]

[2]


🟦 🟦 🟦

內容目錄

🧐 這個漏洞到底在講什麼?先用一個生活化比喻搞懂

先別急著看落落長的技術名詞,我們把你的 WooCommerce 商店想像成一間防備森嚴的高級俱樂部。這款由 Addify 開發的外掛,就是俱樂部大門口那位負責收「入會申請表」的櫃台人員。它原本的任務很單純:讓 B2B(企業對企業)商店可以在客人結帳、順便完成註冊時,多問一句「你是一般散客、批發商,還是簽約企業客戶?」讓客人自己在下拉選單裡勾選 😌

問題出在,這位櫃台人員完全沒有核對申請表內容的能力。正常來說,客人只能在俱樂部事先印好的幾個選項裡打勾(一般會員、企業會員),但如果有個心懷不軌的客人,自己在申請表最底下手寫加了一格「俱樂部總經理」並且打勾交出去,這位訓練有嚴重缺陷的櫃台人員居然連問都不問,直接發給他一把能打開金庫、機房和所有辦公室的萬能鑰匙 🔑

換成技術語言:攻擊者在向網站送出結帳/註冊請求時,會在資料裡偷偷夾帶一個參數,告訴系統「我要當 administrator」。外掛完全沒有檢查這個值合不合法,就直接把它寫進 WordPress 的權限系統裡,一個原本應該只是路過買東西的訪客,下一秒就成了你網站的最高統治者 😱

💡 白話文教室:什麼是「Store API」?
現代 WooCommerce 商店為了讓結帳頁面跑得更快,會用一種叫「Store API」的機制,讓瀏覽器直接跟後端伺服器「悄悄話」交換結帳資料,不用整頁重新整理。這就像餐廳導入了「平板點餐」——服務生(傳統表單)被省略了,客人自己在平板上點餐直接送進廚房。方便是方便,但如果廚房(外掛)沒有設計好「這桌客人到底能不能點主廚特餐」的權限判斷,任何人都能在平板上打字「我要當老闆」,廚房居然還真的把主控權端出去給他 🍽️

🟢 🟢 🟢

📖 事情的來龍去脈:一場悄悄進行中的資安倒數計時

此漏洞由資安研究員 0xd4rk5id3 發現並通報,交由 Wordfence 資安團隊審核與協調揭露。

時間節點 具體進展與處置動作
2026-07-10 漏洞編號 CVE-2026-15369 正式向 CVE 官方機構提出保留(Reserved)申請
2026-08-29 Wordfence 與美國國家漏洞資料庫(NVD)正式對外公開漏洞細節,同日開發商 Addify 釋出 2.2.4 修補版本
2026-08-29 起 多家國際威脅情報中心(IONIX、Tenable)陸續發布嚴重警告,提醒攻擊門檻極低
2026-08-31(本文撰寫時) 官方通報尚未列出大規模在野攻擊災情,但情報機構評估自動化攻擊腳本已具備公開流傳的條件

🟡 合理推論:由於漏洞公開與修補版本同一天釋出,理論上留給駭客的「情報差」時間很短;但這也代表一旦有人把攻擊腳本寫成自動化工具,尚未更新的商店會在短時間內被地毯式掃描,這種「One-Day」等級的風險,往往就發生在店主覺得「應該還好吧、晚點再更新」的那幾天 ⏰

💡 碎碎念:這不是單一個案,是一整個「類型」的漏洞
就在同一週,另一款熱門會員外掛 Ultimate Member 也被爆出幾乎一模一樣的手法(CVE-2026-19423):讓未驗證的訪客在自己的註冊表單裡夾帶角色參數提權。這說明「讓使用者自選角色」這個設計思路,正是目前 WordPress 外掛生態中一個容易被反覆踩到的地雷類型,不是只有這一款外掛倒楣 🌠


🔶 🔶 🔶

⚠️ 對我的網站有什麼實質影響?

此漏洞被評為 🔴 嚴重(CVSS 9.8),之所以逼近滿分,是因為攻擊者不需要任何帳號、不需要任何前置條件、也不需要誘騙管理員做任何事,只要你的網站開著這個外掛且對外公開,遠端就能直接觸發。

  • ✅ 已確認事實:攻擊者只要送出一個特製結帳請求,就能建立一個持有 administrator 權限的全新帳號,取得跟店主本人完全對等的最高控制權——能新增刪除商品、竄改訂單狀態、更換收款帳號,甚至把你踢出自己的網站。
  • 🟡 合理推論:拿到最高權限後,駭客極可能匯出整個 WooCommerce 資料庫,包含所有顧客的姓名、電話、地址、消費紀錄;也可能在商品頁埋入通往博弈或色情網站的隱藏連結(SEO 毒化),讓你的網站被 Google 標記警告、流量瞬間歸零。
  • 🔴 假設情境:若伺服器本身權限沒有做好隔離(例如 PHP 執行身分過大、沒有設定 open_basedir 限制),駭客甚至可能透過佈景主題編輯器寫入後門,讓攻擊範圍從單一網站擴大到整台主機,殃及同伺服器上代管的其他無辜網站。

🟪 🟪 🟪

🎭 四種情境對應:你的網站到底該怎麼辦?

🙋 一般網站管理者(部落格主、小型 WooCommerce 商店)

你不需要看懂任何程式碼,只要做兩件事:登入後台確認外掛版本,以及確認自己有沒有開啟「使用者角色選擇」這個功能。這是決定你網站到底有沒有暴露在風險中的關鍵開關 🌠

適合行動:高優先。 立刻更新到 2.2.4 以上,這個動作完全免費、三分鐘內就能完成,是全文性價比最高的一步 👍

👨‍💻 獨立開發者/網站代管商(DevOps)

如果你手上管理多個客戶的 WooCommerce 站台,光靠人工逐一點擊後台效率太低。建議透過 WP-CLI 批次檢查所有站台的外掛版本,並在 WAF 層級預先部署阻擋規則,作為修補前的緩衝防線(詳見下方技術深潛篇)🔧

適合行動:中高優先。 建議直接看後段的 WAF 攔截規則與日誌排查特徵表格。

🏢 企業商業用途(B2B 商店、批發/簽約會員制網站)

這款外掛本身的主打賣點就是「讓一般散客與 B2B 批發商在同一個表單註冊」,因此越是需要角色分類的正規企業商店,越是這次漏洞的高風險族群。若你的網站有大量客戶個資或簽約單價資訊,這不只是資安問題,更直接牽涉個資法與 GDPR 合規責任 🚫

適合行動:極高優先,且需留存證據。 除了更新,建議同步啟動內部資安事件應變流程,保留稽核日誌以供後續調查與法遵佐證。

🚨 高風險場景:你以為跟自己無關,其實正好中招

🔴 假設情境:外貿批發商的隱形陷阱
一間台灣的跨境批發商,為了讓國外的零售商跟一般散客都能自己選擇「零售/批發」身分下單,特地開啟了「使用者角色選擇」這個貼心功能。網站主心想「我的防火牆有裝、外掛也不是三天兩頭沒更新,應該安全」,卻完全沒意識到,正是這個「開放角色下拉選單」的設定,恰好符合了此漏洞被觸發的必要條件——任何人結帳時都能把下拉選單裡看不到的隱藏值塞進去,變成管理員 🆘

⛔️ 這類「因為 B2B 業務需求而開啟角色選擇」的商店,反而是最容易誤判自己「應該沒事」的高風險族群,請務必優先確認這項設定是否開著 🙅‍♂️

[58]


🔴 🔴 🔴

🔍 技術深潛篇:CVE 底層原理拆解(給 DevOps 看)

項目 內容
外掛名稱 Custom User Registration Fields for WooCommerce(開發商:Addify)
CVE 編號 CVE-2026-15369
CVSS v3.1 評分 9.8(Critical)
CVSS 向量 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — 可遠端網路觸發、攻擊複雜度低、無需任何權限與使用者互動,機密性/完整性/可用性全面重創
CWE 分類 CWE-269(不當的權限管理,Improper Privilege Management)
受影響版本 <= 2.2.3
安全版本 >= 2.2.4
觸發前提 外掛的「使用者角色選擇(User Role Selection)」設定必須為開啟狀態

🧑‍🔧 DevOps 小知識:這不是傳統的 SQL Injection,是「兩個各自無害的功能疊在一起」出事
問題源自 af_reg_checkout_data_to_order_meta_data_block() 這個函數。當未驗證的訪客對 WooCommerce Store API 結帳端點 /wc/store/v1/checkout 發起請求時,這個函數會直接抓取請求中的 afreg_select_user_role 參數值,完全沒有做白名單驗證,就把它寫進訂單中介資料(Order Meta Data)持久化存進資料庫。接著在結帳完成、觸發 woocommerce_thankyou 這個核心動作鉤子時,掛在上面的 af_reg_custom_order_processing_function() 會把剛才被污染的角色值,原封不動地丟進 WordPress 核心 API WP_User::add_role()。由於這支 API 的設計前提是「呼叫它的上層業務邏輯早就做好把關」,它不會質疑「為什麼一個結帳流程要求我把 administrator 權限給一個新帳號」,於是就這樣照單全收,完全繞過外掛後台本來設定好的「允許角色清單」⛑️

🎯 攻擊鏈路:從觸發到接管只要一次結帳

階段 技術動作
❶ 掃描 攻擊者以自動化工具識別出目標站台安裝了 ≤ 2.2.3 版本的外掛,且「使用者角色選擇」功能開啟
❷ 構造請求 組出一個符合 Store API 規範的 JSON 結帳請求,內含建立新帳號所需的基本欄位,並額外夾帶 “afreg_select_user_role”: “administrator”
❸ 觸發 /wc/store/v1/checkout 送出請求,WordPress 依正常流程建立新帳戶,並在結帳完成階段被外掛強制附加 administrator 權限
❹ 接管 使用剛才自訂的帳密,透過標準 /wp-login.php 登入,直接取得網站最高控制權

🟡 合理推論(Probable Escalation):取得管理員權限後,攻擊者最可能的下一步是竄改 WooCommerce 的金流閘道設定(例如把 PayPal/Stripe 金鑰換成自己掌控的帳戶),讓後續所有顧客的付款悄悄轉走;也可能上傳偽裝外掛或佈景主題,寫入深埋的 PHP 後門,讓單純修補漏洞無法徹底根除威脅 ⚠️


🟫 🟫 🟫

✅ 資安排查與自我檢查 Checklist

🤖 重要提醒:以下指令與排查流程由 Gemini 產生、Claude 複查
雖然已盡力對照常見實務經驗與 WP-CLI 官方文件,但仍可能因伺服器環境、權限設定或外掛版本差異而產生疏漏。正式環境(尤其是生產站)的高風險操作——包含更新、停用、刪除檔案、變更權限或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter 🧙

如果你的商店曾經跑過 Custom User Registration Fields for WooCommerce(外掛目錄名稱 user-registration-plugin-for-woocommerce2.2.3 或更舊版本,而且「使用者角色選擇」這個開關曾經開著,那真正該問的問題已經不是「現在有沒有漏洞」,而是「在你把它關掉、把版本升上去之前,有沒有陌生人已經幫自己開了一把後門鑰匙?」這件事光靠肉眼看後台使用者清單是抓不出來的,因為一個寫得漂亮的假帳號,長得跟真的管理員一模一樣 📜

🕵️ 這台 WooCommerce 商店,到底有沒有被摸過?把現場一層一層挖出來

這一節依 環境判斷 → 盤點 → Log 排查 → 後門檢查 → 加固 的順序往下拆,同時涵蓋單一網站、獨立多站、Multisite 三種常見部署型態,每一步都會先講「為什麼要這樣做」,再給實際指令跟預期輸出,最後補上該怎麼往下分支判斷 🧭

所以這一節不採「跑一條掃描指令、看到綠燈就收工」的做法,而是照著 環境判斷 👉 盤點 👉 設定核對 👉 Log 排查 👉 後門帳號檢查 👉 檔案完整性驗證 👉 加固 的順序,一層一層把可疑範圍縮小。每一步都會先講「為什麼要這樣查」,再給指令、預期輸出跟分支判斷,並且同時涵蓋單一網站、獨立多站、Multisite 三種常見的維運型態,因為 DevOps 現場常常不是只有一個乾淨的 WordPress 資料夾在等你操作 🌠

🔥 一句話先記住:「外掛顯示 2.2.4 以上」代表你已經跨過 CVE-2026-15369 的版本修補門檻;它不等於「這台主機歷史上從來沒被建立過惡意管理員帳號」。如果舊版本+角色選擇開啟這個組合曾經公開暴露在網路上超過幾天,版本修補與入侵排查應該視為兩件不同的工作,缺一不可🚷


🧭 🧭 🧭

🧭 ㊀ 先判斷環境:你手上到底是一個 WordPress,還是一整座商店群?

這一步看起來很無聊,卻是後面所有批次指令能不能安全下手的地基。三種型態外觀常常很像,但錯誤的判斷會讓你把「獨立多站」誤當成「Multisite」去操作,結果一個指令炸掉不該動的網站🧯

環境 你看到的結構 正確思路 常用 WP-CLI 定位方式
💠 單一網站 一個獨立的 WordPress + WooCommerce 安裝 直接在該網站目錄下操作即可 –path=”$WP_PATH”
❇️ 獨立多站 一台 VPS/主機上有多個各自獨立的 WooCommerce 商店 每一份 wp-config.php 都代表一個完全獨立的安裝,資料庫、使用者、外掛狀態互不相通 –path=”$path”
✴️ Multisite 一個 WordPress Network,底下掛著多個 Site 外掛檔案屬於整個 Network 共用,不是每個 Site 各裝一份;但每個 Site 可以各自啟用/停用 –url=”$url”

💠 單一網站:先確認 WP-CLI 真的站在 WordPress 裡

決策原理:在做任何盤點之前,先確認目前路徑真的能載入 WordPress 核心,遠比直接對一個「可能根本不是網站根目錄」的地方下指令安全得多。這一步也順便確認 WP-CLI 版本能不能正常吃到 WooCommerce 相關的資料表結構。

※下方 /var/www 請替換為你網站的真實路徑,全程保留雙引號,避免路徑含空格或特殊字元時整條指令解析錯誤。

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” 的雙引號,不要為了「看起來乾淨」自行拿掉。

❇️ 獨立多站:先把每一個真正的 WordPress 找出來,建立一份清單

決策原理:用 HestiaCP、cPanel 或自行維運 VPS 時,「同一台主機上有很多 WooCommerce 商店」不等於 Multisite。這時應該從每一份 wp-config.php 出發,先建立完整 Inventory,才不會漏查任何一個客戶站。

SEARCH_ROOT="/var/www"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"

    if wp --path="$path" core is-installed 2>/dev/null; then
        printf 'WordPress: %s\n' "$path"
    fi
done

預期輸出:

WordPress: /var/www/shop-a/public_html
WordPress: /var/www/shop-b/public_html
WordPress: /var/www/shop-c/public_html

分支判斷:

  • ✔️ 每個路徑都能通過 👉 可以建立獨立多站的排查名單。
  • 🟠 找到 wp-config.php 但無法載入 WordPress 👉 先確認 PHP 版本、檔案權限或路徑是否正確,不要直接對它下更新指令。
  • 🟡 同一個網站被找到多份 wp-config.php(常見於 Staging/備份目錄)👉 先人工確認哪一份才是正式環境,避免誤查、誤改到備份或測試站。

✴️ Multisite:這一步才把 Network 底下的 Site 列出來

決策原理:wp site list 是 Multisite 專屬指令,WP-CLI 官方文件明確定義它是用來列出 Multisite installation 中的 Site,因此不能拿它去掃描一整台主機上的獨立多站。

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://shop.example.com/
2        https://tw.example.com/
3        https://b2b.example.com/

分支判斷:能列出多個 Site 👉 進入 Multisite 流程;只查到單一網站不代表一定是獨立多站,還要確認是否存在 Network 設定(例如 wp-config.php 內是否定義了 MULTISITE 常數)。

🧑‍🔧 DevOps 小知識:「獨立多站」像同一棟大樓裡有三家彼此無關的公司,各自有門鎖、帳本與倉庫;Multisite 則更像同一家公司底下的三個部門,共用同一套後勤系統。外觀看起來都是「三個網站」,但外掛檔案共用與否、資料庫結構完全不同,這也是為什麼漏洞排查跟後續更新的「作用範圍」會差很多🚫


🔍 🔍 🔍

🔎 ㊁ 盤點:先回答「哪些站裝了這款外掛、目前到底是哪一版?」

公開漏洞資料明確指出 2.2.3 以下版本受影響,2.2.4 為修補版本,這個邊界同時出現在 Wordfence Threat Intelligence 公告與 CVE.org、NVD 官方紀錄中。

💠 單一網站:讓 WP-CLI 回報版本,不要自己肉眼猜

決策原理:外掛版本盤點應該以 WordPress 自己認得的 Plugin metadata 為準,而不是自己打開 readme.txt 用眼睛找數字,人工比對很容易看錯或漏看。

WP_PATH="/var/www/example.com/public_html"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"

wp --path="$WP_PATH" plugin get "$PLUGIN_SLUG" \
    --fields=name,status,version,update,update_version

預期輸出:

name: user-registration-plugin-for-woocommerce
status: active
version: 2.2.3
update: available
update_version: 2.2.4

分支判斷:

  • 🔴 2.2.3 或更舊 👉 視為受影響,直接進入後續的設定核對與備份/修補流程。
  • 🟢 2.2.4 或更新 👉 此漏洞的版本條件已解除,但如果這台站曾經跑過舊版,仍建議往下做完整的入侵排查,不要跳過。
  • 🟠 找不到外掛 👉 確認是否已被移除、是否用了不同的商店安裝來源(例如客戶自行改名資料夾),並確認沒有查錯網站路徑。

❇️ 獨立多站:逐站查,結果千萬不要混成一份

決策原理:每個獨立商店的外掛安裝狀態彼此無關,因此必須逐一帶上 –path 執行,並且把「網站路徑」跟「版本號」一起記錄下來,才能做出可行動的風險地圖。

SEARCH_ROOT="/var/www"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"

    if ! wp --path="$path" core is-installed 2>/dev/null; then
        continue
    fi

    version="$(wp --path="$path" \
        plugin get "$PLUGIN_SLUG" \
        --field=version 2>/dev/null || true)"

    if [ -n "$version" ]; then
        printf 'Site: %s | Plugin: %s\n' "$path" "$version"
    fi
done

預期輸出:

Site: /var/www/shop-a/public_html | Plugin: 2.2.4
Site: /var/www/shop-b/public_html | Plugin: 2.2.3
Site: /var/www/shop-c/public_html | Plugin: 2.1.9

分支判斷:這份清單就是你的第一張「風險地圖」:B、C 應立刻列入緊急修補名單;A 可以繼續往下做歷史入侵排查,確認過去暴露期間有沒有留下痕跡。

✴️ Multisite:外掛檔案只盤點一次,再從 Site 層確認啟用狀態

決策原理:Multisite 共用同一套外掛檔案,因此不能把每個 Site 當成獨立外掛安裝來各自更新,但每個 Site 是否「啟用」這個外掛、以及是否開啟角色選擇功能,仍然可能不一樣。

WP_PATH="/var/www/network/public_html"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"

wp --path="$WP_PATH" plugin get "$PLUGIN_SLUG" \
    --fields=name,status,version,update,update_version

wp --path="$WP_PATH" site list \
    --field=url

預期輸出:

name: user-registration-plugin-for-woocommerce
status: active
version: 2.2.3
update: available
update_version: 2.2.4

https://shop.example.com/
https://tw.example.com/
https://b2b.example.com/

分支判斷:外掛版本以 Network 安裝的檔案為核心依據;各 Site 是否實際啟用該外掛,需搭配 –url=”$url” 逐站確認,因為 Multisite 底下的外掛是「Network Activate」或「單站各自啟用」,會直接影響每個 Site 是否暴露在風險中。


🎛️ 🎛️ 🎛️

⚙️ ㊂ 最關鍵的一格:這台站的「使用者角色選擇」開關,到底開著還是關著?

Wordfence 的漏洞說明白紙黑字寫著,這個漏洞的觸發前提是「User Role Selection」設定必須為開啟狀態。換句話說,就算版本沒更新,只要這個開關本來就沒開,攻擊者送再多惡意結帳請求,外掛也不會把角色值餵給 WP_User::add_role()。這一格判斷結果,直接決定你的商店現在是「高危」還是「暫時安全但仍需更新」🌠

🚨 畫重點:目前公開資料沒有列出這個開關在資料庫中對應的確切 Option Name 或 Meta Key,因此下面的資料庫查詢屬於輔助線索,唯一權威的確認方式仍是親自登入後台,進到該外掛的設定頁面用眼睛核對【❓待確認:官方未公開此設定對應的確切資料庫欄位名稱】🙅‍♂️

💠 單一網站:後台目視核對 + 資料庫輔助線索

決策原理:由於確切的 Option Name 未公開,最保險的做法是先用人工登入後台核對,再用一條「不預設欄位名稱」的模糊搜尋指令,把 wp_options 表裡跟這款外掛有關的設定列出來,方便你比對是否存在角色相關開關。

WP_PATH="/var/www/example.com/public_html"

wp --path="$WP_PATH" db query \
    "SELECT option_name, LEFT(option_value, 120) AS preview
     FROM wp_options
     WHERE option_name LIKE '%afreg%'
        OR option_name LIKE '%addify%'
     ORDER BY option_name;"

預期輸出(示意,欄位名稱請以你實際查到的結果為準):

+---------------------------+------------------------------+
| option_name               | preview                       |
+---------------------------+------------------------------+
| afreg_general_settings    | a:12:{s:...                  |
| afreg_registration_fields | a:5:{s:...                   |
+---------------------------+------------------------------+

分支判斷:

  • ✔️ 後台介面顯示「使用者角色選擇」為關閉 👉 此漏洞暫時無法被觸發,風險降低,但仍應儘速更新,因為設定隨時可能被誤觸打開。
  • 🟠 後台介面顯示為開啟 👉 直接視為高風險站台,優先進入下方 Log 排查與後門帳號檢查,並同步啟動「短期應急」關閉此設定。
  • 🟡 資料庫查到的序列化資料(a:12:{…})不易人工判讀 👉 不要自行猜測欄位代表的意思,以後台實際顯示的開關狀態為準,資料庫查詢只作為交叉比對線索。

❇️ 獨立多站:逐站登入核對,並保留查詢紀錄以便比對

決策原理:每個獨立商店的設定值互不相通,因此無法用一條批次指令「代替」人工核對,但可以先用批次查詢,把「有裝這個外掛、且資料庫存在相關設定」的站台整理出優先核對清單,減少人工逐一登入的時間成本。

SEARCH_ROOT="/var/www"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"

    if ! wp --path="$path" core is-installed 2>/dev/null; then
        continue
    fi

    count="$(wp --path="$path" db query \
        "SELECT COUNT(*) FROM wp_options WHERE option_name LIKE '%afreg%';" \
        --skip-column-names 2>/dev/null || echo 0)"

    if [ "$count" -gt 0 ]; then
        printf 'Needs manual UI check: %s (related option rows: %s)\n' "$path" "$count"
    fi
done

預期輸出:

Needs manual UI check: /var/www/shop-a/public_html (related option rows: 2)
Needs manual UI check: /var/www/shop-b/public_html (related option rows: 3)

分支判斷:列出的站台,請逐一登入後台肉眼核對開關狀態,並記錄在你的排查表格中;沒有被列出的站台,代表資料庫中找不到相關設定紀錄,風險相對較低,但仍建議一併確認外掛版本。

✴️ Multisite:先看 Network 是否統一啟用,再逐站核對

決策原理:Multisite 下,這款外掛通常是「Network Activate」統一啟用,因此優先確認是否所有 Site 都共用同一組設定,再決定要不要逐站個別核對。

WP_PATH="/var/www/network/public_html"

wp --path="$WP_PATH" plugin status \
    "user-registration-plugin-for-woocommerce"

預期輸出:

Plugin user-registration-plugin-for-woocommerce details:
    Name: Custom User Registration Fields for WooCommerce
    Status: Network Activated
    Version: 2.2.3

分支判斷:顯示 Network Activated 👉 設定通常共用一份,登入 Network 後台核對一次即可;顯示為各 Site 自行啟用 👉 必須逐一登入每個 Site 的後台核對,因為不同 B2B 客戶站可能各自打開了不同的角色設定。


🧾 🧾 🧾

📜 ㊃ Log 排查:不是看到 404 就緊張,而是找「漏洞路徑+參數+結果」三件事同時出現

Wordfence 公告與 CVE 描述都指出,問題出在未驗證的訪客對 WooCommerce Store API 結帳端點 /wc/store/v1/checkout 發送請求,並在 Request Body 中夾帶 afreg_select_user_role 參數。因此排查 Log 時,真正有價值的不是隨便找一個「陌生 IP」,而是確認是否曾出現「這個路徑+這個參數名稱+高權限角色字串+成功回應碼」這四件事同時發生的紀錄📔

🚨 再次強調:以下所有指令只用於「比對防禦特徵」,不會、也不建議提供任何可直接送出的攻擊 Payload。請勿將這些查詢指令套用在未經授權的網站上進行測試🙅‍♂️

💠 單一網站:先找到 Log,再做時間範圍與特徵搜尋

決策原理:不同主機環境的 Access Log 路徑差異很大(Apache/Nginx/LiteSpeed/代管面板各自不同),因此不要把某一個路徑當成所有伺服器的固定答案,先確認 Log 存在再往下查。

LOG_FILE="/var/log/apache2/access.log"

if [ -f "$LOG_FILE" ]; then
    grep -Ei \
        'POST /wc/store/v1/checkout.*afreg_select_user_role' \
        "$LOG_FILE" |
    tail -n 100
else
    echo "LOG NOT FOUND: $LOG_FILE"
fi

預期輸出:

LOG NOT FOUND: /var/log/apache2/access.log

這不是失敗,而是提醒你先去確認實際的 VirtualHost/Web Server Log 路徑,很多共用主機環境會把 Log 放在 ~/logs/ 或面板專屬目錄下。

若確實找到 Log,可能會看到類似(以下為排查特徵示意,非可執行攻擊指令):

... "POST /wc/store/v1/checkout HTTP/1.1" 403 ...
... "POST /wc/store/v1/checkout HTTP/1.1" 200 ...

分支判斷:

  • 🟢 只有 403/拒絕紀錄 👉 代表請求曾抵達但被擋下,不能單憑這一點判定入侵成功。
  • 🟡 出現 200/201/302 等成功回應碼,且時間點集中在 2026-08-29(漏洞公開日)之後 👉 提高事件優先級,接著比對同一時間附近的帳號建立紀錄。
  • 🔴 同一時間附近同時出現新建 administrator 帳號、或 wp-content/uploads 內出現可疑檔案 👉 直接進入下方「已中招/疑似中招」流程,不要再停留在排查階段。

❇️ 獨立多站:每個 VirtualHost 都要對應自己的 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 -Ei \
        'POST /wc/store/v1/checkout.*afreg_select_user_role' \
        "$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 可能已經輪替、被刪除,或攻擊者走的是其他未被記錄的服務層。

✴️ Multisite:同一份 Web Server Log,靠 URL 與時間再回推是哪個 Site

決策原理:Multisite 通常共用同一份或少數幾份 Web Server Log,需要額外用網域/URL 欄位反查是哪個 Site 被攻擊。

LOG_FILE="/var/log/apache2/access.log"

if [ -f "$LOG_FILE" ]; then
    grep -Ei \
        'POST /wc/store/v1/checkout.*afreg_select_user_role' \
        "$LOG_FILE" |
    awk '{print $1, $7}' |
    sort |
    uniq -c |
    sort -rn |
    head -n 20
else
    echo "LOG NOT FOUND: $LOG_FILE"
fi

預期輸出:依「來源 IP+請求路徑」聚合後的次數統計,方便你判斷是否有單一來源對某個特定網域大量嘗試。

分支判斷:若統計結果集中在特定網域對應的路徑 👉 針對該 Site 執行 –url=”$url” 的後續帳號與檔案排查;若分散在多個 Site 👉 應視為整個 Network 都需要排查,優先程度一致拉高。


🔍 🔍 🔍

🦹‍♂️ ㊄ 後門與帳號檢查:真正該抓的是「不該存在的最高權限帳號」

這個漏洞的攻擊終點就是「憑空生出一個 administrator」,所以帳號比對是整個排查流程中優先級最高的一步,比檔案掃描更直接👍

💠 單一網站:列出所有管理員帳號,比對建立時間與 Email 網域

決策原理:正常情況下,管理員帳號數量少、建立時間集中在網站上線初期或既有人員異動時。任何「時間點落在漏洞公開後、Email 網域看起來陌生」的 administrator,都應該視為高度可疑。

WP_PATH="/var/www/example.com/public_html"

wp --path="$WP_PATH" user list \
    --role=administrator \
    --fields=ID,user_login,user_email,user_registered \
    --format=table

預期輸出:

+----+------------+---------------------------+---------------------+
| ID | user_login | user_email                | user_registered     |
+----+------------+---------------------------+---------------------+
| 1  | shop_owner | owner@example.com         | 2023-04-11 09:12:03 |
| 58 | jt29xk     | jt29xk@disposable-mail.tw | 2026-08-30 03:14:52 |
+----+------------+---------------------------+---------------------+

分支判斷:

  • ✔️ 帳號清單裡每一個都認得、建立時間都合理 👉 這一格暫時過關,但仍建議往下做檔案完整性驗證。
  • 🔴 出現不認得的帳號,尤其建立時間落在 2026-08-29 之後、且是凌晨等異常時段 👉 立刻進入「已中招/疑似中招」流程,不要先刪除帳號,先保留證據(見下方第4節A小節)。
  • 🟡 wp user list 只看得到目前「還存在」的帳號 👉 若懷疑攻擊者事後自行刪除帳號滅證,需要額外查資料庫的異動紀錄或備份快照比對,單看目前清單並不保證完整。

除了帳號本身,權限欄位也可能被單獨竄改,因此建議額外檢查 wp_usermeta 中的 wp_capabilities 欄位:

WP_PATH="/var/www/example.com/public_html"

wp --path="$WP_PATH" db query \
    "SELECT u.ID, u.user_login, um.meta_value
     FROM wp_users u
     JOIN wp_usermeta um
       ON u.ID = um.user_id
     WHERE um.meta_key = 'wp_capabilities'
       AND um.meta_value LIKE '%administrator%'
     ORDER BY u.ID DESC;"

預期輸出:會列出目前所有具備 administrator 權限的帳號 ID 與序列化的權限欄位內容,可用來交叉比對上面 wp user list 的結果是否一致,若兩份結果對不上,代表權限欄位可能被直接改寫而非透過正常註冊流程新增。

❇️ 獨立多站:逐站列出管理員清單,並集中彙整成一份總表

決策原理:攻擊者可能只鎖定其中一兩站測試,也可能對整批站台掃描過一輪,逐站列出並彙整,才能看出是「單點事件」還是「批次攻擊」。

SEARCH_ROOT="/var/www"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"

    if ! wp --path="$path" core is-installed 2>/dev/null; then
        continue
    fi

    printf '\n===== %s =====\n' "$path"

    wp --path="$path" user list \
        --role=administrator \
        --fields=ID,user_login,user_email,user_registered \
        --format=table 2>/dev/null
done

預期輸出:依網站路徑分段列出各站的管理員清單,方便你逐一核對每一站的名單是否都能對得上「你認識的人」。

分支判斷:多站同時出現時間相近、Email 網域雷同的陌生帳號 👉 視為批次自動化攻擊事件,應同步啟動所有相關站台的應變流程,並保留這份彙整清單作為事件證據。

✴️ Multisite:留意「Super Admin」與「單站管理員」的差異

決策原理:Multisite 底下的權限分兩層:Network 層級的 Super Admin,以及各 Site 層級的 administrator。這個漏洞寫入的是單站層級的 administrator 角色,因此排查時要以 –url 逐站檢查,不能只看 Network 層。

WP_PATH="/var/www/network/public_html"

wp --path="$WP_PATH" site list --field=url |
while IFS= read -r url; do
    printf '\n===== SITE: %s =====\n' "$url"

    wp --path="$WP_PATH" \
        --url="$url" \
        user list \
        --role=administrator \
        --fields=ID,user_login,user_email,user_registered \
        --format=table 2>/dev/null
done

預期輸出:依每個 Site 的網址分段列出各自的管理員清單。

分支判斷:任何一個 Site 出現陌生的單站 administrator 👉 針對該 Site 啟動應變,不需要立刻假設整個 Network 都淪陷,但仍建議連帶檢查該 Site 使用的外掛設定與 Log。


🧮 🧮 🧮

🧮 ㊅ 檔案完整性驗證:Core 可以比對官方指紋,但這款外掛不行——這裡有一個很多人會踩的坑

WP-CLI 內建的 wp core verify-checksumswp plugin verify-checksums,運作原理都是向 WordPress.org 官方伺服器下載該版本的官方檔案指紋,再跟你本機的檔案逐一比對。這對 WordPress 核心跟大部分免費外掛很好用,但這裡要先戳破一個容易被忽略的誤解📜

🚨 畫重點!千萬別踩雷:Custom User Registration Fields for WooCommerce 是透過 WooCommerce Marketplace 與開發商 Addify 官網銷售的付費外掛,並未上架在免費的 wordpress.org 外掛目錄,也就沒有對應的公開 SVN 版本庫。這代表 wp plugin verify-checksums 對這款外掛會直接失敗或找不到官方指紋可供比對,而不是「跑出來全部正常」——如果你看到它默默跳過或報錯,請不要誤判成「已驗證沒問題」🚫

正因為沒有公開 SVN/GitHub 原始碼庫可以直接抓官方版本做逐行 diff,防禦端能做、也應該做的替代方案,是「用自己的乾淨副本當基準,做雜湊比對」,而不是想辦法去外部下載一份來源不明的「原始碼」 🫆

💠 單一網站:三層替代驗證法

決策原理:既然無法向官方要指紋,就退而求其次,用「更新前後的雜湊值比對」+「關鍵函數存在性檢查」+「Core 仍可正常做官方指紋比對」三層方式,拼湊出等同效果的完整性把關。

第一層:WordPress Core 仍可正常做官方指紋比對

WP_PATH="/var/www/example.com/public_html"

wp --path="$WP_PATH" core verify-checksums

預期輸出:

Success: WordPress installation verifies against checksums.

第二層:對外掛目錄自行建立雜湊清單,作為「往後比對」的基準線(更新到 2.2.4 後執行一次,留存下來)

WP_PATH="/var/www/example.com/public_html"
PLUGIN_DIR="$WP_PATH/wp-content/plugins/user-registration-plugin-for-woocommerce"
BASELINE_FILE="/root/checksum-baselines/urp-woo-2.2.4.sha256"

mkdir -p "$(dirname -- "$BASELINE_FILE")"

find "$PLUGIN_DIR" -type f -name "*.php" -print0 |
xargs -0 sha256sum > "$BASELINE_FILE"

echo "Baseline saved to: $BASELINE_FILE"

預期輸出:

Baseline saved to: /root/checksum-baselines/urp-woo-2.2.4.sha256

之後任何時候懷疑檔案被竄改,都可以用同一支腳本重新產生雜湊,再用 diff 跟基準線比對,找出被改動過的檔案:

PLUGIN_DIR="/var/www/example.com/public_html/wp-content/plugins/user-registration-plugin-for-woocommerce"
BASELINE_FILE="/root/checksum-baselines/urp-woo-2.2.4.sha256"
CURRENT_FILE="/tmp/urp-woo-current.sha256"

find "$PLUGIN_DIR" -type f -name "*.php" -print0 |
xargs -0 sha256sum > "$CURRENT_FILE"

diff "$BASELINE_FILE" "$CURRENT_FILE"

預期輸出:若無差異則不會印出任何內容;若有檔案被改動,會列出雜湊不同的那幾行,代表對應檔案內容已變化。

第三層:直接檢查關鍵函數是否已包含官方修補邏輯

Wordfence 的公開技術說明已明確點出漏洞位置在 af_reg_checkout_data_to_order_meta_data_block()af_reg_custom_order_processing_function() 這兩個函數,且後者掛在 woocommerce_thankyou 這個動作鉤子上,最終呼叫 WP_User::add_role()。由於沒有公開 SVN 可比對修補前後 diff,我們改用「檢查關鍵字是否存在」的方式,間接確認 2.2.4 版本是否真的加入了白名單驗證邏輯,這屬於檢查自己安裝檔案的行為,不涉及還原或重建攻擊手法

PLUGIN_DIR="/var/www/example.com/public_html/wp-content/plugins/user-registration-plugin-for-woocommerce"

grep -rn "af_reg_checkout_data_to_order_meta_data_block" "$PLUGIN_DIR" --include="*.php"
grep -rn "af_reg_custom_order_processing_function" "$PLUGIN_DIR" --include="*.php"

預期輸出:會列出這兩個函數定義所在的檔案與行號。

分支判斷:

  • ✔️ Core Checksum 通過、外掛雜湊比對無差異 👉 目前這份安裝檔案狀態正常。
  • 🟡 找不到上述任一函數名稱 👉 【❓待確認:開發商可能已在 2.2.4 版本中重新命名內部函數】,不代表一定異常,但建議另外用官方購買管道重新下載一份 2.2.4 安裝包,直接整包比對檔案結構是否一致,而不要自行猜測函數邏輯內容。
  • 🔴 雜湊比對出現不在你更新/變更紀錄範圍內的差異檔案 👉 視為潛在後門,優先進入下方第4節「已中招/疑似中招」流程,保留該檔案副本以供後續鑑識,不要直接覆蓋或刪除。

❇️ 獨立多站:逐站建立各自的基準線,避免站台間互相汙染比對結果

決策原理:不同站台可能安裝了不同版本或客製化過的外掛副本,統一用同一份基準線比對容易產生大量假警報,因此每一站都應該有自己專屬的基準檔案。

SEARCH_ROOT="/var/www"
BASELINE_ROOT="/root/checksum-baselines"

mkdir -p "$BASELINE_ROOT"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"
    plugin_dir="$path/wp-content/plugins/user-registration-plugin-for-woocommerce"
    site_key="$(basename -- "$path")"

    if [ ! -d "$plugin_dir" ]; then
        continue
    fi

    find "$plugin_dir" -type f -name "*.php" -print0 |
    xargs -0 sha256sum > "$BASELINE_ROOT/$site_key.sha256"

    printf 'Baseline saved: %s\n' "$BASELINE_ROOT/$site_key.sha256"
done

預期輸出:

Baseline saved: /root/checksum-baselines/shop-a.sha256
Baseline saved: /root/checksum-baselines/shop-b.sha256

分支判斷:建議在所有站台都已確認更新到 2.2.4 且完成一輪排查後才建立這份基準線,這樣未來才有意義;如果是在懷疑已中招的當下才建立,基準線本身可能已經包含被竄改的檔案,需另外搭配下方 Log 與帳號排查交叉判斷。

✴️ Multisite:外掛檔案只需驗證一次,但要留意各 Site 的客製化程式碼(Must-Use Plugins/Custom Hook)

WP_PATH="/var/www/network/public_html"

wp --path="$WP_PATH" core verify-checksums

分支判斷:Multisite 共用同一套外掛檔案,因此雜湊基準線只需對 Network 安裝目錄建立一次;但要另外留意各 Site 若有透過 mu-plugins 或佈景主題 functions.php 掛載額外的自訂 Hook,這些不屬於外掛本體,仍需個別排查,官方指紋比對範圍不會涵蓋這些客製化程式碼。


🧱 🧱 🧱

🧱 ㊆ 加固檢查:修補完之後,順手把安全等級再往上拉一層

版本更新只是回到「沒有已知漏洞」的基準線,真正讓下一次事件發生機率降低的,是這幾項長期加固措施📔

💠 單一網站

WP_PATH="/var/www/example.com/public_html"

# 檢查目前是否已有雙重驗證相關外掛
wp --path="$WP_PATH" plugin list \
    --search="two factor" \
    --fields=name,status

# 檢查 uploads 目錄下是否存在偽裝成圖片的 PHP 檔案
find "$WP_PATH/wp-content/uploads" -type f \
    \( -iname "*.php" -o -iname "*.phtml" -o -iname "*.php*" \) \
    -print

分支判斷:

  • ✔️ 尚未安裝 2FA 相關外掛或未啟用系統內建雙重驗證 👉 建議為所有能存取 wp-admin 的帳號啟用兩步驟驗證,這是防止「即使外掛再出漏洞,帳號也不會被輕易接管」的第二道防線。
  • 🟠 uploads 目錄下出現任何 .php 相關檔案 👉 這個目錄設計上不應該存在可執行的 PHP 檔案,一律視為可疑,先保留備份再評估刪除。
  • 🟡 建議同時確認「使用者角色選擇」若非商業上必要,考慮長期維持關閉狀態,減少攻擊面,而不是等下一次修補完又重新打開卻忘記加固。

❇️ 獨立多站

SEARCH_ROOT="/var/www"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"
    uploads="$path/wp-content/uploads"

    if [ ! -d "$uploads" ]; then
        continue
    fi

    printf '\n===== UPLOADS PHP CHECK: %s =====\n' "$path"

    find "$uploads" -type f \
        \( -iname "*.php" -o -iname "*.phtml" -o -iname "*.php*" \) \
        -print
done

分支判斷:任何一站在 uploads 目錄下出現 PHP 檔案,立即標記為高優先事件,不要等到全部站台掃完才處理,先隔離該站再繼續排查其餘站台。

✴️ Multisite

決策原理:Multisite 的加固建議優先落在 Network 層級的統一政策,例如強制所有 Super Admin 啟用 2FA、限制哪些角色可以在前台觸發註冊流程。

WP_PATH="/var/www/network/public_html"

wp --path="$WP_PATH" super-admin list

分支判斷:確認 Super Admin 清單只有真正需要 Network 管理權限的人員,任何不熟悉的帳號都應立即降權或移除,並比照下方第4節流程保留證據。


🛡️ 🛡️ 🛡️

🛡️ 從「已經中招」到「正式修補」,三種站台的完整救援路線

這一節分成三個情境:A. 已中招/疑似中招(第3節排查出現紅燈)、B. 短期應急(尚未確認中招,但需要立刻降低風險)、C. 正式修補(不論前面結果如何,最終都必須完成的標準作業流程)。三者可以同時進行,不是「做完 A 才能做 B」的線性關係🎓


🆘 🆘 🆘

🆘 A. 已中招/疑似中招情境:先隔離、留證據,再談清除

🚨 畫重點!千萬別踩雷:發現可疑管理員帳號的當下,本能反應千萬不要直接刪除。先降權、保留紀錄,讓自己有機會回溯攻擊時間軸與影響範圍,直接刪除等於把犯罪現場的指紋都擦掉了🧯

💠 單一網站

決策原理:處理順序應為「先降權隔離、再強制登出、再備份存證、最後才是清除」,避免在還沒搞清楚全貌前就永久刪除關鍵資訊。

WP_PATH="/var/www/example.com/public_html"
SUSPECT_LOGIN="jt29xk"
EVIDENCE_DIR="/root/incident-evidence/example-com-$(date +%Y%m%d)"

mkdir -p "$EVIDENCE_DIR"

# 步驟1:保留可疑帳號的完整資料,作為事件證據
wp --path="$WP_PATH" user get "$SUSPECT_LOGIN" \
    --format=json > "$EVIDENCE_DIR/suspect-user.json"

# 步驟2:立刻把可疑帳號降為最低權限,而非直接刪除
wp --path="$WP_PATH" user set-role "$SUSPECT_LOGIN" subscriber

# 步驟3:強制登出該帳號所有現存 Session
wp --path="$WP_PATH" user session destroy "$SUSPECT_LOGIN" --all

echo "Evidence saved to: $EVIDENCE_DIR"

預期輸出:

Success: Changed role for jt29xk to subscriber.
Success: Destroyed sessions.
Evidence saved to: /root/incident-evidence/example-com-20260901

分支判斷:

  • ✔️ 降權與登出都成功 👉 接續執行資料庫冷備份,並比對該帳號建立前後的訂單、商品異動紀錄,確認實際造成的影響範圍。
  • 🔴 發現該帳號已在後台留下額外的自訂管理員、或佈景主題編輯器有異常修改紀錄 👉 影響範圍已超出「單一帳號」層級,應評估是否需要從乾淨備份還原整站,而不只是刪帳號了事。
  • 🟡 無法確定是否還有其他未發現的可疑帳號 👉 回到第3節「後門與帳號檢查」,擴大比對範圍到所有具備寫入權限的角色(不只 administrator,也包含被賦予自訂 Capability 的帳號)。

確認證據保留完成後,執行資料庫存證備份:

WP_PATH="/var/www/example.com/public_html"
EVIDENCE_DIR="/root/incident-evidence/example-com-$(date +%Y%m%d)"

wp --path="$WP_PATH" db export \
    "$EVIDENCE_DIR/db-snapshot-before-cleanup.sql"

❇️ 獨立多站

決策原理:一旦其中一站確認中招,應立即比照第3節的批次帳號檢查結果,優先處理所有「出現相同可疑 Email 網域或相同建立時間窗」的站台,因為這通常代表同一波自動化攻擊。

AFFECTED_SITES="/root/incident-evidence/affected-sites.txt"
# 內容為第3節排查後列出的受影響站台路徑,一行一個

while IFS= read -r path; do
    [ -z "$path" ] && continue

    evidence_dir="/root/incident-evidence/$(basename -- "$path")-$(date +%Y%m%d)"
    mkdir -p "$evidence_dir"

    printf '\n===== ISOLATING: %s =====\n' "$path"

    wp --path="$path" user list \
        --role=administrator \
        --format=json > "$evidence_dir/admin-users.json"

    wp --path="$path" db export \
        "$evidence_dir/db-snapshot-before-cleanup.sql"

done < "$AFFECTED_SITES"

分支判斷:逐站產出的證據檔案應集中留存,並標註網站路徑對應的客戶資訊,方便後續若需要對外說明(例如企業客戶要求提供資安事件報告)時有完整佐證。

✴️ Multisite

決策原理:Multisite 下若確認有 Site 層級的可疑 administrator,先確認是否波及 Network 層的 Super Admin 清單,這是影響範圍評估的關鍵分水嶺。

WP_PATH="/var/www/network/public_html"
SUSPECT_LOGIN="jt29xk"

wp --path="$WP_PATH" super-admin list | grep -x "$SUSPECT_LOGIN" \
    && echo "WARNING: This account also has Super Admin privileges." \
    || echo "OK: Not found in Super Admin list."

分支判斷:若可疑帳號同時出現在 Super Admin 清單 👉 視為 Network 層級重大事件,應立即通知所有 Site 管理者並考慮暫停對外服務進行完整鑑識;若只是單站 administrator 👉 可先集中處理該 Site,但仍建議通報其他 Site 管理者提高警覺。


🔧 🔧 🔧

🛠️ B. 短期應急(尚未確認中招,但版本一時半刻改不了)

如果因為維護窗口、客戶審核流程或其他原因,沒辦法立刻升級外掛版本,這裡提供不用動到程式碼、風險最低的暫時性做法💯

💠 單一網站:直接關閉「使用者角色選擇」開關

決策原理:既然漏洞觸發前提明確要求這個開關必須開啟,把它關掉就能直接切斷攻擊路徑,這是投入產出比最高的暫時性做法,且不需要任何程式碼異動。

建議做法:登入 WordPress 後台,找到 Custom User Registration Fields for WooCommerce 的設定介面,將「使用者角色選擇」功能完全關閉並儲存設定。這一步優先透過後台介面操作,因為前面已確認資料庫欄位名稱未公開,透過介面操作才能確保正確對應到外掛實際讀取的設定值。

操作完成後,可用第3節㊂小節同樣的查詢方式,確認資料庫中相關設定筆數有無變動,作為「有成功儲存」的輔助佐證:

WP_PATH="/var/www/example.com/public_html"

wp --path="$WP_PATH" db query \
    "SELECT option_name, LEFT(option_value, 120) AS preview
     FROM wp_options
     WHERE option_name LIKE '%afreg%'
     ORDER BY option_name;"

分支判斷:關閉後建議實際到前台結帳頁面測試一次,確認角色下拉選單已從表單消失,這比單看後台設定頁面的勾選狀態更可靠,因為前端快取(Page Cache/CDN)可能讓舊版表單還留在使用者眼前。

❇️ 獨立多站:批次提醒 + 逐站人工關閉

決策原理:這個設定沒有公開的 WP-CLI 專屬子指令可以直接批次修改(因為它是外掛自訂的設定介面,不是標準 WordPress Option 結構),因此只能靠批次腳本「找出需要處理的站台清單」,實際關閉動作仍需人工逐站進後台操作。

SEARCH_ROOT="/var/www"
TODO_LIST="/root/incident-evidence/needs-manual-toggle-off.txt"

: > "$TODO_LIST"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"

    if ! wp --path="$path" core is-installed 2>/dev/null; then
        continue
    fi

    version="$(wp --path="$path" \
        plugin get "user-registration-plugin-for-woocommerce" \
        --field=version 2>/dev/null || true)"

    if [ -n "$version" ]; then
        printf '%s | version=%s\n' "$path" "$version" >> "$TODO_LIST"
    fi
done

cat "$TODO_LIST"

預期輸出:產出一份需要人工登入核對/關閉開關的站台待辦清單,含各站目前版本,方便安排處理順序(優先處理版本仍為 2.2.3 以下的站台)。

分支判斷:清單站數不多時建議當天內全部人工核對完畢;站數龐大時,優先處理第3節排查中已標記「Log 出現可疑命中」或「B2B/開放註冊」性質的站台。

✴️ Multisite:確認是否可在 Network 層一次處理

決策原理:若外掛設定介面支援 Network 層級的統一設定頁面,登入 Network 後台操作一次即可套用到所有 Site;若外掛設計上每個 Site 各自獨立設定,仍須逐站處理。

WP_PATH="/var/www/network/public_html"

wp --path="$WP_PATH" site list --field=url |
while IFS= read -r url; do
    version="$(wp --path="$WP_PATH" --url="$url" \
        plugin get "user-registration-plugin-for-woocommerce" \
        --field=version 2>/dev/null || true)"

    if [ -n "$version" ]; then
        printf 'Site needs manual check: %s | version=%s\n' "$url" "$version"
    fi
done

分支判斷:先到 Network 後台確認是否存在統一設定入口;若沒有,就依上方輸出清單逐站處理,並在每站確認完成後記錄時間,方便日後稽核追蹤。


🪛 🪛 🪛

🪛 C. 正式修補:判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證

🩵 荷包試算:升級外掛完全免費、單一網站三分鐘內能做完;但如果拖到被入侵後才處理,資安鑑識費用、清除後門工時、客戶個資外洩後的法律與商譽成本,動輒是「立刻更新」成本的數十倍,這筆帳怎麼算都不划算 💸

💠 單一網站

決策原理:正式修補不是「跑一條 update 指令」而已,完整順序應該是:先備份、再用 Dry-run 確認可更新版本、正式更新、更新後立刻做版本與功能雙重驗證,任何一步失敗都要能安全回退。

WP_PATH="/var/www/example.com/public_html"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"
BACKUP_DIR="/root/backups/example-com-$(date +%Y%m%d-%H%M)"

mkdir -p "$BACKUP_DIR"

cat <<'EOF'
===== STEP 1/5: 資料庫冷備份 =====
EOF
wp --path="$WP_PATH" db export "$BACKUP_DIR/db-backup.sql"

cat <<'EOF'
===== STEP 2/5: 外掛檔案備份 =====
EOF
tar -czf "$BACKUP_DIR/plugin-backup.tar.gz" \
    -C "$WP_PATH/wp-content/plugins" \
    "$PLUGIN_SLUG"

cat <<'EOF'
===== STEP 3/5: Dry-run 確認可更新版本 =====
EOF
wp --path="$WP_PATH" plugin update "$PLUGIN_SLUG" --dry-run

cat <<'EOF'
===== STEP 4/5: 正式更新並清除快取 =====
EOF
wp --path="$WP_PATH" plugin update "$PLUGIN_SLUG"
wp --path="$WP_PATH" cache flush

cat <<'EOF'
===== STEP 5/5: 強制登出所有現有 Session =====
EOF
wp --path="$WP_PATH" user session destroy --all

echo "Backup stored at: $BACKUP_DIR"

預期輸出(節錄):

===== STEP 3/5: Dry-run 確認可更新版本 =====
Plugin update to 2.2.4 available. (Dry run)

===== STEP 4/5: 正式更新並清除快取 =====
Updating 'Custom User Registration Fields for WooCommerce' (2.2.3 => 2.2.4)
Success: Updated 1 of 1 plugins.
Success: The cache was flushed.

===== STEP 5/5: 強制登出所有現有 Session =====
Success: Destroyed sessions.

分支判斷:

  • ✔️ Dry-run 顯示有可更新版本、正式更新成功 👉 進入下方驗證步驟。
  • 🟠 Dry-run 顯示無可更新版本,但後台顯示的版本仍是 2.2.3 以下 👉 【❓待確認:可能是授權金鑰過期或未連結 WooCommerce 帳號導致無法取得更新】,需先確認外掛授權狀態是否有效,而非重複執行更新指令。
  • 🔴 更新過程中斷或報錯 👉 立即從 $BACKUP_DIR 還原外掛檔案,不要在錯誤狀態下讓網站繼續對外服務,排除問題後再重新嘗試。

更新完成後,立即進行版本與關鍵功能雙重驗證:

WP_PATH="/var/www/example.com/public_html"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"

wp --path="$WP_PATH" core is-installed
wp --path="$WP_PATH" plugin get "$PLUGIN_SLUG" \
    --fields=name,status,version

wp --path="$WP_PATH" cron event list \
    --fields=hook,next_run_gmt,recurrence \
    --format=table

預期結果檢查表:

  • ✔️ WordPress 可正常載入
  • ✔️ 外掛版本顯示 2.2.4 或更新
  • ✔️ 前台結帳頁面正常,且角色下拉選單已依你的決策(保持關閉或已加上合法白名單限制)正確呈現
  • ✔️ 後台可正常登入
  • ✔️ PHP Error Log 沒有因為更新而突然大量增加 Fatal Error

最後回到第3節帳號檢查,重新掃描一次確認沒有殘留可疑帳號:

WP_PATH="/var/www/example.com/public_html"

wp --path="$WP_PATH" user list \
    --role=administrator \
    --fields=ID,user_login,user_email,user_registered \
    --format=table

❇️ 獨立多站

決策原理:採「一站一閉環」——每一站都完整走完備份、Dry-run、更新、驗證,全部確認無誤才處理下一站,避免批次腳本讓某一站在錯誤狀態下被跳過。

SEARCH_ROOT="/var/www"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"
BACKUP_ROOT="/root/backups/$(date +%Y%m%d-%H%M)"

mkdir -p "$BACKUP_ROOT"

find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
    path="$(dirname -- "$config")"
    site_key="$(basename -- "$path")"
    backup_dir="$BACKUP_ROOT/$site_key"

    if ! wp --path="$path" core is-installed 2>/dev/null; then
        continue
    fi

    version="$(wp --path="$path" \
        plugin get "$PLUGIN_SLUG" \
        --field=version 2>/dev/null || true)"

    if [ -z "$version" ]; then
        continue
    fi

    mkdir -p "$backup_dir"

    printf '\n===== PROCESSING: %s (current: %s) =====\n' "$path" "$version"

    wp --path="$path" db export "$backup_dir/db-backup.sql"

    tar -czf "$backup_dir/plugin-backup.tar.gz" \
        -C "$path/wp-content/plugins" \
        "$PLUGIN_SLUG"

    wp --path="$path" plugin update "$PLUGIN_SLUG" --dry-run

    wp --path="$path" plugin update "$PLUGIN_SLUG"
    wp --path="$path" cache flush
    wp --path="$path" user session destroy --all

    new_version="$(wp --path="$path" \
        plugin get "$PLUGIN_SLUG" \
        --field=version 2>/dev/null || true)"

    printf 'DONE: %s | %s -> %s\n' "$path" "$version" "$new_version"
done

分支判斷:任一站更新後版本號沒有變化或指令報錯 👉 立即暫停對後續站台的批次處理,先排除該站問題(例如授權金鑰、檔案權限),避免同一個錯誤在多站重複發生卻沒被人工注意到。

✴️ Multisite

決策原理:外掛檔案只需更新一次,但驗證與帳號複查必須逐 Site 進行,因為每個 Site 的資料狀態各自獨立。

WP_PATH="/var/www/network/public_html"
PLUGIN_SLUG="user-registration-plugin-for-woocommerce"
BACKUP_DIR="/root/backups/network-$(date +%Y%m%d-%H%M)"

mkdir -p "$BACKUP_DIR"

cat <<'EOF'
===== STEP 1/4: Network 資料庫備份 =====
EOF
wp --path="$WP_PATH" db export "$BACKUP_DIR/network-db-backup.sql"

cat <<'EOF'
===== STEP 2/4: 外掛檔案備份(Network 只有一份) =====
EOF
tar -czf "$BACKUP_DIR/plugin-backup.tar.gz" \
    -C "$WP_PATH/wp-content/plugins" \
    "$PLUGIN_SLUG"

cat <<'EOF'
===== STEP 3/4: Dry-run 與正式更新 =====
EOF
wp --path="$WP_PATH" plugin update "$PLUGIN_SLUG" --dry-run
wp --path="$WP_PATH" plugin update "$PLUGIN_SLUG"
wp --path="$WP_PATH" cache flush

cat <<'EOF'
===== STEP 4/4: 逐 Site 驗證與強制登出 =====
EOF
wp --path="$WP_PATH" site list --field=url |
while IFS= read -r url; do
    printf '\n----- VERIFY: %s -----\n' "$url"

    wp --path="$WP_PATH" --url="$url" core is-installed
    wp --path="$WP_PATH" --url="$url" plugin get "$PLUGIN_SLUG" \
        --fields=name,status,version
    wp --path="$WP_PATH" --url="$url" user session destroy --all
done

分支判斷:任一 Site 驗證階段出現錯誤 👉 暫停批次流程,先處理該 Site,不要為了趕進度繼續擴大變更範圍;全部 Site 驗證通過 👉 進入最終驗收表格,確認整個 Network 都已脫離風險狀態。


✅ ✅ ✅

✅ 最終驗收:這 7 格全部打勾,才算真正收工

驗收項目 單一網站 獨立多站 Multisite
① 環境判斷 WP Path 逐站 wp-config.php Network + Site List
② 版本+設定核對 2.2.4+,角色選擇已核對 每站 2.2.4+,逐站核對 Network 2.2.4+,逐 Site 核對
③ 備份 DB+外掛檔案 每站獨立備份 Network DB+外掛檔案
④ Dry-run+更新 已完成 逐站完成 Network 完成
⑤ 帳號/Log複查 無新異常 逐站無新異常 Network+Site 無新異常
⑥ 檔案完整性 Core Verified+雜湊基準線已建立 逐站 Verified+各自基準線 Network Verified+基準線
⑦ 功能驗證 前台+後台正常 逐站驗證 重要 Site+Network 驗證

完成標準:不是「WP-CLI 顯示 Updated successfully」就算結束,而是版本已跨過 2.2.4、角色選擇設定已核對、備份可用、帳號與 Log 沒有新異常、Core Checksum 正常、外掛雜湊基準線已建立,前台與後台功能也都正常運作,這樣才算真正把這起事件收尾💯

🧑‍🔧 DevOps 小知識:這起事件真正值得留下來的經驗,不是某一條神奇指令,而是把「漏洞修補」變成一套可以重複執行、可以驗證、可以安全回退的維運流程:判斷環境 👉 盤點 👉 設定核對 👉 備份 👉 Dry-run 👉 更新 👉 驗證。下一次換成別的外掛、別的 CVE,這套方法依然可以直接套用 🎓

[59]


📎 📎 📎

📚 參考資料與延伸資源

列印本文 [65] 👨‍👩‍👧‍👦 31 次瀏覽