你的 WordPress 密碼重置信,可能正被「翻譯秘書」抄進公開字典裡:TranslatePress 高危漏洞全解析(CVE-2026-19632/CVSS 9.8)
🔥 懶人包:
WordPress 知名多語系外掛 TranslatePress 被爆出嚴重漏洞 CVE-2026-19632,CVSS 評分高達 9.8(近乎滿分),影響超過 40 萬個活躍網站。問題出在外掛會把「密碼重置信」的內容當成一般翻譯字串,明文寫進任何人都能透過 AJAX API 讀取的公開翻譯資料庫,等於攻擊者不用破解任何密碼,就能直接撿到管理員的「備用鑰匙」🔑 目前概念驗證攻擊程式碼(PoC)已經公開流傳,強烈建議所有網站管理者立刻更新至 3.3.2 以上版本,並強制重置所有管理員密碼 ⛑️
🌐 前往 TranslatePress 官方外掛頁面確認版本
內容目錄
🧐 這個漏洞到底在講什麼?先用一個生活化比喻搞懂
先別急著看落落長的技術名詞,我們把你的 WordPress 網站想像成一家跨國企業的總部,而 TranslatePress 這個外掛,就像是總部裡一位極度勤奮的「多語系翻譯秘書」。這位秘書有個標準作業流程:只要是他經手翻譯過的任何文件或信件,他都會習慣把「原文與翻譯結果」一字不漏地抄寫在放在大廳的「公開翻譯對照字典」裡,方便下次遇到相同句子時直接查表,不用重新翻譯,節省運算資源 💡
這套流程本身立意良好,問題出在它沒有辨別「機密文件」的能力。當網站最高權限的管理員忘記密碼、系統要寄出一封夾帶「密碼重置連結」的機密信件時,只要這位管理員的個人後台語言剛好設定成外語(不是網站的預設語言),秘書就會依照慣例,把這封信攔截下來翻譯 —— 而那組原本應該保密到家的重置金鑰網址,也就這樣一併被抄進了大廳那本人人都能自由翻閱的公開字典裡 😱
致命的地方在於,攻擊者完全不需要具備高超的駭客技術,也不用硬闖你的防火牆。他們只需要做兩件事:第一,在登入畫面按下「忘記密碼」誘使系統寄信;第二,走到大廳翻開那本公開字典,把剛剛被抄進去的「備用鑰匙」找出來。拿到鑰匙後,直接設定新密碼,就能大搖大擺地以管理員身分走進你的網站總部,為所欲為 🙅♂️
外掛提供了一個讓前端頁面「動態抓翻譯」的 AJAX 端點,但這個端點在設計時完全沒有裝門禁系統,等於是大樓裡一台原本該給員工用的「內部專用電梯」,卻忘記裝刷卡感應器,路上隨便一個陌生人都能直接按下樓層鍵,長驅直入翻閱那本字典裡的任何一頁內容,包含剛剛寫進去的密碼重置金鑰。
📖 事情的來龍去脈:一場與駭客賽跑的時間戰
這起資安事件的發展速度快到令人咋舌,完整呈現了現代網路攻防之間的激烈時間壓力。該漏洞最初由資安研究員 Yuto Hyakumoto(社群暱稱 momopon1415)透過 Wordfence 的漏洞獎金計畫發現,並在通報後拿到 975 美元的漏洞獎金 🏆
| 時間節點 | 具體進展與處置動作 |
|---|---|
| 2026-08-11 | 資安研究員向 Wordfence 提交漏洞報告,確認未經授權的帳號接管風險 |
| 2026-08-12 | 資安團隊完成驗證,緊急聯繫 TranslatePress 開發商 Cozmoslabs |
| 2026-08-13 | 開發團隊極速釋出 3.3.2 修補版本;Wordfence 同日為付費用戶部署 WAF 防火牆規則 |
| 2026-08-25 | 漏洞細節與 CVE-2026-19632 編號正式對外公開,引爆全球資安社群關注 |
| 2026-08-26 | GitHub 與 Telegram 駭客社群出現名為 YonLiud/CVE-2026-19632 的概念驗證攻擊程式碼(PoC) |
| 2026-09-12 | Wordfence 預計將 WAF 防護規則下放至所有免費版用戶 |
值得注意的是,從研究員通報到 PoC 攻擊程式碼公開流傳,前後不到兩週。資安情報顯示,一旦 PoC 公開,攻擊的潛伏期會被壓縮到數小時之內,這代表自動化的惡意機器人極可能已經在全球範圍內,大規模掃描並攻擊那 40 萬個尚未更新的網站,任何拖延都是在跟時間賭運氣 ⏰
💡 碎碎念:什麼是「One-Day PoC」?
「One-Day」指的是漏洞資訊已經公開、官方也已經釋出修補程式,但仍有大量網站因為沒有及時更新而暴露在風險中。跟真正未知的「Zero-Day」不同,這種攻擊其實完全可以避免——只要你有按時更新外掛就沒事,但現實是很多網站主根本沒有定期檢查更新的習慣 🌠
⚠️ 對我的網站有什麼實質影響?
此漏洞被權威機構評定為 🔴 嚴重(CVSS 9.8),這是漏洞評分系統中近乎滿分的危險等級。之所以如此嚴重,是因為攻擊者不需要擁有任何前置帳號密碼,且能完全透過網際網路遠端執行 ⚠️
- ⭐ 資料外洩與隱私災難:攻擊者取得管理員權限後,可毫無阻礙地匯出網站內所有會員資料、訂單紀錄、客戶聯絡方式。若網站是 WooCommerce 電商,可能直接導致商譽受損,甚至觸犯個人資料保護法而面臨鉅額罰款
- ⭐ 網站聲譽與 SEO 排名毀滅:駭客接管後通常不會立刻搞破壞,而是暗中植入惡意程式或博弈、色情廣告連結(俗稱 SEO 毒化)。一旦被 Google Safe Browsing 偵測到,網站會被貼上「可能遭駭客入侵」的紅色警告標籤,瞬間流失所有自然搜尋流量
- ⭐ 長期潛伏的後門風險:攻擊者甚至可能安裝惡意後門外掛或竄改核心程式碼,確保就算你事後重置密碼,他們依然能隨時進出你的伺服器,把主機資源拿去挖礦或發動 DDoS 攻擊
🚨 畫重點!千萬別踩雷:更新外掛只能阻止未來的洩漏,無法讓「已經被寫入資料庫、可能早就被攻擊者下載走」的舊重置金鑰失效。如果你在更新前就已經被盯上,光是更新版本並不夠,後面章節會教你正確的補救順序 🙅♂️
🎭 四種情境對應:你的網站到底該怎麼辦?
🙋 一般網站管理者(部落格主、小型商店)
你不需要看懂任何程式碼,只要做兩件事:登入後台確認 TranslatePress 版本,以及檢查自己有沒有把管理員語言設成跟網站不同的語言。這是最容易被忽略、卻決定你到底有沒有暴露在風險中的關鍵設定 🌠
適合行動:高優先。 立刻更新到 3.3.2 以上,並強制重置管理員密碼,這兩步驟完全不需要程式背景,五分鐘內就能完成 👍
👨💻 獨立開發者/網站代管商(DevOps)
如果你手上管理的是多個客戶的 WordPress 網站,光靠手動點擊後台更新效率太低。建議透過 WP-CLI 批次檢查所有站台的外掛版本與管理員語系設定,並在 WAF 層級預先部署阻擋規則,作為修補前的緩衝防線 🔧
適合行動:中高優先。 這類讀者請直接看後段技術深潛篇的排查指令與緊急防堵程式碼 ⚙️
🏢 企業商業用途(電商、會員制網站)
若你的網站涉及 WooCommerce 交易或大量會員個資,這起漏洞不只是資安問題,更直接牽涉個資法與 GDPR 合規責任。一旦資料外洩被證實,企業面臨的可能不只是商譽受損,還有主管機關的裁罰 🚫
適合行動:極高優先,且需留存證據。 除了更新與重置密碼,建議同步啟動資安事件應變流程,保留稽核日誌以供後續調查與法遵佐證 ⚠️
🚨 高風險場景:你以為跟自己無關,其實正好中招
一間台灣的跨境電商,為了服務歐美 VIP 客戶,特別把管理員後台語言設定成英文,方便國際團隊成員操作。網站主觀認為「我們外掛版本沒有過舊、防火牆也有裝,應該很安全」,卻完全沒意識到,正是這個「管理員語言設為非網站預設語言」的貼心設定,恰好符合了此漏洞被觸發的必要條件——密碼重置信因此被送進翻譯管線,重置金鑰就這樣被寫進了公開字典裡 🆘
⛔️ 這類「因為國際化需求而把管理員語言設成外語」的網站,反而是最容易誤判自己「應該沒事」的高風險族群,請務必優先確認自己的語言設定 🙅♂️
🔍 技術深潛篇:CVE 底層原理拆解(給 DevOps 看)
在系統架構層面,這個漏洞是個很有教育意義的經典案例,展示了兩個「本身都不算惡意」的功能,如何在交互作用下(Feature Interaction)衍生出致命的商業邏輯缺陷(Business Logic Flaw)📊
| 項目 | 內容 |
|---|---|
| CVE 編號 | CVE-2026-19632 |
| 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-640(密碼重置機制薄弱導致的敏感資訊外洩) |
| 受影響版本 | TranslatePress <= 3.3.1 |
| 安全版本 | >= 3.3.2 |
🧑🔧 DevOps 小知識:漏洞成因不是傳統的記憶體破壞或 SQL Injection
這次的問題是「兩個核心模組邏輯耦合」導致的敏感資訊持久化外洩。機制一:外掛在 includes/class-translation-render.php 中實作了 wp_mail_filter(),掛載在 WordPress 核心的 wp_mail() 上,會根據收件者語系動態翻譯信件,並預設開啟「自動儲存字串」功能,把任何尚未存在字典中的字串(包含密碼重置網址)自動寫入 wp_trp_dictionary_[語言代碼] 資料表。機制二:includes/class-editor-api-regular-strings.php 中的 AJAX 端點 trp_get_translations_regular,同時綁定了 wp_ajax_nopriv_trp_get_translations_regular,代表允許未驗證訪客存取,且內部完全缺乏 current_user_can() 或嚴格 Nonce 驗證,任何人都能構造請求把整張字典表撈出來。
🎯 攻擊鏈路:從觸發到接管只要四步
| 階段 | 技術動作 |
|---|---|
| ❶ 觸發 | 攻擊者向 /wp-login.php?action=lostpassword 發送 POST 請求,迫使 WordPress 產生重置金鑰並觸發 wp_mail() 寄信 |
| ❷ 寫入 | 若目標管理員設定了第二語言,包含明文金鑰的網址會被 wp_mail_filter() 攔截,寫入 wp_trp_dictionary 資料表 |
| ❸ 抽取 | 攻擊者向 /wp-admin/admin-ajax.php 發送夾帶 action=trp_get_translations_regular 的請求,利用無驗證缺陷撈出重置網址 |
| ❹ 接管 | 直接在瀏覽器訪問該重置網址,完成密碼重設,取得 Administrator 控制權 |
🟡 合理推論(Probable Escalation):取得管理員權限後,駭客通常會利用內建的佈景主題編輯器寫入惡意 PHP 程式碼,或上傳偽裝外掛,讓攻擊面從應用程式層升級為遠端程式碼執行(RCE)⚠️
🔴 假設情境(Worst-Case):若 DevOps 團隊沒有落實最小權限原則,例如 PHP 進程以 root 身分執行、未配置 open_basedir 限制,攻擊者甚至可能透過 RCE 進行權限提升與橫向移動,在共享主機環境中連帶讓同台伺服器的其他無辜網站一併淪陷 🚫
✅ 資安排查與自我檢查 Checklist
🤖 重要提醒:以下指令與排查流程由 Gemini 產生、Claude 複查
雖然已盡力對照常見實務經驗與 WP-CLI 官方文件,但仍可能因伺服器環境、權限設定或外掛版本差異而產生疏漏。正式環境(尤其是生產站)的高風險操作——包含更新、停用、刪除檔案、變更權限或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter 🧙
如果你的網站曾經跑過 TranslatePress 3.3.1 或更舊版本,真正該問的問題不是「現在有沒有漏洞」,而是「漏洞修掉以前,那本公開字典裡有沒有已經被抄走的東西?」這也是為什麼這一節不採「跑一條掃描指令、看到綠字就收工」的做法,而是照 環境判斷 👉 盤點 👉 觸發條件 👉 Log 👉 資料庫鐵證 👉 帳號/後門 👉 加固 👉 鑑識快照 的順序,把可能留下的痕跡一層一層縮小 🔍
🕵️ 你的網站到底有沒有被「翻譯秘書」出賣過?先把現場一層層挖出來
🔥 一句話先記住:「3.3.2 以上」代表你已經跨過官方公告的修補門檻;它不等於「這台網站歷史上從來沒被抄過鑰匙」。版本修補跟入侵排查,請視為兩件不同的工作來對待🚷
🧭 ㊀ 先判斷環境:你在管一個網站,還是一整台伺服器?
這一步看似無聊,卻是後面所有批次指令能不能安全執行的地基,跳過它,很容易把備份或測試站誤當成正式站處理 📔
| 環境 | 你看到的結構 | 正確思路 | 常用 WP-CLI 定位方式 |
|---|---|---|---|
| 💠 單一網站 | 一個 WordPress 安裝 | 直接在該網站目錄操作 | –path=”$WP_PATH” |
| ❇️ 獨立多站 | 一台 VPS 上有多個完全獨立的 WordPress | 每個 wp-config.php 都代表一個獨立安裝 | –path=”$path” |
| ✴️ Multisite | 一個 WordPress Network,底下有多個 Site | 外掛檔案屬於整個 Network,不是每個 Site 一份 | –url=”$url” |
💠 單一網站:先確認 WP-CLI 真的站在 WordPress 裡
決策原理:先確認目前路徑真的能載入 WordPress,再往下查外掛版本,這比直接執行一長串指令安全許多 🌠
※注意下方的網站路徑 /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 時,「同一台伺服器有很多 WordPress」不代表 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/site-a/public_html WordPress: /var/www/site-b/public_html WordPress: /var/www/site-c/public_html
分支判斷:
- ✅ 每個路徑都能通過 👉 代表可以建立獨立多站 Inventory。
- 🟠 找到 wp-config.php 卻無法載入 WordPress 👉 先確認 PHP、權限或路徑,不要直接更新。
- 🟡 同一個網站被找到多個 wp-config.php 👉 先人工確認哪個才是正式環境,避免誤更新備份或測試站。
✴️ Multisite:這時才把 Network 裡的 Site 列出來
決策原理:wp site list 是 Multisite 專用指令,官方文件明確定義它是用來列出 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 流程;只能列出單一網站不代表一定是獨立多站,還要看是否存在 Network 設定。
🧑🔧 DevOps 小知識:「獨立多站」像同一棟大樓裡有三家公司,各自有自己的門、帳冊與倉庫;Multisite 則比較像同一家公司底下有三個部門,外觀看起來都是「三個網站」,但資料庫、外掛與更新邏輯完全不同 🚫
🔎 ㊁ 盤點:先回答「哪些站裝了 TranslatePress、目前到底是哪一版?」
目前公開情資將 3.3.1 以下列入受影響範圍,3.3.2 為官方修補版本;但 WordPress.org 目前掛牌的正式版本已經更新到 3.3.4,所以判斷標準建議設成「至少 3.3.2,但能拉多新就拉多新」🎓
💠 單一網站:不要自己解析 PHP,讓 WP-CLI 回報版本
決策原理:外掛盤點應以 WordPress 自己認得的 Plugin metadata 為主,而不是自己 grep PHP 檔案猜版本。
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin get "translatepress-multilingual" \
--fields=name,status,version,update,update_version
預期輸出:
name: translatepress-multilingual status: active version: 3.3.1 update: available update_version: 3.3.4
分支判斷:
- 🔴 3.3.1 或更舊 👉 視為受影響,進入觸發條件排查與備份/修補流程。
- 🟢 3.3.2 或更新 👉 此漏洞的版本條件已解除,但若網站曾經跑過舊版,仍建議繼續往下做入侵排查。
- 🟠 找不到外掛 👉 確認是否已移除、是否改了 slug,並確認沒有查錯網站路徑。
❇️ 獨立多站:逐站查,不要把不同網站混成一份結果
決策原理:每個獨立 WordPress 都有自己的外掛安裝狀態,因此應逐一使用 –path。
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
version="$(wp --path="$path" \
plugin get "translatepress-multilingual" \
--field=version 2>/dev/null || true)"
if [ -n "$version" ]; then
printf 'Site: %s | TranslatePress: %s\n' "$path" "$version"
fi
done
預期輸出:
Site: /var/www/site-a/public_html | TranslatePress: 3.3.4 Site: /var/www/site-b/public_html | TranslatePress: 3.3.1 Site: /var/www/site-c/public_html | TranslatePress: 3.2.9
分支判斷:這份結果就是你的第一張「風險地圖」:B、C 應優先列入修補名單;A 可以繼續往下做歷史排查即可。
✴️ Multisite:外掛檔案只盤點一次,再從 Site 層確認狀態
決策原理:Multisite 共用同一套外掛檔案,因此不能把每個 Site 當成一個獨立外掛安裝來更新。
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin get "translatepress-multilingual" \
--fields=name,status,version,update,update_version
wp --path="$WP_PATH" site list \
--field=url
分支判斷:外掛版本以 Network 安裝的檔案為核心;Site 層則用 –url=”$url” 確認各站載入後的實際啟用狀態,官方文件也把 –url 定義為 Multisite 指定目標 Site 的方式。
🌐 ㊂ 觸發條件排查:這裡才是這次漏洞真正的「開關」
版本過舊只是必要條件,這個漏洞真正會被引爆,還需要同時滿足兩個觸發條件:①「自動儲存字串」功能是開著的(預設值就是開);②有管理員的個人語系被設成網站上已發布的次要語言。只要任一條件不成立,攻擊鏈就斷了,所以這一步的優先權其實不輸給版本盤點 💯
💠 單一網站:先看有沒有「語系跟網站不同」的管理員
決策原理:WordPress 使用者的個人語系存放在 locale 這個 usermeta,空字串代表「跟隨網站預設語言」;只要不是空字串,就代表這個帳號有被設成特定語言,值得逐一檢查。
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login,user_email \
--format=csv |
tail -n +2 |
while IFS=, read -r id login email; do
locale="$(wp --path="$WP_PATH" user meta get "$id" locale 2>/dev/null || true)"
printf 'ID=%s | login=%s | locale=%s\n' "$id" "$login" "${locale:-<site-default>}"
done
預期輸出:
ID=1 | login=admin | locale=<site-default> ID=7 | login=support_de | locale=de_DE
分支判斷:
- 🟢 所有管理員 locale 都顯示 <site-default> 👉 這台網站的第二個觸發條件不成立,風險相對較低,但仍建議更新版本。
- 🔴 有管理員 locale 被設成已發布的次要語言(如上例的 de_DE) 👉 若版本又 ≤ 3.3.1,代表兩個觸發條件同時成立,優先度拉到最高。
接著確認「自動儲存字串」這個設定,因為它是序列化陣列,不建議用 WP-CLI 猜測內部欄位結構去硬改,這裡只做唯讀確認:
WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" option get trp_settings --format=json
分支判斷:若輸出內容找不到停用自動翻譯字串儲存的相關設定,請直接登入後台「TranslatePress → 設定 → 進階」介面確認並手動關閉,不要嘗試用指令直接改寫這個序列化選項,避免把整個設定陣列寫壞【❓待確認:官方文件未公開此選項的完整欄位結構,故不建議用 CLI 直改,改用後台介面最保險】。
❇️ 獨立多站:逐站確認,帳號跟語系設定每站都不一樣
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 \
--format=csv |
tail -n +2 |
while IFS=, read -r id login; do
locale="$(wp --path="$path" user meta get "$id" locale 2>/dev/null || true)"
printf ' ID=%s | login=%s | locale=%s\n' "$id" "$login" "${locale:-<site-default>}"
done
done
分支判斷:某一站出現「舊版本 + 非預設語系管理員」的組合 👉 該站優先處理;其他站即使版本較舊,只要語系正常,急迫度可以稍微往後排一點,但仍要修補。
✴️ Multisite:Network 帳號跟 Site 使用狀態要分開看
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login \
--format=csv |
tail -n +2 |
while IFS=, read -r id login; do
locale="$(wp --path="$WP_PATH" user meta get "$id" locale 2>/dev/null || true)"
printf 'ID=%s | login=%s | locale=%s\n' "$id" "$login" "${locale:-<site-default>}"
done
分支判斷:Multisite 的 Super Admin 帳號權限橫跨整個 Network,一旦某個 Super Admin 的 locale 被設成次要語言,風險等同於整個 Network 都曝險,不能只當成單一 Site 的問題處理。
🧾 ㊃ Log 排查:找「重置信 + AJAX 撈字典 + 拿鑰匙開門」這一串時間序列
公開情資描述的攻擊鏈路是三個請求的組合:先觸發密碼重置信,接著呼叫危險的 AJAX 端點撈字典,最後拿撈出來的金鑰去登入頁完成接管。因此排查 Log 時,真正有價值的不是找某個「神奇 IP」,而是確認這三個特徵是否在短時間內連續出現 📔
💠 單一網站:先找得到 Log,再做關鍵字比對
決策原理:不同主機環境的 Access Log 路徑完全不同,因此不要把 /var/log/apache2/access.log 當成所有伺服器的固定答案。
LOG_FILE="/var/log/apache2/access.log"
if [ -f "$LOG_FILE" ]; then
grep -Ei \
'action=lostpassword|trp_get_translations_regular|action=rp&key=' \
"$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,可能會看到類似這樣的序列:
... "POST /wp-login.php?action=lostpassword HTTP/1.1" 200 ... ... "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 ... (body 含 action=trp_get_translations_regular) ... "GET /wp-login.php?action=rp&key=XXXXXXXX&login=admin HTTP/1.1" 200 ...
分支判斷:
- 🟢 只出現第一段(觸發重置信),且來源就是網站主自己 👉 屬於正常操作。
- 🟡 出現第二段,但發起請求的 IP 沒有合法登入 Cookie 👉 提高事件優先級,接著比對是否緊接著出現第三段。
- 🔴 三段特徵在數秒到數分鐘內連續出現,且來自同一個陌生 IP 👉 高度懷疑已遭利用,直接進入本文第 4 節「已中招」流程。
❇️ 獨立多站:每一個 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 \
'action=lostpassword|trp_get_translations_regular|action=rp&key=' \
"$log" 2>/dev/null || true)"
if [ -n "$matches" ]; then
printf '\n===== %s =====\n' "$log"
printf '%s\n' "$matches" | tail -n 50
fi
done
分支判斷:某站出現命中 👉 標記為需要進一步鑑識;全部沒有命中 👉 風險降低,但不能證明「絕對沒有被攻擊」,因為 Log 可能已輪替或遺失。
✴️ Multisite:同一份 Web Server Log,從 URL/時間再定位 Site
LOG_FILE="/var/log/apache2/access.log"
if [ -f "$LOG_FILE" ]; then
grep -Ei \
'action=lostpassword|trp_get_translations_regular|action=rp&key=' \
"$LOG_FILE" |
tail -n 100
fi
分支判斷:找到可疑時間點後,再用 wp site list 取得 Network Site 清單,依 Log 中的 Host/URL 判斷哪個 Site 受影響,這一步不要只看「外掛是不是啟用」,因為 Network 中各 Site 的使用狀態可能不同。
🚨 畫重點:Log 裡出現 trp_get_translations_regular 不等於「一定已經成功接管」;同樣地,完全找不到這串字,也不能保證「完全沒被摸過」。Log 只是證據之一,真正的鐵證要靠下一節的資料庫查詢 🙅♂️
🗄️ ㊄ 資料庫鐵證:字典表裡到底有沒有躺著一把備用鑰匙?
這是整起事件最關鍵的排查——如果重置金鑰真的曾被寫進公開字典,它會留在 wp_trp_dictionary_[語言代碼] 這一系列資料表裡,就算後來密碼已經改過,只要這行紀錄還在,就代表曾經被「翻譯秘書」抄過一次,需要一併清理 ⛑️
💠 單一網站:先列出所有語言字典表,再逐一撈
決策原理:字典表是每個語言各自一張,命名規則固定為 wp_trp_dictionary_[語言代碼],官方 wp db tables 指令支援萬用字元搜尋,可以先取得完整清單,不用自己憑印象猜語言代碼。
WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv
預期輸出:
wp_trp_dictionary_de_de wp_trp_dictionary_fr_fr wp_trp_dictionary_ja
接著針對每一張表,查詢是否存在密碼重置連結特徵:
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
hit="$(wp --path="$WP_PATH" db query \
"SELECT COUNT(*) FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--skip-column-names 2>/dev/null || true)"
printf '%s : %s\n' "$table" "${hit:-0}"
done
預期輸出:
wp_trp_dictionary_de_de : 1 wp_trp_dictionary_fr_fr : 0 wp_trp_dictionary_ja : 0
分支判斷:
- 🟢 所有表都回傳 0 👉 目前沒有找到重置金鑰被寫入字典的直接證據。
- 🔴 任何一張表回傳 1 以上 👉 這是漏洞已被觸發的鐵證,先不要刪除,改用下方指令把完整內容匯出保存,再進入本文第 4 節「已中招」流程作廢金鑰。
若要保留完整證據內容,可用以下方式匯出可疑紀錄,不要只看到數字就直接刪掉:
WP_PATH="/var/www/example.com/public_html"
EVIDENCE_FILE="$HOME/trp-dictionary-hits-$(date +%Y%m%d_%H%M%S).txt"
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
wp --path="$WP_PATH" db query \
"SELECT id, original, status FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--format=csv 2>/dev/null |
sed "s/^/${table},/"
done > "$EVIDENCE_FILE"
echo "Evidence saved to: $EVIDENCE_FILE"
❇️ 獨立多站:每一站的字典表都是獨立資料庫,逐站掃
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
tables="$(wp --path="$path" db tables 'wp_trp_dictionary_*' --format=csv 2>/dev/null || true)"
if [ -z "$tables" ]; then
continue
fi
printf '\n===== DICTIONARY CHECK: %s =====\n' "$path"
printf '%s\n' "$tables" |
while IFS= read -r table; do
hit="$(wp --path="$path" db query \
"SELECT COUNT(*) FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--skip-column-names 2>/dev/null || true)"
printf ' %s : %s\n' "$table" "${hit:-0}"
done
done
分支判斷:某一站任何字典表命中 👉 只針對該站進入「已中招」流程,不要因為其他站沒事就掉以輕心,也不要因為單一站中招就直接對整台 VPS 做無差別清理。
✴️ Multisite:字典表通常隨 Network 主資料庫共用一份
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
hit="$(wp --path="$WP_PATH" db query \
"SELECT COUNT(*) FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--skip-column-names 2>/dev/null || true)"
printf '%s : %s\n' "$table" "${hit:-0}"
done
分支判斷:找到命中紀錄後,再依內容與時間對照 wp site list 取得的 Site 清單與 Access Log,判斷金鑰對應的是哪一個 Site 的管理員,不要因為資料庫共用就直接假設整個 Network 淪陷。
👤 ㊅ 帳號與後門檢查點:駭客留下的未必只有一行資料庫紀錄
如果金鑰真的被撈走並成功登入,檔案與帳號才是後續持久化的重點,這一步跟資料庫查詢同樣重要。
💠 單一網站:列出管理員、檢查活躍 Session、掃 mu-plugins
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=table
wp --path="$WP_PATH" user session list "1" --format=table
find "$WP_PATH/wp-content/mu-plugins" -maxdepth 2 -type f -name "*.php" -print 2>/dev/null
分支判斷:
- 🟡 任何不認識的管理員帳號都值得進一步確認,但「陌生」本身不等於惡意,可能是維運商或既有帳號。
- 🔴 user_registered 時間點恰好落在可疑 Log 時間附近 👉 高度懷疑是攻擊者自建的幽靈帳號,先留證再處理。
- 🔴 mu-plugins 目錄出現不屬於你部署流程、檔名怪異的 PHP 檔 👉 極可能是後門,不要直接刪除,先移到隔離區保存原始權限與時間戳記。
❇️ 獨立多站:每個 WordPress 各自列管理員與 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")"
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
find "$path/wp-content/mu-plugins" -maxdepth 2 -type f -name "*.php" -print 2>/dev/null
done
分支判斷:某站出現未知管理員或可疑 mu-plugins 檔案 👉 優先標記該站進行事件調查,不要因為同一台 VPS 上的其他站看起來正常就跳過它。
✴️ Multisite:先釐清 Network Administrator 跟各 Site 管理員的差異
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" user list \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=table
find "$WP_PATH/wp-content/mu-plugins" -maxdepth 2 -type f -name "*.php" -print 2>/dev/null
分支判斷:發現未知帳號後,再進一步確認它具備哪個 Network/Site 層級的權限,不要只看到帳號存在就直接刪除,因為 Super Admin 與一般 Site 管理員的影響範圍完全不同。
🧱 ㊆ 加固檢查:就算目前沒中招,也要把兩個觸發開關一起關掉
這起漏洞最值得記住的教訓是:光更新版本只解決了「條件一(版本過舊)」,「條件二(觸發設定)」如果沒有一併檢查,未來換一個類似機制的外掛,同樣的錯誤配置依然可能重演👍
💠 加固項目一:把管理員語系全部拉回網站預設值
決策原理:把 locale 這個 usermeta 更新成空字串,就等於讓該帳號跟隨網站預設語言,這是官方情資建議的緩解手段之一。
WP_PATH="/var/www/example.com/public_html" TARGET_USER_ID="7" wp --path="$WP_PATH" user meta update "$TARGET_USER_ID" locale ""
預期輸出:
Success: Updated custom field 'locale'.
分支判斷:若該管理員本來就需要用外語操作後台介面(例如國際團隊成員),建議改用瀏覽器層級的翻譯功能,或等外掛正式修補後再視情況恢復,不要在漏洞尚未修補前保留這個設定。
💠 加固項目二:確認「自動儲存字串」已經關閉
由於這個選項是序列化陣列,官方公開文件也只建議透過後台介面調整,因此這裡維持第 3 節提到的唯讀確認方式,實際關閉請走「TranslatePress → 設定 → 進階」介面操作,不建議用指令直接改寫這個選項。
❇️/✴️ 獨立多站與 Multisite:批次巡檢語系設定
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===== LOCALE CHECK: %s =====\n' "$path"
wp --path="$path" user list \
--role=administrator \
--fields=ID,user_login \
--format=csv |
tail -n +2 |
while IFS=, read -r id login; do
locale="$(wp --path="$path" user meta get "$id" locale 2>/dev/null || true)"
printf ' ID=%s | login=%s | locale=%s\n' "$id" "$login" "${locale:-<site-default>}"
done
done
分支判斷:把巡檢結果做成一份清單,逐一確認每個非空 locale 是否真的有業務需求,沒有需求的一律改回空字串,這一步請每季重複執行一次,而不是修完這次事件就再也不檢查。
📦 ㊇ 建立鑑識快照:看到異常時,先留證再刪東西
如果你已經在前面幾節看到可疑帳號、字典命中或異常 Log,不建議第一反應就是 rm -rf 或直接 DELETE,因為刪掉之後,反而可能把最有價值的證據一起清空 🧯
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
EVIDENCE_DIR="$HOME/trp-evidence-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE_DIR"
wp --path="$WP_PATH" core version \
> "$EVIDENCE_DIR/core-version.txt"
wp --path="$WP_PATH" plugin get "translatepress-multilingual" \
--fields=name,status,version,update,update_version \
> "$EVIDENCE_DIR/trp-version.txt"
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login,user_email,display_name,user_registered \
--format=csv \
> "$EVIDENCE_DIR/administrators.csv"
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
wp --path="$WP_PATH" db query \
"SELECT id, original, status FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--format=csv 2>/dev/null |
sed "s/^/${table},/"
done > "$EVIDENCE_DIR/dictionary-hits.csv"
echo "Evidence directory: $EVIDENCE_DIR"
預期輸出:
Evidence directory: /home/admin/trp-evidence-20260904_120000
分支判斷:如果確定存在疑似入侵跡象,先把這份 Evidence Directory 保存到受保護的位置,再進入下一節的止血流程。
❇️ 獨立多站
每個網站建立獨立證據目錄,不要讓 A 站與 B 站的資料混在一起。
SEARCH_ROOT="/var/www"
EVIDENCE_ROOT="$HOME/trp-evidence-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE_ROOT"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' config; do
path="$(dirname -- "$config")"
safe_name="$(printf '%s' "$path" | tr '/ ' '__')"
evidence="$EVIDENCE_ROOT/$safe_name"
if ! wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
mkdir -p "$evidence"
wp --path="$path" plugin get "translatepress-multilingual" \
--fields=name,status,version,update,update_version \
> "$evidence/trp-version.txt" 2>/dev/null || true
wp --path="$path" user list \
--role=administrator \
--fields=ID,user_login,user_email,user_registered \
--format=csv \
> "$evidence/administrators.csv"
done
echo "Evidence root: $EVIDENCE_ROOT"
分支判斷:每個子目錄就是一個獨立站點的鑑識資料,後續才能把「哪個網站、哪個時間、哪個帳號」對得起來。
✴️ Multisite
WP_PATH="/var/www/network/public_html"
EVIDENCE_DIR="$HOME/trp-multisite-evidence-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE_DIR"
wp --path="$WP_PATH" core version \
> "$EVIDENCE_DIR/core-version.txt"
wp --path="$WP_PATH" plugin get "translatepress-multilingual" \
--fields=name,status,version,update,update_version \
> "$EVIDENCE_DIR/trp-version.txt"
wp --path="$WP_PATH" site list \
--fields=blog_id,url \
--format=csv \
> "$EVIDENCE_DIR/sites.csv"
echo "Evidence directory: $EVIDENCE_DIR"
分支判斷:Multisite 的證據應以 Network 為中心,再依 Site URL 與時間軸細分,不需要重複 dump 同一套 Network 資料。
🛟 鑰匙真的被撿走了怎麼辦?從「先止血」一路走到「確認關門」
前一節解決的是「怎麼判斷」。這一節進入真正的維運 Runbook:
| 階段 | 目的 | 核心動作 |
|---|---|---|
| ① 判斷環境 | 避免操作錯網站 | 單站/獨立多站/Multisite |
| ② 盤點 | 知道誰受影響 | 版本、觸發條件、Site Inventory |
| ③ 備份 | 保留回復點 | DB+必要網站檔案 |
| ④ Dry-run | 先看更新會做什麼 | –dry-run |
| ⑤ 更新 | 關閉版本漏洞 | 升級至 3.3.2 以上(建議拉到最新版) |
| ⑥ 驗證 | 確認修補真的完成 | 版本+Checksum+功能+Log |
🔥 這裡最重要的閉環:
判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證。更新成功的訊息不代表這件事結束——已經外洩的舊金鑰依然有效,這是這起事件跟一般版本更新最大的不同 🙅♂️
🚨 A. 已中招/疑似中招:先讓外洩的鑰匙全部作廢,再談修補
如果第 3 節的資料庫查詢已經命中,或 Log 出現完整的三段攻擊特徵,處理順序跟「單純版本更新」不一樣——這裡的第一優先,是讓已經外流的重置金鑰跟任何可能存在的登入 Session 全部失效 ⛑️
💠 單一網站:先建立事件狀態,再依序作廢金鑰與 Session
決策原理:疑似入侵時,最大的敵人不是漏洞本身,而是在沒有留下證據的情況下直接清理,所以第一步一定是先固化現況。
WP_PATH="/var/www/example.com/public_html"
INCIDENT_DIR="$HOME/trp-incident-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$INCIDENT_DIR"
{
echo "=== Incident collection ==="
date -Is
echo "WP_PATH=$WP_PATH"
wp --path="$WP_PATH" core version
wp --path="$WP_PATH" plugin get "translatepress-multilingual" \
--fields=name,status,version,update,update_version
} > "$INCIDENT_DIR/summary.txt" 2>&1
echo "Incident evidence: $INCIDENT_DIR"
預期輸出:
Incident evidence: /home/admin/trp-incident-20260904_121500
證據固化完成後,立即進行三個止血動作:作廢字典裡的金鑰紀錄、強制重置所有管理員密碼、終止所有現存 Session。
WP_PATH="/var/www/example.com/public_html"
# 步驟一:把命中的重置金鑰紀錄從字典表刪除(先確認第 3 節的證據已匯出保存)
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
wp --path="$WP_PATH" db query \
"DELETE FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';"
done
# 步驟二:強制重置所有管理員密碼(WP-CLI 會產生隨機新密碼並顯示於輸出)
wp --path="$WP_PATH" user list \
--role=administrator \
--field=ID |
while IFS= read -r id; do
wp --path="$WP_PATH" user update "$id" --user_pass="$(wp --path="$WP_PATH" eval 'echo wp_generate_password( 20, true, true );')"
done
# 步驟三:終止所有目前登入的 Session
wp --path="$WP_PATH" user list --field=ID |
while IFS= read -r id; do
wp --path="$WP_PATH" user session destroy "$id" --all
done
預期輸出:
Success: Updated user 1. Success: Sessions destroyed.
分支判斷:
- ✅ 三個步驟都執行完畢 👉 攻擊者手上的舊金鑰與任何既有登入 Session 已經同時失效,可以進入下方「正式修補」流程。
- 🔴 mu-plugins 目錄先前有查到可疑檔案 👉 在重置密碼之前,先把該檔案的檔案權限改成 000 並移出網站目錄隔離,避免它在你操作期間持續運作。
- 🟡 是企業/電商/會員制網站 👉 同步啟動內部資安事件應變流程,把 $INCIDENT_DIR 底下的證據留存,供後續稽核與法遵佐證使用。
❇️ 獨立多站:只隔離有證據的站,避免整台 VPS 一起誤傷
決策原理:獨立多站的最大優勢就是隔離性,某一站疑似遭入侵時,先確認受影響網站,再針對該站執行止血動作。
INCIDENT_PATH="/var/www/site-b/public_html"
if wp --path="$INCIDENT_PATH" core is-installed; then
echo "Incident target confirmed: $INCIDENT_PATH"
else
echo "ERROR: target is not a WordPress installation."
fi
wp --path="$INCIDENT_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
wp --path="$INCIDENT_PATH" db query \
"DELETE FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';"
done
wp --path="$INCIDENT_PATH" user list --field=ID |
while IFS= read -r id; do
wp --path="$INCIDENT_PATH" user session destroy "$id" --all
done
分支判斷:確認路徑正確 👉 再執行網站級止血;不要把整個 /var/www 當成單一網站處理,也不要因為 Site B 中招就連 Site A、Site C 一起強制重置密碼。
✴️ Multisite:先把事件視為 Network 級問題
決策原理:Multisite 共用核心、外掛與資料庫,因此某個 Site 出現入侵跡象時,不能只處理那個 Site 就宣稱完成。
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" site list \
--fields=blog_id,url \
--format=table
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
wp --path="$WP_PATH" db query \
"DELETE FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';"
done
wp --path="$WP_PATH" user list --field=ID |
while IFS= read -r id; do
wp --path="$WP_PATH" user session destroy "$id" --all
done
分支判斷:任一 Site 出現明確入侵跡象 👉 把整個 Network 的管理員都納入密碼重置與 Session 清除範圍,因為 Super Admin 權限本來就橫跨所有 Site。
🧯 B. 短期應急:尚未確認中招,但現在還不能立刻升級
短期應急的目標不是「修好漏洞」,而是縮短兩個觸發條件同時成立的時間窗口 🦺
🚨 短期應急 ≠ 正式修補。
關閉觸發條件、部署 WAF 規則或暫時停用外掛都屬於緩衝措施;真正的修補仍然是把版本升級到官方安全版本以上 🦺
💠 單一網站:優先切斷「語系」這個開關,比停用整個外掛更務實
決策原理:相較於直接停用外掛(可能影響前台多語系功能),先確認並修正管理員語系設定,通常是干擾最小、卻能立刻讓觸發條件不成立的做法。
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login \
--format=csv |
tail -n +2 |
while IFS=, read -r id login; do
locale="$(wp --path="$WP_PATH" user meta get "$id" locale 2>/dev/null || true)"
if [ -n "$locale" ]; then
echo "Resetting locale for $login (ID=$id)"
wp --path="$WP_PATH" user meta update "$id" locale ""
fi
done
如果評估後認為網站現階段可以不靠 TranslatePress 運作,也可以直接暫時停用:
WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" plugin deactivate "translatepress-multilingual"
預期輸出:
Plugin 'translatepress-multilingual' deactivated. Success: Deactivated 1 of 1 plugins.
分支判斷:
- 🟢 可以安全停用 👉 保留停用狀態,盡快進入下方正式修補。
- 🟠 前台多語系功能是核心業務 👉 不要在正式環境直接停用外掛,改採「修正語系設定」+「WAF 規則」雙重防護,並盡快安排升級窗口。
在 Web Server 或 WAF 層,建議部署緊急防護規則,攔截所有 URI 包含 /wp-admin/admin-ajax.php、HTTP Method 為 POST,且內容含有 trp_get_translations_regular,同時缺乏合法登入 Cookie 的請求:
# 這裡只記錄「防禦規則應針對的端點」, # 不提供可直接複製到正式環境的特定 Web Server 語法。 # # 目標端點: # POST /wp-admin/admin-ajax.php (action=trp_get_translations_regular) # # 原則: # 1. 僅在確定該站使用 TranslatePress 時套用 # 2. 先在測試環境驗證 # 3. 以 403/拒絕作為預期防禦結果 # 4. 正式修補後重新評估規則是否仍有必要保留
🚨 不要把 WAF 規則當成永久解法。WAF 的角色比較像大樓門口臨時增加的保全,真正有問題的門鎖還是得換掉 🔓
❇️ 獨立多站:逐站做應急,不要一次動到整台 VPS 的所有站
INCIDENT_PATH="/var/www/site-b/public_html"
wp --path="$INCIDENT_PATH" user list \
--role=administrator \
--fields=ID,user_login \
--format=csv |
tail -n +2 |
while IFS=, read -r id login; do
locale="$(wp --path="$INCIDENT_PATH" user meta get "$id" locale 2>/dev/null || true)"
if [ -n "$locale" ]; then
echo "Resetting locale for $login (ID=$id) on $INCIDENT_PATH"
wp --path="$INCIDENT_PATH" user meta update "$id" locale ""
fi
done
分支判斷:只影響 Site B,不應連 Site A、Site C 一起強制改動語系設定。
✴️ Multisite:先確認外掛是否為 Network Activated
WP_PATH="/var/www/network/public_html"
if wp --path="$WP_PATH" plugin is-active \
"translatepress-multilingual" \
--network; then
echo "TranslatePress is Network Activated."
else
echo "TranslatePress is not Network Activated."
fi
預期輸出:
TranslatePress is Network Activated.
分支判斷:如果是 Network Activated,語系巡檢與應急動作要把整個 Network 的管理員都納入範圍,不要以為只調整某個 Site 的帳號就能把共用外掛的風險隔離。
🧑🔧 DevOps 小知識:WP-CLI 的 –network 是 Network 層操作;Multisite 的 –url=”$url” 則是指定某一個 Site,兩者不是同一個概念,混用容易造成「以為改了全部,其實只改了一個」的誤判 🙅
💾 C-1. 正式修補:備份,修補前先留下可以回家的路
💠 單一網站:資料庫+必要網站檔案
決策原理:外掛更新主要改的是外掛檔案本身,但資料庫內容(含前面清理過的字典表)仍值得留一份回復點 ❤️🩹
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_trp_update_${STAMP}.sql"
tar -czf \
"$BACKUP_DIR/example-com_wp-content_pre_trp_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_trp_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")"
if ! wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
site_name="$(basename "$path")"
db_file="$BACKUP_ROOT/${site_name}_pre_trp_update_${STAMP}.sql"
echo "=== Backup: $path ==="
wp --path="$path" db export "$db_file"
printf 'Database backup: %s\n' "$db_file"
done
分支判斷:某一站備份失敗,就把該站標記為未完成,不要因為其他站成功就直接進入批次更新。
✴️ 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_trp_update_${STAMP}.sql"
tar -czf \
"$BACKUP_DIR/multisite_wp-content_pre_trp_update_${STAMP}.tar.gz" \
-C "$WP_PATH" \
"wp-content"
分支判斷:Database+必要檔案備份成功 👉 進 Dry-run;不需要因為 Network 底下有 20 個 Site 就 dump 20 次同一個資料庫。
🧪 C-2. Dry-run:讓 WP-CLI 先說它準備更新什麼
WP-CLI 官方文件明確支援 wp plugin update –dry-run,用途就是預覽會被更新的外掛與目標版本,而不真正執行更新 📔
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin update \
"translatepress-multilingual" \
--dry-run
預期輸出:
Plugin translatepress-multilingual 3.3.1 will be updated to 3.3.4.
分支判斷:看到目標版本 👉 可以進行正式更新;沒有可用更新 👉 回頭確認目前版本與外掛來源,不要因為「No updates available」就直接認定網站安全。
❇️ 獨立多站:每站各自 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")"
if ! wp --path="$path" core is-installed 2>/dev/null; then
continue
fi
version="$(wp --path="$path" \
plugin get "translatepress-multilingual" \
--field=version 2>/dev/null || true)"
if [ -n "$version" ]; then
printf '\n===== %s | current=%s =====\n' "$path" "$version"
wp --path="$path" plugin update \
"translatepress-multilingual" \
--dry-run
fi
done
分支判斷:先確認每站 Dry-run 結果都合理,再進正式更新,第一批正式環境建議仍採「一站完成 👉 驗證 👉 下一站」的節奏,不要一次全部下去。
✴️ Multisite:外掛檔案只 Dry-run 一次
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin update \
"translatepress-multilingual" \
--dry-run
分支判斷:不要因為 Network 裡有 10 個 Site 就執行 10 次外掛更新 Dry-run,外掛檔案屬於同一個 WordPress installation。
🚀 C-3. 正式更新:拉到 3.3.2 以上,能拉多新就拉多新
⚠️ 安全提醒:正式環境升級外掛前,務必先於測試環境備份並驗證,避免網站崩潰🛟
💠 單一網站:更新一站、驗證一站
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin update \
"translatepress-multilingual"
預期輸出:
Updating 'translatepress-multilingual'... Plugin updated successfully. Success: Updated 1 of 1 plugins.
分支判斷:更新成功 👉 立即進版本驗證;更新失敗 👉 不要反覆重跑,先查看錯誤訊息、確認檔案權限與備份是否可用。
❇️ 獨立多站:一站一個完整閉環
WP_PATH="/var/www/site-a/public_html"
wp --path="$WP_PATH" plugin update \
"translatepress-multilingual"
wp --path="$WP_PATH" plugin get \
"translatepress-multilingual" \
--fields=name,status,version,update,update_version
分支判斷:Site A 更新+驗證成功後,再把同樣流程套到 Site B,不要「全部更新完最後才檢查」。
✴️ Multisite:外掛檔案只更新一次,再逐 Site 驗證
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin update \
"translatepress-multilingual"
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 "translatepress-multilingual" \
--fields=name,status,version
done
分支判斷:外掛檔案版本只需要更新一次;Site 層則逐一確認是否正常載入與啟用。
🔐 C-4. Checksum 驗證:確認更新進去的檔案沒被動過手腳
💠 單一網站
決策原理:官方的 wp plugin verify-checksums 會拿外掛檔案與 WordPress.org 提供的 checksum 比對,適合做檔案完整性檢查,但不是完整入侵鑑識。
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin verify-checksums \
"translatepress-multilingual"
wp --path="$WP_PATH" core verify-checksums
預期輸出:
Success: Verified 1 of 1 plugins. Success: WordPress installation verifies against checksums.
分支判斷:
- 🟢 兩者皆 Verified 👉 外掛與核心檔案 checksum 都符合官方版本。
- 🔴 出現 Checksum mismatch 👉 不要忽略,檢查是否有人手動修改檔案,並回到第 3 節重新做入侵鑑識。
- 🟡 Checksum 無法驗證 👉 不代表一定遭入侵,可能是 WordPress.org 對該版本沒有提供對應 checksum。
❇️ 獨立多站
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===== CHECKSUM: %s =====\n' "$path"
wp --path="$path" plugin verify-checksums \
"translatepress-multilingual"
wp --path="$path" core verify-checksums
done
分支判斷:每個網站獨立判斷,不要拿 A 站的結果代替 B、C 站。
✴️ Multisite
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" plugin verify-checksums \
"translatepress-multilingual"
wp --path="$WP_PATH" core verify-checksums
外掛與核心檔案共用同一套 WordPress installation,因此同樣只需驗證一次。
🚨 Checksum 不是「無罪證明書」。它只能回答「這些由 WordPress.org 提供 checksum 的檔案,現在符不符合官方版本」,回答不了字典資料表、mu-plugins、管理員帳號是否曾經被動過手腳,這部分請以第 3 節的排查結果為準🦹♂️
🧪 C-5. 修補後回歸測試:不要只看版本號,網站真的還活著嗎?
💠 單一網站
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" core is-installed
wp --path="$WP_PATH" plugin get \
"translatepress-multilingual" \
--fields=name,status,version
wp --path="$WP_PATH" user list \
--role=administrator \
--fields=ID,user_login \
--format=csv |
tail -n +2 |
while IFS=, read -r id login; do
locale="$(wp --path="$WP_PATH" user meta get "$id" locale 2>/dev/null || true)"
printf 'ID=%s | login=%s | locale=%s\n' "$id" "$login" "${locale:-<site-default>}"
done
預期結果:
- 🏮 WordPress 可正常載入
- 🏮 TranslatePress 顯示 3.3.2 或更新版本
- 🏮 所有管理員 locale 都已回到網站預設值
- 🏮 前台語言切換功能正常
- 🏮 後台可正常登入
- 🏮 PHP Error Log 沒有突然大量增加 Fatal Error
❇️ 獨立多站
採「一站一驗證」:
WP_PATH="/var/www/site-a/public_html"
wp --path="$WP_PATH" core is-installed
wp --path="$WP_PATH" plugin get \
"translatepress-multilingual" \
--fields=name,status,version
echo "Verification target: $WP_PATH"
Site A 完成後再處理 Site B、C。
✴️ Multisite
Network 外掛驗證完成後,再逐一測試重要 Site:
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" site list --field=url |
while IFS= read -r url; do
printf '\n===== VERIFY SITE: %s =====\n' "$url"
wp --path="$WP_PATH" \
--url="$url" \
core is-installed
wp --path="$WP_PATH" \
--url="$url" \
plugin get "translatepress-multilingual" \
--fields=name,status,version
done
分支判斷:任一 Site 出現錯誤 👉 暫停批次流程,先處理該 Site,不要繼續擴大變更範圍。
🔍 C-6. 最後一輪複查:確認修補後沒有新的異常
💠 單一網站
LOG_FILE="/var/log/apache2/access.log"
WP_PATH="/var/www/example.com/public_html"
if [ -f "$LOG_FILE" ]; then
grep -Ei \
'action=lostpassword|trp_get_translations_regular|action=rp&key=' \
"$LOG_FILE" |
tail -n 50
fi
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
hit="$(wp --path="$WP_PATH" db query \
"SELECT COUNT(*) FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--skip-column-names 2>/dev/null || true)"
printf '%s : %s\n' "$table" "${hit:-0}"
done
分支判斷:更新後仍持續出現可疑 AJAX 請求、或字典表再度出現命中紀錄 👉 不要把它當成「已修好所以沒事」,應回到第 4 節 A 情境重新進行事件調查。
❇️ 獨立多站
逐站執行同樣檢查,結果一定要保留網站路徑。
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===== POST-PATCH CHECK: %s =====\n' "$path"
wp --path="$path" db tables 'wp_trp_dictionary_*' --format=csv 2>/dev/null |
while IFS= read -r table; do
hit="$(wp --path="$path" db query \
"SELECT COUNT(*) FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--skip-column-names 2>/dev/null || true)"
printf ' %s : %s\n' "$table" "${hit:-0}"
done
done
✴️ Multisite
WP_PATH="/var/www/network/public_html"
wp --path="$WP_PATH" db tables 'wp_trp_dictionary_*' --format=csv |
while IFS= read -r table; do
hit="$(wp --path="$WP_PATH" db query \
"SELECT COUNT(*) FROM \`${table}\` WHERE original LIKE '%action=rp&key=%';" \
--skip-column-names 2>/dev/null || true)"
printf '%s : %s\n' "$table" "${hit:-0}"
done
分支判斷:任何修補後新增的可疑紀錄,都應回到「事件應變」而不是單純重新更新外掛。
🧾 C-7. 最終驗收:這 7 格全部打勾,才算真正收工
| 驗收項目 | 單一網站 | 獨立多站 | Multisite |
|---|---|---|---|
| ① 環境判斷 | WP Path | 逐站 wp-config.php | Network+Site List |
| ② 版本+觸發條件 | 3.3.2+,locale 已重置 | 每站 3.3.2+,locale 已重置 | Network 3.3.2+,Super Admin locale 已重置 |
| ③ 備份 | DB+必要檔案 | 每站獨立 DB | Network DB+必要檔案 |
| ④ Dry-run | 已完成 | 逐站完成 | Network 完成 |
| ⑤ Checksum | 外掛+核心 Verified | 逐站 Verified | Network Verified |
| ⑥ 字典表複查 | 0 命中 | 逐站 0 命中 | Network 0 命中 |
| ⑦ 帳號安全 | 密碼已重置+Session 已清空 | 逐站已重置+清空 | 全 Network 管理員已重置+清空 |
✅ 完成標準:不是「WP-CLI 顯示 Updated successfully」就算結束,而是版本已跨過 3.3.2、觸發條件(語系設定)已排除、字典表沒有殘留金鑰、所有管理員密碼與 Session 都已強制更新,前台後台功能也都正常運作 🦸
🫂 新手求助:完全看不懂上面這些指令怎麼辦?
如果你不熟悉 WP-CLI 或伺服器操作,最快的方式是把這份 Checklist 直接轉傳給你的主機代管商或網站維護廠商,跟他們說「這個外掛有 CVE-2026-19632 漏洞,麻煩幫我確認字典表跟管理員語系設定並完成修補」,多數台灣主機商都有提供這類技術支援管道,不用不好意思開口求助 📚







