🚨 網站大門沒鎖?免帳密就能偷偷塞指令的 Forminator 表單外掛漏洞(CVE-2026-92229)大解密!
🔥 懶人包:
是的,又㕛叒叕來了,前幾天才出包過的🚨表單裡的下拉選單,居然是駭客的後門?60萬站受影響 Forminator Forms 高危漏洞全解析(CVE-2026-15748/CVSS 9.8) [1],全球裝機量超高的 WordPress 表單外掛 Forminator Forms 被爆出一個超大漏洞(CVE-2026-92229,CVSS 危險分數高達 9.1 分)。駭客「完全不用登入、連填表單都不必」,就能隔空叫你的網站執行任意短代碼。問題出在外掛把一個叫 current_url 的參數,毫無防備地直接丟給系統執行。如果你還在用 1.57.2 以下 版本,不要懷疑請立刻以手刀衝刺的速度升級到 1.57.3 以上 🏎️
內容目錄
🧐 第一章:超商店員太老實?幫忙收表單的小外掛,怎麼變成駭客的遙控器?
想像一下,你家樓下超商有一個超熱心、但有點少根筋的店員(也就是我們的外掛 Forminator Forms)。平常他的工作很簡單,就是幫忙收客人填好的問卷表單 ✍️
每次客人交問卷時,這位店員都會隨口問一句:「你是從哪個路口走過來的啊?」(這在程式裡叫做 current_url 參數,本來只是用來記錄來源網址)。
正常人會回答「中山路口」,但如果遇到詐騙集團(駭客),他故意回答了一段「超商廣播系統的隱藏控制碼」(這就是 WordPress 裡的短代碼 Shortcode)。結果這位太老實的店員,竟然沒有檢查這個回答是不是正常路名,就直接對著全店的廣播系統把控制碼唸了出來!於是,駭客就算沒穿店員制服(不需要帳號密碼),也能輕鬆遙控這家店的廣播系統 📢
換句話說,這次出包不是 Forminator 本身壞掉,而是它忘了核對身分,就隨便幫人廣播指令!而這個指令到底有多大破壞力,完全取決於你的網站還裝了哪些會被觸發的短代碼,簡直就像是在玩俄羅斯輪盤 💣
🧊 冷知識:短代碼(Shortcode)原本是懶人福音
短代碼是 WordPress 早期推出的功能,像這種用[]方括號包起來的咒語。[gallery]原本是為了讓電腦小白不用寫程式,打幾個字就能變出相簿或表單。沒想到十幾年後,當這句咒語能被陌生人「從外面偷偷唸出來」時,反而變成了超危險的資安死角 🪄
⏱️ 漏洞爆發時間軸:偷偷補洞的標準節奏
資安圈的慣例是「先把洞補好,再廣播告訴大家」,我們來看看這次的時間軸:
| 日期 | 事件節點 | 發生了什麼事 |
|---|---|---|
| 2026-08-27 | 1.57.2 版釋出 | 官方更新日誌僅含糊寫著「安全性改進」,此時大眾還不知道有這個漏洞 🟡。 |
| 2026-09-17 | 1.57.3 版釋出 | 這個版本被資安社群視為針對漏洞的真正修補版,強烈建議大家升級 🟡。 |
| 2026-09-18/19 | 細節正式揭露 | Wordfence 等資安情報站正式公開漏洞細節,各大平台同步收錄 ✅。 |
| 2026-09-19 之後 | 概念驗證(PoC)流出 | 駭客用來測試攻擊的工具已經在 GitHub 上公開,代表攻擊門檻變超低 ✅。 |
🚨 畫重點!這才是負責任的處理方式:
你會發現 1.57.3 修補版早在細節公開前就悄悄上線了。這是資安圈標準的「負責任揭露」——先給大家好好換鎖的時間,再來討論原來舊鎖哪裡有問題,才不會教壞歹徒開鎖 🔐
不過這裡要補充一個小道消息【❓待確認】:不同資安情報站對於「最先通報此漏洞的研究員是誰」說法不一,有的寫單一人名,有的列出一大串,目前就先等官方日後統一說法囉 🤯
⚠️ 說白了,駭客能拿這個漏洞對我的網站幹嘛?
這漏洞最賊的地方在於:它自己不會引爆,但如果你網站上有其他「易燃物」(其他外掛的敏感短代碼),那就慘了。我們照事實確定的程度拆解給你看:
- 🏮 ✅ 已確認事實:攻擊者完全不需要登入帳號,也不用跟表單有任何互動,只要連上網站就能發動攻擊。CVSS 的權限(PR)與互動(UI)分數都是最低門檻
- 🏮 ✅ 已確認事實:攻擊者可以叫你的網站執行任何已經註冊好的短代碼指令
- 🏮 🟡 合理推論:要是你網站剛好裝了「可以匯出會員資料」或「建立新使用者」的短代碼,就有機會被駭客串接利用,導致資料大洩漏
- 🏮 🔴 假設情境:在最倒霉的情況下,若被串連到具備檔案寫入能力的短代碼,理論上有機會被植入後門,整台網站直接被端走
😫 碎碎念:被自己名字坑了的 Forminator
Forminator 這個名字是 Form(表單)加上 Terminator(終結者)組成的,本來是想說要「終結大家做表單的煩惱」。結果這次社群都在笑說:真的是終結者被自己的短代碼給終結了 😅
🎭 第二章:別急著拔電源!對號入座,看看你是哪一種苦主?
遇到資安危機別慌張,先喝口手搖飲 🧋 看看你屬於下面哪一種情況,應對方式大不同:
🙋 一般輕量使用者:我只是開個小部落格,放個聯絡表單而已
。適合度:需要優先處理,但超簡單,請直接滑到第三章看「五分鐘無痛自救指南」。如果你的網站只是寫寫遊記、放作品集,裝 Forminator 純粹是為了讓讀者填表單找你,那你不用太焦慮!但千萬別因為「站上沒機密」就擺爛。因為這個漏洞門檻超低,只要外掛還開著,你家大門就是敞開的,隨時會被波及💯
👨💻 獨立開發者/自架玩家:我手上同時管著好幾個客戶的網站
如果你像包租公一樣管著一整批 VPS 伺服器,手動一個個登入後台絕對會點到崩潰。建議你晚點直接跳去第五章的「技術深潛篇」,那邊有準備好的 WP-CLI 批次盤點與更新終端機指令,直接貼上就能跑 👇
🏢 企業商業用途:我的網站有會員系統,甚至會收信用卡!
如果是電商老闆或會員平台,這就不只是「按個更新」能解決的了。你必須認真盤點站上是不是還有其他高權限的短代碼(例如匯出訂單、產生優惠券)。如果舊版暴露在網路上太久,強烈建議啟動內部的資安應變流程,把所有的稽核紀錄留存下來,未來不管是查帳還是跟客戶交代,都非常需要 🩺
想像一間小行銷公司,三年前用 Forminator 架了一個心理測驗活動頁,活動結束後就放著不管了。老闆心想:「反正沒在用了,不用管它。」
大錯特錯! 🙅 這就像是家裡有個三年沒開的後門,鎖早就生鏽了。駭客製造了一個「假車禍」(偽造的 current_url 參數),連互動都不用,就直接透過這個生鏽的後門,串接了另一套會員管理系統的指令,悄悄把你的會員 Email 全部撈走!等到你發現客戶收到奇怪的詐騙信,早就來不及了 🆘
🚥 畫重點:「很久沒出包」不等於「很安全」。只要外掛還在啟用名單裡,駭客就有縫可以鑽 🙅♂️
🧑🔧 DevOps 小知識:為什麼「只按更新」有時候不夠?
更新外掛就像是把被撬壞的門鎖換成新的,能擋住下一次的壞人。但是!如果駭客早就在你還沒換鎖時溜進來,在你家沙發底下藏了備用鑰匙(後門檔案),你換再多鎖,他一樣能大搖大擺走進來。所以排查後門跟更新一樣重要 🛡️
🛟 第三章:阿公阿嬤也能懂!五分鐘無痛自救 SOP,跟著點就對了
無論你是哪種角色,這一段建議每個人都跟著做一遍——完全不用寫程式,動動滑鼠就能搞定 🤏
- ㊀ 先去哪裡看版本?
登入 WordPress 後台,目光移到左邊選單找「外掛」👉「已安裝的外掛」。在長長的清單裡找出 Forminator,名字下方就會顯示你現在的版本號 🔢 - ㊁ 怎麼看我現在安不安全?
睜大眼睛看!如果版本號是 1.57.2 或是更小的數字,代表你現在就像裸奔一樣危險!如果已經是 1.57.3 或更大,恭喜你暫時安全了,可以去泡杯茶 🍵 - ㊂ 要馬上更新嗎?
廢話,當然要!而且是立刻、馬上!直接在那個畫面點擊「立即更新」按鈕。等圈圈轉完,重新整理網頁,確定數字變成 1.57.3 或更大就大功告成了 💪 - ㊃ 按更新會不會害網站壞掉或跑版?
別怕!這次修補的是內部「驗證指令來源」的邏輯,根本沒動到表單的外觀,所以跑版當機的機率極低。但優良傳統還是要保持:按更新前,先用主機商的「一鍵備份」功能備份一下,積陰德保平安 🛟 - ㊄ 當偵探抓內鬼:檢查「管理員」名單
到後台找「使用者」👉「所有使用者」,看看有沒有你不認識的帳號。如果看到名字是一串亂碼、Email 網域超怪,或是註冊時間剛好在最近幾天,先截圖存證!千萬別急著自己亂刪 🔍 - ㊅ 我真的不敢自己亂點怎麼辦?
超正常!你可以把這篇文章的網址直接貼給幫你顧網站的工程師或主機商客服,大喊:「救命!我的外掛有 CVE-2026-92229 漏洞,請幫我更新並檢查!」台灣的主機商通常都很佛心,找專家幫忙絕對是最聰明的選擇 🤗
🫂 新手求助:沒備份過,怕一失足成千古恨怎麼辦?
如果你連備份按鈕在哪都不知道,請先到後台的「工具」👉「匯出」,把內容下載下來當個底。接著直接撥電話或發服務單給你的主機商客服,請他們幫忙確認系統有沒有每天自動備份,確認安全無虞後再按更新,晚上睡得比較安穩 🤗。
🤿 第四章:技術深潛篇——這串漏洞的程式碼底層到底哪裡出包
接下來這段是給想知道「為什麼會這樣」的人看的——如果你只想知道怎麼補洞,前面幾章其實已經夠用了。但如果你也好奇這種等級的漏洞底層長什麼樣子,我們繼續往下拆 ⚔️
| 評估指標 | 具體資訊 |
|---|---|
| 外掛名稱 | Forminator Forms – Contact Form, Payment Form & Custom Form Builder |
| 開發廠商 | WPMU DEV |
| CVE 編號 | CVE-2026-92229 |
| CVSS 3.1 評分 | 9.1(Critical) |
| CVSS 向量 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H【❓待確認:不同情資站對完整性/可用性衝擊子項標示不完全一致,核心的「網路可達、低複雜度、免權限、免互動、機密性衝擊高」已交叉確認,其餘子項細節仍請以官方最終公告為準】 |
| 受影響版本 | ≤ 1.57.2 |
| 安全版本 | ≥ 1.57.3(2026-09-17 釋出) |
| 漏洞類型 | 未經身分驗證的任意短代碼執行(Unauthenticated Arbitrary Shortcode Execution)/CWE-94 程式碼生成控制不當 |
| 是否已遭在野利用 | 概念驗證(PoC)已公開,✅已確認事實;尚未列入 CISA KEV 清單,✅已確認事實 |
🧊 冷知識:CWE-94 早就是資安圈的「老面孔」了
CWE-94(程式碼生成控制不當)這個分類早在多年前就被寫進通用弱點列舉資料庫,涵蓋範圍非常廣——從 PHP 世界裡的 eval()、create_function(),到 WordPress 專屬的 do_shortcode(),只要「讓未經驗證的外部輸入流向會動態執行程式碼的函式」,都會被歸進這一類。Forminator 這次的問題,剛好就是最經典的那一種:外掛忘了先確認「這段字串,真的是我允許執行的指令嗎」 🗝️
🟡 合理推論(依多份公開技術分析交叉比對,官方尚未公開完整修補 diff):問題出在外掛處理表單/測驗前端提交時的邏輯上。Forminator 會讀取請求裡帶的 current_url 參數,原本設計上這只是拿來記錄「訪客從哪個網址提交表單」,但外掛沒有先對這個字串做過濾或白名單檢查,就直接把它交給 WordPress 核心的 do_shortcode() 函式去解析執行。這代表只要字串裡藏著方括號包起來的短代碼語法,系統就會把它當成合法指令執行,而不是單純的一段文字內容。
🧑🔧 DevOps 小知識:「導向用的參數」跟「顯示用的內容」,天生就不該混在一起處理
像 current_url 這類原本設計來做網址導向、提交後跳轉的參數,理論上只該經過 esc_url_raw() 之類的網址驗證,絕對不該直接進到會渲染內容的函式裡。這次事件可以理解成一種「信任邊界錯置」——本來只該拿來判斷「要跳去哪」的資料,卻被拿去「決定要顯示什麼」,這種分類錯誤在表單類外掛裡其實蠻常見的,值得所有自己寫外掛或客製化功能的開發者引以為戒 🔧
🎯 骨牌是怎麼一片片倒下去的:概念層級的攻擊路徑
整條攻擊鏈揉合了「免驗證觸發」跟「條件式串聯」兩個特性,拆開來看邏輯其實相當工整,以下僅為防禦目的之概念層級說明,不提供可執行的攻擊細節:
- 🏮 第一步:確認目標裝了受影響版本。攻擊者能透過網站前端頁面原始碼或常見指紋辨識工具,判斷是否安裝了 Forminator,以及版本是否落在受影響範圍。
- 🏮 第二步:發送帶有惡意短代碼的請求。攻擊者對站上任何載入 Forminator 前端功能的端點(例如透過 admin-ajax.php 提交表單或測驗的流程),發送一個 current_url 參數裡夾帶方括號短代碼語法的請求。整個過程不需要登入,也不需要真的跟任何一份表單互動過。
- 🏮 第三步:伺服器端解析並執行短代碼。外掛在處理這個請求時,把攻擊者控制的字串直接交給 do_shortcode() 解析,觸發字串中包含的短代碼邏輯。
- 🏮 第四步:如果環境中剛好有可串聯的短代碼,骨牌開始倒下。若站上其他外掛剛好註冊了具備敏感操作能力的短代碼,攻擊者就有機會把這次的免驗證入口,跟那些敏感功能「拼」在一起,達成資訊洩露甚至更嚴重的後果。
✅ 已確認的影響:攻擊者能在完全不驗證身分的情況下,讓外掛執行他指定的短代碼。🟡 合理推論的影響:若環境中存在可用的高權限短代碼,可能被串聯成資訊洩露或帳號建立。🔴 假設情境的影響:在最嚴重的狀況下,攻擊者可能藉此在系統中留下後門,以伺服器為跳板進行更大範圍的入侵——但目前尚無公開實際案例證實這個情境已經發生 🛑
🕵️♀️ 第五章:資安排查 Checklist——DevOps 該怎麼確認自己有沒有已經中招
🤖 重要提醒:以下指令與排查流程由 DeepSeek、Gemini、Meta AI 產生、Claude 複查修正
雖然已盡力對照常見實務經驗與 WP-CLI 官方文件,統一改寫成路徑雙引號包覆、`find -print0` 安全迴圈等寫法,但仍可能因伺服器環境、權限設定或外掛版本差異而產生疏漏。正式環境(尤其是正在營業中的網站)的高風險操作——包含更新、停用、刪除檔案、變更權限或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter 🧙
如果你是自己架 VPS、同時管理多個 WordPress,這一節最重要的不是「背一條神奇迴圈指令」,而是先搞清楚自己到底是哪一種 WordPress 架構。搞錯架構,後面的批次指令就算語法完全正確,也可能處理錯對象;這一節會依序走過環境判斷 👉 盤點(版本比對)→ Log 日誌排查 👉 後門檢查點 👉 加固檢查五個關卡,並且分別涵蓋單一網站、獨立多站、Multisite 三種環境 📜
🧭 ㊀ 環境判斷:你在管一個 WordPress,還是一整台 WordPress 伺服器?
決策原理:單一站、獨立多站、Multisite 三者的 WP-CLI 操作對象與資料範圍完全不同。判斷錯誤會導致後續盤點、備份、更新全部跑偏——例如對獨立多站裡的某個網站以為是 Multisite 子站處理,結果外掛檔案其實各自獨立,白做工還可能造成版本不一致。
WP_PATH="/var/www/example.com/public_html"
# ❶ 檢查是否為 Multisite 網路環境(不產生輸出,靠結束碼判斷)
if wp --path="$WP_PATH" core is-installed --network 2>/dev/null; then
echo "這是 Multisite 網路"
else
echo "這不是 Multisite 網路"
fi
# ❷ 檢查 wp-config.php 中是否定義了 MULTISITE 常數
grep -n "MULTISITE" "$WP_PATH/wp-config.php"
# ❸ 若不是 Multisite,尋找系統中所有的 wp-config.php,確認是單站還是獨立多站
sudo find /var/www -type f -name "wp-config.php" -print0 2>/dev/null |
while IFS= read -r -d '' CONFIG; do
printf '找到網站設定檔:%s\n' "$CONFIG"
done
預期輸出範例:
# ❶ 若為 Multisite 這是 Multisite 網路 # ❷ 的輸出 12:define( 'MULTISITE', true ); 13:define( 'SUBDOMAIN_INSTALL', false ); # ❸ 若為獨立多站,會列出多個路徑 找到網站設定檔:/var/www/site1.com/public_html/wp-config.php 找到網站設定檔:/var/www/site2.com/public_html/wp-config.php
分支判斷邏輯:
- ⭕ ❶ 顯示「這是 Multisite 網路」→ 進入 ✴️ Multisite 流程,記得外掛檔案是全網共用,通常只需在網路層級操作一次。
- ⭕ ❶ 顯示「這不是 Multisite 網路」,且 ❸ 只找到一個 wp-config.php → 💠 單一網站。
- ⭕ ❸ 找到多個 wp-config.php,且分別位於不同目錄、彼此無關 → ❇️ 獨立多站,後續每一站要各自處理,不能混在一起跑。
🔎 ㊁ 盤點(版本比對):先回答「哪些站裝了它、目前是哪一版?」
決策原理:不建議在指令裡把安全版本寫死成 1.57.3,而是讓 WP-CLI 動態抓取 WordPress.org 官方認定的目前版本與可更新版本,確保無論什麼時候執行都能拿到正確資訊 💪
# 💠 單一網站
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin get forminator --fields=name,status,version,update,update_version
# ❇️ 獨立多站:逐站盤點,動態抓取檔案擁有者,避免權限錯亂
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' CONFIG; do
WP_PATH="$(dirname -- "$CONFIG")"
OWNER="$(stat -c '%U' -- "$CONFIG" 2>/dev/null || echo 'www-data')"
printf '=== 網站:%s(擁有者:%s) ===\n' "$WP_PATH" "$OWNER"
sudo -u "$OWNER" -- wp --path="$WP_PATH" plugin get forminator \
--field=version 2>/dev/null || printf '(外掛未安裝)\n'
done
# ✴️ Multisite:外掛檔案全網共用,查一次主站即可代表整個網路
WP_PATH="/var/www/network.example.com/public_html"
wp --path="$WP_PATH" plugin get forminator --fields=name,status,version,update,update_version
預期輸出範例:
name: forminator status: active version: 1.57.1 update: available update_version: 1.57.3
分支判斷邏輯:
- 🔴 版本為 1.57.2 或更舊 → 視為受影響,進入第六章的備份/應急/修補流程。
- 🟢 版本已是 1.57.3 或更新 → 版本條件已解除,但如果這個站曾經對外公開超過一段時間,仍建議繼續往下做 Log 排查與後門檢查,確認舊版暴露期間沒有被人摸過。
- 🟠 顯示「(外掛未安裝)」→ 該網站不受此漏洞影響,無需進一步處理。
📊 ㊂ Log 日誌排查特徵:不是看到怪字元就緊張,而是找「參數+特徵+回應」的組合
決策原理:此漏洞是透過 current_url 參數觸發,官方目前未公開確切的觸發端點特徵,因此以下屬於 🟡 合理推論的通用排查方式,並非官方證實的攻擊指紋——短代碼語法必定帶有方括號 [ 與 ](含 URL 編碼形式 %5B 與 %5D),我們就以此為線索掃描存取日誌 🔎
ACCESS_LOG="/var/log/nginx/access.log"
ERROR_LOG="/var/log/nginx/error.log"
# 若使用 Apache,請改成:
# ACCESS_LOG="/var/log/apache2/access.log"
# ERROR_LOG="/var/log/apache2/error.log"
# ❶ 搜尋帶有 current_url 參數,且夾帶短代碼特徵([ ] 或其 URL 編碼)的請求
if [ -f "$ACCESS_LOG" ]; then
sudo grep -aE "current_url=.*(%5B|\[).*(%5D|\])?" "$ACCESS_LOG" | tail -n 50
else
printf 'LOG NOT FOUND: %s\n' "$ACCESS_LOG"
fi
# ❷ 搜尋錯誤日誌中與短代碼解析相關的異常訊息
if [ -f "$ERROR_LOG" ]; then
sudo grep -ai "do_shortcode\|forminator" "$ERROR_LOG" | tail -n 30
fi
預期輸出範例:
# 若未遭觸發:無任何符合輸出 # 若發現異常: 203.0.113.45 - - [19/Sep/2026:14:32:01 +0800] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 4521 "..." "current_url=http%3A%2F%2Fexample.com%2F%5Bsome_shortcode%5D"
分支判斷邏輯:
- 🟢 只有正常的表單提交紀錄,沒有短代碼特徵 → 目前沒有直接證據顯示已遭觸發,但仍建議繼續往下做後門檢查。
- 🟡 出現短代碼特徵,但回應狀態碼是 4xx/5xx → 可能是攻擊嘗試失敗,仍建議記錄來源 IP 供後續觀察。
- 🔴 出現短代碼特徵,且回應為 200,時間點又跟後續發現的異常帳號吻合 → 進入第六章「A. 已中招/疑似中招」流程。
- 🟡 日誌已被輪替或清空 → 檢查 logrotate 設定,確認是否還有備份日誌可查。
若是獨立多站環境,可用以下寫法逐一掃描每個網站各自的日誌檔,避免漏掉某個 VirtualHost:
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="$(sudo grep -aE "current_url=.*(%5B|\[)" "$LOG" 2>/dev/null || true)"
if [ -n "$MATCHES" ]; then
printf '\n===== %s =====\n' "$LOG"
printf '%s\n' "$MATCHES" | tail -n 30
fi
done
🕳️ ㊃ 後門檢查點:檔案完整性不能只看外表
決策原理:若短代碼執行真的被串成攻擊鏈,攻擊者常見的落腳點是 mu-plugins 目錄(因為這裡的檔案不需要在後台被「啟用」就會自動載入)、近期被修改過的 PHP 檔案,以及資料庫裡異常的管理員帳號或選項值 🕵️♀️
WP_PATH="/var/www/example.com/public_html"
MU_PLUGINS="$WP_PATH/wp-content/mu-plugins"
# ❶ 檢查 mu-plugins 目錄
if [ -d "$MU_PLUGINS" ]; then
find "$MU_PLUGINS" -maxdepth 2 -type f -iname "*.php" -print
else
printf 'MU-PLUGINS DIRECTORY NOT FOUND: %s\n' "$MU_PLUGINS"
fi
# ❷ 檢查近 14 天內被修改過的 PHP 檔案(排除快取目錄避免雜訊)
find "$WP_PATH" -type f -name "*.php" -mtime -14 -not -path "*/wp-content/cache/*" -print
# ❸ 檢查是否有異常的管理員帳號
wp --path="$WP_PATH" user list --role=administrator --fields=ID,user_login,user_email,user_registered
# ❹ 檢查 wp_options 中跟 forminator 相關的異常記錄(用 heredoc 避免多層跳脫符號互相干擾)
QUERY_FILE="$(mktemp)"
cat > "$QUERY_FILE" <<'EOF'
SELECT option_name, LEFT(option_value, 200) AS value_preview, autoload
FROM wp_options
WHERE option_name LIKE '%forminator%'
ORDER BY option_id DESC
LIMIT 20;
EOF
wp --path="$WP_PATH" db query < "$QUERY_FILE"
rm -f -- "$QUERY_FILE"
預期輸出範例:
# ❶ 正常情況(無異常檔案) MU-PLUGINS DIRECTORY NOT FOUND: /var/www/example.com/public_html/wp-content/mu-plugins # ❸ 的輸出 +----+------------+-------------------+---------------------+ | ID | user_login | user_email | user_registered | +----+------------+-------------------+---------------------+ | 1 | admin | admin@example.com | 2024-01-15 08:00:00 | +----+------------+-------------------+---------------------+
分支判斷邏輯:
- 🔴 mu-plugins 目錄裡出現不是你們部署流程建立的 PHP 檔案,特別是檔名看起來像亂數字串 → 高度可疑,先隔離不要直接刪除,並進入第六章「A. 已中招」流程。
- 🟡 發現近 14 天內被修改的 PHP 檔案,且修改時間跟異常登入或提交時間吻合 → 需進一步檢查檔案內容。
- 🟡 wp_options 裡出現含有 base64_encode 或 eval 字串的可疑選項值 → 高度可疑,需人工鑑識。
- 🟢 上述四項都沒有發現異常 → 這一輪檢查沒有找到明確跡象,但任何不認識的管理員帳號仍值得再確認一次——「陌生」不等於「惡意」,可能是維運商的既有帳號,先問過再判斷。
🔧 ㊄ 加固檢查:就算修好了,也把第二道門一起鎖起來
決策原理:更新外掛是根本,但這次的漏洞類型(未驗證輸入流向程式碼執行函式)本身提醒我們,就算這次補好了,同一類問題也可能在別的外掛裡重演——所以除了升級之外,還應該從 PHP 執行環境跟伺服器層防護兩個角度收斂攻擊面,降低萬一再出現類似漏洞時的「爆炸半徑」 🔍
# ❶ 備份設定檔(用時間戳記避免覆蓋掉之前的備份) PHP_INI="/etc/php/8.4/fpm/php.ini" sudo cp -- "$PHP_INI" "$PHP_INI.BAK.$(date +%F_%H%M%S)" # ❷ 停用高風險函式(冪等替換:重複執行也不會出問題) sudo sed -i -E 's/^[;#]*\s*disable_functions\s*=.*/disable_functions = exec,passthru,shell_exec,system,proc_open,popen/' "$PHP_INI" # ❸ 重啟 PHP-FPM 讓設定生效 sudo systemctl restart php8.4-fpm # ❹ 確認設定已生效 php -i | grep disable_functions # ❺ 檢查並收緊 wp-config.php 的檔案權限 WP_PATH="/var/www/example.com/public_html" sudo chmod 640 "$WP_PATH/wp-config.php" stat -c "%a %n" "$WP_PATH/wp-config.php"
若想在伺服器層先擋一道,可以用以下 Nginx 規則暫時攔截帶有短代碼特徵的 current_url 參數(套用前務必先在測試環境驗證,避免誤擋正常請求):
sudo tee /etc/nginx/conf.d/forminator_block_current_url.conf > /dev/null <<'EOF'
# Temporary mitigation for CVE-2026-92229
if ($args ~* "current_url=.*(%5B|\[)") {
return 403;
}
EOF
sudo nginx -t && sudo systemctl reload nginx
預期輸出範例:
# ❹ 的輸出 disable_functions => exec,passthru,shell_exec,system,proc_open,popen => exec,passthru,shell_exec,system,proc_open,popen # ❺ 的輸出 640 /var/www/example.com/public_html/wp-config.php # nginx -t 的輸出 nginx: configuration file /etc/nginx/nginx.conf test is successful
分支判斷邏輯:
- 🟢 disable_functions 已包含必要的高風險函式 → 不需重複設定,繼續下一步即可。
- 🟠 wp-config.php 的權限大於 644 → 建議收緊到 600 或 640。
- 🟡 Nginx 規則套用後前台功能異常 → 先確認是否誤擋了正常帶有 current_url 參數的合法請求,適度調整正則後再觀察一段時間的錯誤率。
🧊 冷知識:WordPress 的「鹽值」到底是什麼碗糕?
你可能沒注意過,但每個 wp-config.php 裡都藏著八組隨機字串,正式名稱叫「金鑰與鹽值(Keys and Salts)」,負責加密使用者的登入憑證。只要重新產生這八組字串,所有目前已登入的使用者(包括潛在的攻擊者)都會被強制登出。這個功能在懷疑網站已經被摸過的時候特別好用,就像發現有人偷了鑰匙,最快的處理方式不是換密碼,而是直接把整棟樓的鎖都換掉 🔑
📋 ㊅ 三環境速查表:把五個關卡濃縮成一張表
| 階段 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| ① 環境判斷 | wp core is-installed –network 判斷結束碼 | find /var/www -name wp-config.php 逐一列出 | 同左,結束碼為 0 即為 Multisite |
| ② 盤點版本 | wp plugin get forminator | 逐站以 stat -c ‘%U’ 取得擁有者後執行 | 查主站一次即代表全網 |
| ③ Log 排查 | 單一 access.log 掃描 | 逐 VirtualHost 掃描 | 共用 Log,依 URL/時間回推子站 |
| ④ 後門檢查 | mu-plugins+近期檔案+帳號 | 逐站各自檢查,不互相取代 | mu-plugins 全網共用,帳號分 Network/Site 兩層看 |
| ⑤ 加固檢查 | php.ini+檔案權限+Nginx 規則 | 逐站檢查,不假設一站正常全部正常 | php.ini 屬伺服器層級,通常一次到位 |
🤔 讀到這裡有點喘?幾個常見焦慮先幫你解掉
- 🏮 Q:更新之後,網站的表單或測驗功能會不會壞掉?
A:這次修的是外掛內部「怎麼判斷 current_url 這段字串該不該被當成指令」的邏輯,並沒有變更對外的表單渲染方式,所以功能異常的機率不高。更新完成後,建議隨手測試一份表單,確認送出流程還能正常運作就可以放心了 😮💨 - 🏮 Q:廠商說「我們的表單沒有收敏感資料,這功能不會有事」,該信嗎?
A:先別急著全信。這個漏洞真正需要的只是「站上還有其他可被串聯的短代碼」,跟你這支表單本身收不收敏感資料沒有直接關係——問題出在整台網站的外掛組合,而不是單一支表單。除了確認這支表單的內容,同步盤點一次站上其他外掛的短代碼功能,會比只問「表單有沒有敏感欄位」更保險 🔎 - 🏮 Q:我完全看不懂技術深潛篇那些指令,是不是就沒救了?
A:完全不會。前面「五分鐘無痛自救指南」那一段從頭到尾不需要碰任何指令,只要在後台點幾下滑鼠就能完成最基本的防護,技術深潛篇是給有 SSH 權限、想深入排查的人看的補充內容 🤗
🛠️ 第六章:修不了就先擋——短期應急與正式修補三部曲
前面解決的是「怎麼判斷」,這一章則進入真正的維運 Runbook。無論你屬於哪一種情境,強制流程都是同一套:盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證,每個階段都必須完成,才能進入下一階段 🧹
🚨 ㊀ A. 已中招/疑似中招:先止血再修補
決策原理:一旦第五章的排查出現明確跡象(mu-plugins 可疑檔案、日誌裡出現短代碼特徵且回應正常、資料庫留言或選項值異常),優先目標是「切斷觸發鏈路+保留證據+隔離後門」,而不是急著更新——更新只是後續步驟。最大的敵人不是漏洞本身,而是在沒有留下證據的情況下直接清理,反而把最有價值的線索一起刪掉了 🧯
WP_PATH="/var/www/example.com/public_html"
STAMP="$(date +%Y%m%d-%H%M%S)"
# ❶ 立即隔離:停用 Forminator,切斷觸發點
wp --path="$WP_PATH" plugin deactivate forminator
# ❷ 緊急取證:打包日誌與可疑目錄,先保留現場再動手清理
FORENSIC_DIR="$HOME/wp-incident-forensics-${STAMP}"
mkdir -p "$FORENSIC_DIR"
sudo cp -a -- "$WP_PATH/wp-content/mu-plugins" "$FORENSIC_DIR/" 2>/dev/null
sudo tar -czf "$FORENSIC_DIR/access_logs_${STAMP}.tar.gz" \
/var/log/nginx/*access*.log 2>/dev/null
wp --path="$WP_PATH" db export "$FORENSIC_DIR/db_snapshot_${STAMP}.sql"
printf '取證資料已存放於:%s\n' "$FORENSIC_DIR"
# ❸ 隔離可疑檔案(先移到隔離區,不要直接刪除,保留鑑識線索)
QUARANTINE_DIR="$HOME/wp-quarantine-${STAMP}"
mkdir -p "$QUARANTINE_DIR"
find "$WP_PATH/wp-content/mu-plugins" -type f -iname "*.php" -mtime -30 -print0 2>/dev/null |
while IFS= read -r -d '' SUSPECT; do
printf '隔離可疑檔案:%s\n' "$SUSPECT"
chmod 000 -- "$SUSPECT"
mv -- "$SUSPECT" "$QUARANTINE_DIR/"
done
# ❹ 強制刷新 Salt 值,讓所有現存的登入 Session(包括攻擊者的)立刻失效
sudo cp -- "$WP_PATH/wp-config.php" "$WP_PATH/wp-config.php.BAK.${STAMP}"
wp --path="$WP_PATH" config shuffle-salts
# ❺ 重設所有管理員的密碼
wp --path="$WP_PATH" user list --role=administrator --field=ID |
while IFS= read -r ADMIN_ID; do
wp --path="$WP_PATH" user update "$ADMIN_ID" --user_pass="$(openssl rand -base64 24)"
printf '已重設使用者 ID %s 的密碼\n' "$ADMIN_ID"
done
# ❻ 一併清除所有現存的登入 Session(跟刷新 Salt 雙重保險)
wp --path="$WP_PATH" eval 'wp_destroy_all_sessions(); echo "所有 Session 已清除\n";'
預期輸出範例:
# ❶ 的輸出 Plugin 'forminator' deactivated. Success: Deactivated 1 of 1 plugins. # ❹ 的輸出(官方標準訊息) Success: Shuffled the salt keys. # ❺ 的輸出 已重設使用者 ID 1 的密碼 # ❻ 的輸出 所有 Session 已清除
分支判斷邏輯:
- ⭕ 取證與隔離步驟全部成功 → 進入下方「C. 正式修補」流程;如果是企業、電商或會員制網站,同步通知內部資安或維運負責人,把 $FORENSIC_DIR 保留在受保護的位置。
- 🟠 隔離過程發現明確的後門檔案內容(例如包含 eval、base64_decode 的可疑程式碼) → 建議把取證資料提供給資安事件應變團隊做深入分析,並持續監控後續一週的流量與帳號活動。
- 🔴 網站牽涉會員個資或線上收款,且確認曾遭觸發 → 這已經不只是「更新外掛」的層級,建議同步啟動內部資安事件應變流程,留存稽核紀錄,以應付後續可能的通報責任。
❇️ 獨立多站:逐站執行上述 ❶~❻,每站完成後 sleep 3 緩衝,不可在單一迴圈裡一次停用所有站;✴️ Multisite:對 Network 主路徑執行 ❶~❻ 一次即可全網生效,取證範圍需一併包含 wp_sitemeta 與各子站的 wp_<blog_id>_options。
🧯 ㊁ B. 短期應急:還沒確認中招,但現在還不能立刻更新
🚨 短期應急 ≠ 正式修補
停用外掛、伺服器層攔截都屬於緩衝措施,目標是縮短攻擊面暴露的時間,真正的修補仍然是升級到 1.57.3 或更新版本 🧱
WP_PATH="/var/www/example.com/public_html"
# 選項一:如果暫時不需要用到表單/測驗功能,直接停用是最乾淨的做法
wp --path="$WP_PATH" plugin deactivate forminator
# 選項二:若業務依賴表單不能停用,先在伺服器層攔截帶有短代碼特徵的請求
sudo tee /etc/nginx/conf.d/forminator_block_current_url.conf > /dev/null <<'EOF'
if ($args ~* "current_url=.*(%5B|\[)") {
return 403;
}
EOF
sudo nginx -t && sudo systemctl reload nginx
預期輸出範例:
# 選項一的輸出 Success: Deactivated 1 of 1 plugins. # 選項二的輸出 nginx: configuration file /etc/nginx/nginx.conf test is successful nginx: configuration file /etc/nginx/nginx.conf test is successful
分支判斷邏輯:
- 🟢 可以安全停用外掛 → 保留停用狀態,盡快排入下方「C. 正式修補」的維護窗口。
- 🟠 網站核心流程依賴 Forminator 的表單或測驗功能(例如活動報名頁正在進行) → 不建議直接停用,先套用 Nginx 攔截規則降低風險,並儘速安排正式修補窗口。
㊂ C. 正式修補:盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證
🩵 荷包試算:這一整套流程要花多少錢?
從盤點到驗證,全程只要有 SSH 權限就能自己動手,本質上是免費的維運工時。真正會花到錢的地方,通常是「已經中招之後」——請資安顧問做鑑識、清除後門的工時,或是網站被列入安全黑名單之後重建流量的成本。這筆帳怎麼算,都是現在花半小時走完這套流程比較划算 💸
❶ 備份:遵循「3-2-1 備份原則」
決策原理:最佳實踐是至少保留 3 份副本、使用 2 種不同的儲存媒介,並將其中 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/site_pre_update_${STAMP}.sql"
tar -czf \
"$BACKUP_DIR/site_wp-content_pre_update_${STAMP}.tar.gz" \
-C "$WP_PATH" \
"wp-content"
ls -lh "$BACKUP_DIR"/*"${STAMP}"*
# ❇️ 獨立多站:逐站備份,檔名含站名避免互相覆蓋
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" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' CONFIG; do
WP_PATH="$(dirname -- "$CONFIG")"
SITE_NAME="$(basename -- "$WP_PATH")"
OWNER="$(stat -c '%U' -- "$CONFIG" 2>/dev/null || echo 'www-data')"
printf '=== 備份:%s ===\n' "$SITE_NAME"
sudo -u "$OWNER" -- wp --path="$WP_PATH" db export \
"$BACKUP_DIR/${SITE_NAME}_pre_update_${STAMP}.sql" 2>/dev/null
tar -czf "$BACKUP_DIR/${SITE_NAME}_wp-content_pre_update_${STAMP}.tar.gz" \
-C "$WP_PATH" "wp-content" 2>/dev/null
printf '完成:%s\n' "$SITE_NAME"
sleep 3
done
# ✴️ Multisite:db export 是整個網路共用資料庫,不是單一 Site
WP_PATH="/var/www/network.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/network_pre_update_${STAMP}.sql"
tar -czf \
"$BACKUP_DIR/network_wp-content_pre_update_${STAMP}.tar.gz" \
-C "$WP_PATH" \
"wp-content"
🚨 提醒:獨立多站的迴圈裡加了 sleep 3,這不是隨手加的裝飾——如果一次性對所有網站並行備份,磁碟 I/O 跟資料庫連線可能會被瞬間打爆,逐站加入緩衝雖然比較慢,但每一站都在可控狀態下完成,出事也好定位是哪一站的問題 🐢
❷ Dry-run:先看更新會做什麼,不真的動手
# 💠 單一網站 WP_PATH="/var/www/example.com/public_html" wp --path="$WP_PATH" plugin update forminator --dry-run
預期輸出範例:
Plugin forminator 1.57.1 will be updated to 1.57.3.
分支判斷邏輯:看到目標版本 → 可以進入正式更新;顯示沒有可用更新 → 回頭確認目前版本與更新來源,不要因為「No updates」就直接認定網站安全,有可能是外掛已經是最新版,也可能是官方套件庫連線異常 🔮
❸ 更新:確認備份與 Dry-run 都沒問題才真正修改
# 💠 單一網站:三部曲 deactivate → update → activate
WP_PATH="/var/www/example.com/public_html"
wp --path="$WP_PATH" plugin deactivate forminator
wp --path="$WP_PATH" plugin update forminator
wp --path="$WP_PATH" plugin activate forminator
wp --path="$WP_PATH" plugin get forminator --field=version
# ❇️ 獨立多站:逐站完成備份→更新→驗證,再進入下一站
SEARCH_ROOT="/var/www"
find "$SEARCH_ROOT" -type f -name "wp-config.php" -print0 |
while IFS= read -r -d '' CONFIG; do
WP_PATH="$(dirname -- "$CONFIG")"
OWNER="$(stat -c '%U' -- "$CONFIG" 2>/dev/null || echo 'www-data')"
printf '=== 處理網站:%s ===\n' "$WP_PATH"
sudo -u "$OWNER" -- wp --path="$WP_PATH" plugin deactivate forminator 2>/dev/null
sudo -u "$OWNER" -- wp --path="$WP_PATH" plugin update forminator 2>/dev/null
sudo -u "$OWNER" -- wp --path="$WP_PATH" plugin activate forminator 2>/dev/null
printf '完成:%s\n' "$WP_PATH"
sleep 3
done
# ✴️ Multisite:外掛檔案只需更新一次,Network 層級啟停用才需要 --network 參數
WP_PATH="/var/www/network.example.com/public_html"
wp --path="$WP_PATH" plugin deactivate forminator --network
wp --path="$WP_PATH" plugin update forminator
wp --path="$WP_PATH" plugin activate forminator --network
wp --path="$WP_PATH" plugin get forminator --field=version
🧑🔧 多站環境的實務原則:一次只處理一個網站,至少在第一批部署時如此。先完成一站的「備份 👉 更新 👉 驗證」,確認沒有 PHP Fatal Error、白畫面或表單提交異常,再繼續下一站,這比一個巨大迴圈從頭跑到尾更容易控制風險。另外要特別注意:–network 參數只用在 activate/deactivate,plugin update 本身不吃這個參數,因為 Multisite 的外掛檔案在檔案系統層級本來就是共用的 🎛️
❹ 驗證:更新成功 ≠ 工作完成,至少確認三層
WP_PATH="/var/www/example.com/public_html"
# ❶ 版本層:確認已經是修補後的版本
wp --path="$WP_PATH" plugin get forminator --field=version
# ❷ 檔案完整性層:跟官方發布版本比對 Checksum
wp --path="$WP_PATH" plugin verify-checksums forminator
wp --path="$WP_PATH" core verify-checksums
# ❸ 前後台功能層
curl -sS -o /dev/null -w "%{http_code}\n" "https://example.com/"
wp --path="$WP_PATH" plugin list --status=active --fields=name,status,version | grep forminator
預期輸出範例:
# ❶ 的輸出 1.57.3 # ❷ 的輸出 Success: Verified 1 of 1 plugins. # ❸ 的輸出 200
🚨 Checksum 驗證的侷限:它只能證明「外掛目錄裡的檔案內容跟官方發布版一致」,無法證明整個網站沒有後門——它檢查不到 mu-plugins 目錄、資料庫裡的異常內容,也檢查不到其他外掛或主題是否被動過手。因此 Checksum 驗證應該被當成完整性檢查的其中一環,而不是唯一的安全保證,建議搭配第五章的後門檢查一起做 🔐
🔥 一句話總結:真正安全的批次維運,是先盤點,再備份,再 Dry-run,確認無誤後才逐批更新,最後驗證。環境判斷錯誤比指令寫錯更致命——這套判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證的閉環,換成下一次別的外掛、別的 CVE,依然可以直接套用 🦸
🧩 舉一反三:這一整類漏洞的通用防禦知識庫
這次的 CVE-2026-92229 屬於典型的程式碼生成控制不當(CWE-94)類型,具體表現為「未經驗證的輸入流入 do_shortcode()」。以下是這一大類漏洞的通用防禦招式,不是本 CVE 的官方要求,而是給想舉一反三的 DevOps 參考:
- ⭐ 「導向用」跟「顯示用」的參數,從架構上就該分開處理。任何原本設計來做網址跳轉、提交後回跳的參數,應該只經過 esc_url_raw() 這類網址驗證,絕對不要讓它有機會流進會渲染內容或執行邏輯的函式。
- ⭐ 凡是會進入 do_shortcode() 的外部輸入,一律用白名單而非黑名單。開發或客製化外掛時,限制允許被觸發的短代碼名稱清單,而不是嘗試過濾掉「看起來危險」的字元——黑名單永遠會被找到繞過方式。
- ⭐ 定期盤點站上到底註冊了哪些短代碼、各自的權限範圍。每一個額外的外掛跟主題,都可能成為別人攻擊鏈的來源;用不到的功能就移除,能用最小權限原則收斂就收斂。
🏆 第七章:事件綜合評估——這次到底該多緊張?
| 評估維度 | 分數 | 一句話評語 |
|---|---|---|
| 漏洞嚴重程度(越高代表風險越大) | 9.1 | 不用登入、不用互動就能觸發,防禦難度極高,殺傷力則取決於站上還裝了什麼 |
| 官方應變速度 | 8 | 補丁比細節公開早了將近三週上線,走的是相當扎實的負責任揭露節奏 |
| 一般站長自救可行性 | 9 | 點更新按鈕就能解決,操作門檻不高,唯一需要多做一步的是盤點管理員帳號 |
| DevOps 技術文件完整度 | 7 | 攻擊鏈路與影響評估清楚,但 CVSS 完整向量與通報者身分仍有部分待官方進一步證實 |
| 🏆 加權綜合建議 | 立即行動 | 最終建議:【今天內完成更新,會員制/電商網站建議提前處理】 |
🔥 這起事件提醒我們一件事:漏洞不見得都躲在複雜的核心系統裡,有時候反而藏在一支「一直很正常在運作」的表單小工具背後。Forminator 這種級數的外掛,多數人裝上去用久了就不會再點開設定頁——但只要它還「啟用」著,就一直是整體攻擊面的一部分。與其等哪天真的湊巧被串成大事,不如現在就花五分鐘,打開後台看一眼版本號碼 🔢
📚 參考資料與延伸資源
- 📌 Wordfence Threat Intelligence|Forminator Forms ≤ 1.57.2 Unauthenticated Arbitrary Shortcode Execution [42]
- 📌 NVD|CVE-2026-92229 官方詳情頁 [43]
- 📌 CVE.org 官方紀錄|CVE-2026-92229 [44]
- 📌 OpenCVE|CVE-2026-92229 情資頁 [45]
- 📌 IONIX Threat Center|CVE-2026-92229 漏洞分析 [46]
- 📌 WP-CLI 官方指令文件|wp plugin update [47]
- 📌 WP-CLI 官方文件|wp config shuffle-salts [48]










