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

🚨客人填的預約表單,怎麼變成讀取資料庫的後門?📒 Amelia 高危漏洞自救全攻略(CVE-2026-112327/CVSS 9.3)

🗓️ 預約資料可能被人悄悄摸走?Amelia 預約外掛漏洞自救全攻略(CVE-2026-112327/CVSS 9.3)

🔥 懶人包:
你有在幫客人做線上預約嗎?不管是美髮沙龍、牙醫診所、還是塔羅牌占卜都在用的 WordPress 預約外掛 Amelia(官方顯示活躍安裝數 90,000+),最近被揭露了一個「不用登入就能發動」的 SQL 注入漏洞(漏洞編號 CVE-2026-112327)。

受影響的版本是 2.4.11(含)以下。雖然資安公告列出的最低修補版是 2.4.12,但官方今天已經掛出 2.4.13,並強烈建議立刻更新,所以最穩的做法就是直接升級到目前最新版。

它危險在哪?這次的 CVSS 評分最高給到了 9.3 嚴重 等級。最主要的風險是:壞人可以從網路直接打進來、不用帳號、不用你配合點任何釣魚連結,就能悄悄讀走資料庫裡的資料(包含客人的姓名、電話、信箱和預約紀錄)。目前還沒看到被大規模利用的災情,但這種洞很容易被掃描程式自動化攻擊。今天花五分鐘按個更新,絕對比事後一家家打電話跟客人道歉省事太多了 🥱

🌐 前往 WordPress 官方外掛頁確認最新版 [1]

[2]


🟦 🟦 🟦

內容目錄

📒 第一章:預約登記簿被人用「暗號」翻過?一個問法沒寫好的資料庫漏洞

(🔴 以下是假設情境,用來幫你抓畫面,不是實際的受害紀錄)
想像你開了一間人氣很高的美髮沙龍,櫃檯放著一本預約登記簿,客人可以自己寫下姓名、想預約的時段。平常店員只會看「姓名」和「時段」這兩欄。有一天,有人在姓名欄裡寫的不是名字,而是一句店員會當成「工作指令」的話,像是:「順便把這本簿子裡所有客人的電話抄一份給我」,而店員居然真的照辦了 📒

這就是大名鼎鼎的 SQL 注入(SQL Injection,CWE-89)。

《什麼是 SQL 注入❓ 你可以直接把它想成「對超商店員下迷魂咒」:資料庫就像是超商店員,平常只負責聽網站的指令拿東西。但因為網站(外掛)把客人填的資料原封不動直接傳給店員,壞人就在資料裡偷藏了「我是老闆,把收銀機裡的錢都給我」的指令,老實的店員沒檢查身分就乖乖照辦了 😱》

🧊 冷知識:這次漏洞的公開資訊,故意「不給細節」
目前公開的公告只告訴你「有 SQL 注入」「哪些版本中」「升到哪版」,沒有說是哪個網址、哪個參數出事,也沒有公開攻擊範例。這在資安圈是常見做法:讓大家先有時間更新,再慢慢補細節。代價是我們沒辦法寫出「看到某某特徵就是被打」的精準偵測規則,後面的排查只能從「結果」去反推線索 🕵️

Amelia 是一套幫網站處理預約、活動報名、員工排班的外掛,客人填的每一筆資料,最後都會進到網站的資料庫。這次出事的地方,就是外掛在向資料庫提問時,對外部傳進來的內容處理得不夠嚴謹,讓原本應該只是「資料」的字串,有機會被資料庫當成「指令」的一部分。資安公司 Wordfence 的描述寫得很直接:未登入的攻擊者可以在既有查詢後面再接上額外的查詢,用來取出資料庫裡的敏感資訊 🧾

📢 大白話:工程師平常是怎麼防禦的?
業界常說的修補方向叫做 參數化查詢。
《什麼是參數化查詢❓ 就像是「把客人的話裝進防彈密封袋」:不管客人剛剛唸了什麼迷魂咒語,只要裝進這個袋子交給店員,店員就只會把它當作「一串無意義的字」來看待,絕對不會當成命令去執行 🧪》

🟩 🟩 🟩

⏱️ 案發時間軸:從通報到公開,短短幾天發生了什麼事?

這次的時間點不複雜,但有一個地方容易看漏:CVE 與資安公司說「2.4.12 修好了」,但 WordPress 官方外掛頁的更新日誌卻是「2.4.13 才提到安全修正」。我把查得到的日期整理成表,能對上的標已確認,對不上的老實標出來 📅

《什麼是 CVE❓ 就像是資安界的「全球統一報案編號」:全世界的駭客和資安專家每天都在找漏洞,為了避免大家雞同鴨講,只要確認是個新漏洞,就會發一張像 CVE-2026-112327 這樣的號碼牌,方便大家追蹤處理進度 📝》

日期/版本 事件節點 可信度與備註
2026-10-18 Patchstack 頁面記載的通報時間 ✅ 多份報告引用一致;我沒有直接讀到該頁原文
2026-10-20 Wordfence 顯示「公開發布日」 ✅ Wordfence 頁面截圖可查,Patchstack 發布日同為此日
2026-10-22 NVD 頁面顯示發布日與修改日 ✅ 國家弱點資料庫(NVD)尚待補充分析評分
2.4.12 各方資安平台一致列為修補版 ✅ 三方一致;但官方對這一版的更新日誌只寫了 Outlook 修正,沒有安全字樣
2.4.13 官方更新日誌寫明含重要安全修正 ✅ 官方頁面可查;並建議立刻更新

那 2.4.12 到底有沒有修好?外掛廠商常常為了避免引起恐慌低調修補、日誌寫得很簡略。2.4.13 是否另外補了其他洞?目前沒有足夠資訊判斷【❓待確認】。所以我的建議很單純:不要停在 2.4.12,直接更新到官方目前提供的最新版。

🩵 小資族荷包試算:這次補洞要花錢嗎?
WordPress 官方目錄裡的 Amelia 免費版,更新是 NT$0,後台按一下就好。有些資安服務商會說「買我們的防火牆就不怕」,但那需要你本來就有訂閱。真正貴的是出事之後:請人鑑識、通知客人、處理客訴,這些都比花五分鐘更新貴太多了 💸

[38]


🟧 🟧 🟧

⚠️ 說大白話:最壞會怎樣?把「鐵證」、「推論」、「假設」分開看

🧊 冷知識:為什麼網站被讀走資料,日誌卻像「歲月靜好」?
伺服器的存取日誌像門口警衛的簽到本,只記「誰在幾點去了哪個網址」,不記「他在表單裡寫了什麼」。所以日誌裡沒看到可疑字眼,不能當成安全的唯一證明 🕵️

資安新聞最怕自己嚇自己。這次評分有兩種版本,但都把 機密性 (Confidentiality) 評為高,完整性 (Integrity) 評為無。

《什麼是機密性與完整性❓ 想像你的網站是一間「高級保險箱出租中心」:機密性破功(高風險)代表小偷拿著手電筒,把你保險箱裡放了多少錢「全部看光光」👀;而完整性沒事(無風險)代表小偷「只能看,不能碰」,他沒辦法把你的錢偷走或把保險箱砸爛 🙅‍♂️》

翻成白話,官方認定的主要風險是資料被讀走,不是被改掉或刪掉 👇

  • ✅ 已確認事實:漏洞類型為 SQL 注入;受影響版本 2.4.11 以下;從網路就能發動、不需要帳號;機密性影響高、完整性影響無。
  • 🟡 合理推論:預約外掛通常放著客人的姓名、聯絡方式甚至付款狀態,所以資料外洩的實際代價,往往比一般外掛嚴重。但實際能讀到哪些資料,還要看漏洞確切位置與資料庫設定【❓待確認】。
  • 🔴 假設情境:有人用掃描工具大量嘗試 👉 讀走客人名單 👉 拿去發詐騙簡訊或釣魚信。這是常見的「資料外洩後劇本」,不代表單靠這個洞就會自動走完全程。

🚥 畫重點:別把「完整性:無」讀成「只會被讀、不用擔心被改」!
這個「無」是評分單位對「這個漏洞本身」的判斷,不是保證。如果你發現網站資料被改、出現陌生管理員,那比較可能是另一條入侵路線,不要直接怪到這個漏洞頭上。


🟪 🟪 🟪

🎭 第二章:先別急著恐慌,看看你是哪一種「預約網站頭家」?

警報一響,不同身分要做的事差很多。先對號入座,再決定你接下來要做什麼 🧭

你的情境 要不要緊張? 現在最該做的事
🙋 第一種:店家老闆、小編(單一網站) 中等,更新就能解決 跳到第三章:確認版本 👉 備份 👉 更新 👉 測一次預約(全程免指令)
👨‍💻 第二種:自架站玩家、接案工程師(管多個站) 中偏高,站越多越容易漏 用腳本盤點整台主機,再逐站修補(詳見完整版後續章節)
🏢 第三種:診所、補習班、企業(握有大量客戶資料) 高度警戒,牽涉個資責任 先更新、保留日誌,並由法務評估是否須通報客戶
🚨 第四種:外掛裝了放著、預約表單還開著 最高,隨時可被掃到 不更新就停用外掛,不要兩邊都不做

🙋 第一種:我只是顧後台、回客人訊息的老闆或小編

你不需要懂程式碼。要做的事只有:確認版本、備份、更新、測一次預約流程。第三章把每一步「滑鼠點哪裡」都拆開了。更新是免費的,大膽按下去就對了 ✅

👨‍💻 第二種:我是包山包海的自架站玩家(多站點)

如果你用面板管著十幾個客戶的網站,需要的是「一次掃完整台主機」的火力。更新完記得清一次網站快取與 CDN 快取,不然前台常常還卡在舊畫面 🔧

🧑‍🔧 DevOps 小知識:資料庫是共用的,傷害就是整包的
SQL 注入打的是資料庫。如果你把好幾個客戶的網站塞在同一個資料庫帳號底下,一個站出事,其他站的資料就有可能被連帶讀到。養成「一個站一個資料庫、獨立帳號」的習慣,是成本很低的保險 🧱

🏢 第三種:我們是診所、補習班或企業,握有一大把客戶資料

預約資料就是個人資料。如果日後確認有資料外洩,台灣《個人資料保護法》的責任歸屬與通報義務需要由專業法務判定【❓待確認】。你現在能做的,就是盡快更新、保留日誌證據、評估資料量 🩺

🚨 第四種:地雷區!「活動早就結束了,放著沒差吧?」

🚨 高風險情境模擬:
(🔴 假設情境)某間工作室兩年前用 Amelia 辦過報名活動,活動結束後網頁撤了,但外掛一直沒停用。老闆以為「沒在用就沒事」。
🙅 這就像你家兩年前裝了一扇後門,平常不走,但鎖壞了你也不知道。只要外掛還亮著「已啟用」,它對外開放的入口就還在,掃描程式照樣能敲門 🆘

🚥 畫重點:更新與停用,今天至少選一個做!如果因為相容性不敢更新,退而求其次就是立刻停用。「沒在用」不代表沒風險,「已停用」才是真正拉下鐵門 🙅‍♂️

[39]


🟩 🟩 🟩

🛟 第三章:電腦小白專屬!五分鐘無痛自救法(全程滑鼠,免敲指令)

不管你是哪一種網站主,都建議照這幾步走一次。每一步都有手把手的防呆說明 🤏

🫂 新手求助:我連後台都進不去、也看不懂版本號,怎麼辦?
直接把這段話複製給你的主機商或接案工程師:「我的網站可能裝了 Amelia 預約外掛(slug 是 ameliabooking),有漏洞編號 CVE-2026-112327,請幫我確認版本是不是在 2.4.13 以上,並先備份再更新。」內行人一看就懂。

《什麼是 slug❓ 就像是外掛的「身分證字號」:很多外掛名字可能取得很像,但 slug 絕對是獨一無二的網址代號,認明它就不會找錯人 🔍》

  • ㊀ 先確認你有沒有裝:
    登入 WordPress 後台,點左邊選單「外掛」→「已安裝的外掛」,右上角搜尋框輸入 Amelia。找到名稱含 Amelia 的外掛就是中獎;沒找到,這篇你可以放心關掉了 🔎
  • ㊁ 檢查版本號,判斷吉凶:
    看外掛名稱下方的數字。2.4.11(含)以下代表在受影響範圍,請馬上往下做;2.4.12代表符合公告列的最低修補版,但仍建議更新到最新;2.4.13 或更新代表版本面上已經沒問題。如果你用的是官網買的付費版,請以官網公告對照【❓待確認】🕵️‍♀️
  • ㊂ 更新前先買個保險(備份):
    到主機控制台按「一鍵備份」,或使用你平常的備份外掛。預約網站要特別確認備份有包含「資料庫」,因為預約紀錄都在裡面 💾
  • ㊃ 按下「立即更新」:
    在外掛列表找到 Amelia,點「立即更新」,轉圈圈轉完後重新整理頁面,確認版本變成最新。萬一畫面變全白,不要狂按重新整理,先把外掛選「停用」,再找工程師 🔄
  • ㊄ 自己當一次客人,測試預約流程:
    打開前台的預約頁面,用自己的信箱完整走一次預約,確認能送出、後台看得到、通知信收得到 🧾
  • ㊅ 看一眼管理員名單:
    點左邊「使用者」,篩選「管理員」。正常:都是你認識的老闆、同事。可疑:出現亂碼名字或你沒印象的帳號。先截圖存證、請所有管理員換密碼 🔀
  • ㊆ 想一下「客人資料」有沒有可能被帶走:
    如果你的版本在更新前曾經是 2.4.11 以下、而且網站對外公開,就把這段期間視為「資料有暴露風險」。沒有任何異常跡象時,更新與換密碼就是最低限度的處理 🔍
  • ㊇ 把自動更新打開:
    在外掛列表右邊點「啟用自動更新」。以後這類安全修補,會自己在半夜裝好 ⏰

💬 溫馨提醒:更新只是「現在換一副新鎖」,不能證明「過去有沒有人用萬能鑰匙進來過」。如果你對網站有疑慮,請找專業人員做進一步排查 🩺

🧊 冷知識:「停用」跟「刪除」差在哪?
「停用」像是把店家鐵門拉下來暫停營業,裝潢(設定與資料)都還在,隨時能重新開張,也能擋住這次的漏洞入口;「刪除」是整間店拆掉。如果你還想保留舊預約資料,千萬不要用刪除來解決問題 🧰


🔴 🔴 🔴

🤿 第四章:同一個漏洞,為什麼有人給 9.3、有人給 7.5?

以下給想知道「為什麼會這樣」的人看。只想補洞的話,前三章已經夠用;想看懂這份公告的分數怎麼來,繼續往下拆 ⚔️

評估指標 具體資訊
外掛名稱 Booking for Appointments and Events Calendar – Amelia(開發者:TMS)
外掛 slug ameliabooking
CVE 編號 CVE-2026-112327(CNA:Patchstack)
漏洞類型 未授權 SQL 注入(CWE-89)
CVSS 3.1(Patchstack/CVE.org/NVD 顯示的 CNA 分數) 9.3 Critical CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:L
CVSS 3.1(Wordfence) 7.5 High CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
受影響版本 ≤ 2.4.11
公告列出的修補版 2.4.12(WordPress.org 目前列出 2.4.13)
研究員 SVO(Wordfence 頁面欄位)
官方外掛頁資料 活躍安裝 90,000+、需 WordPress 6.5+、PHP 7.4+
公開的技術細節 沒有確切端點、參數名稱或修補程式碼差異【❓待確認】

🧮 兩份分數,差在哪兩個字母?

把兩串向量並排看,會發現前面六個指標完全一樣:網路可達(AV:N)、複雜度低(AC:L)、不用權限(PR:N)、不用互動(UI:N)、機密性高(C:H)、完整性無(I:N)。分歧只有兩處 🔬

  • 🏮 Scope(影響範圍):Patchstack 評為 S:C(Changed),Wordfence 評為 S:U(Unchanged)。前者認為影響會跨出外掛本身的安全邊界,後者認為不會。這一格在評分公式裡權重很大,也是 9.3 與 7.5 的主要來源。
  • 🏮 Availability(可用性):Patchstack 評為 A:L(低),Wordfence 評為 A:N(無)。
📢 大白話:「Scope: Changed」是什麼意思?
把外掛想成一間店面。Scope 沒改變,代表壞人最多只能動到這間店裡的東西;Scope 改變,代表壞人從這間店出發,影響到了隔壁的大樓,也就是別的安全邊界。這裡的「大樓」指什麼,公告沒有寫清楚,所以也不要自己腦補成「整台伺服器被接管」,公開資料並沒有這樣說 🏢

兩家為什麼這樣評,公開頁面沒有解釋【❓待確認】。🟡 合理推論:不同機構對「資料庫」算不算獨立的安全範圍,本來就有不同的判斷習慣,所以同一個漏洞出現兩個分數很常見。對你來說,要記得的是:不用等「哪個分數才對」,兩個分數都已經落在需要盡快修補的等級。

🧑‍🔧 DevOps 小知識:「C:H, I:N」不等於「資料絕對不會被改」
評分是針對「這個弱點本身可造成的後果」。實務上 SQL 注入能做到什麼,常常跟資料庫帳號權限、資料庫版本、查詢出現的位置有關。公開資料沒有給細節,所以把它寫成「已確認可寫入/刪除」是不負責任的,把它寫成「保證只能讀」也一樣不嚴謹。比較誠實的說法是:主要風險是資料外洩,其他後果沒有證據,但不能完全排除 🎚️

🔧 2.4.12 與 2.4.13,到底該信哪一個?

WordPress.org 的更新日誌對這兩版是這樣寫的:2.4.12 是「幾項錯誤修正與穩定性改善」,內容是 Outlook 日曆整合的修正;2.4.13 才出現「重要的安全修正,強烈建議立刻更新」的字樣。而 CVE、Patchstack、Wordfence 三個來源一致把 2.4.12 列為修補版 🧾

  • ✅ 已確認:三個漏洞資料來源都把 2.4.12 列為修補版;官方更新日誌對 2.4.13 明確提到安全修正。
  • 🟡 合理推論:外掛廠商常把安全修正寫成簡短的「錯誤修正」,所以 2.4.12 的日誌沒有安全字樣,不代表沒修;2.4.13 可能是補強、或同時處理了其他問題。
  • 【❓待確認】無法確認:2.4.13 修了哪些洞、跟這個 CVE 的關係為何,公開資料沒有說。

在資訊不足的情況下,最保守又最簡單的做法,就是不要賭 2.4.12,直接裝官方當下的最新版。這也是本文所有指令都把 FIXED 當成「最低門檻」、實際更新交給 wp plugin update 的原因 🧱

[40]


🔷 🔷 🔷

🕵️ 第五章:怎麼知道有沒有人先進來看過?SSH 排查 Checklist

🤖 重要提醒:以下指令由五份來源報告(ChatGPT、DeepSeek、Gemini、Grok、Meta AI)彙整,經 Claude 複查修正與整理,並依 WP-CLI 官方文件與 HestiaCP 的目錄慣例逐項核對,所有程式碼區塊都做過 bash 語法檢查 🫡
但你的主機環境不一定跟範例一樣。正式環境(尤其是營業中的網站)的高風險操作——包含更新、停用、更換金鑰、重設密碼、刪除檔案或執行批次腳本——請務必由熟悉該站架構的專業人員做最後確認與執行,切勿直接複製貼上後就按下 Enter。範例中的 /home/user1/web/example.com/public_html 與 example.com 都是示範用名稱,請換成你自己的環境 🧙

🚨 畫重點:這次我刻意「不寫」的排查指令
有幾份來源報告提供了看起來很專業的偵測規則,例如在 Nginx 用網址參數擋 SQL 關鍵字、或指名某個特定的 AJAX 動作名稱。但這次公開資料沒有說明漏洞的確切端點與參數,而且 POST 內容在 Nginx 規則裡根本看不到,這類規則要不是擋不到東西,就是會誤傷正常的預約請求。所以本章的排查從「結果」下手:資料量、帳號、檔案、日誌的弱訊號 🧭

這一章分成五個關卡:環境判斷 👉 盤點版本 👉 資料量與帳號 👉 檔案與日誌 👉 加固,每一關都拆成「決策原理、指令、預期輸出、分支判斷」。預設環境是 Debian 12 + HestiaCP,以 root 身分操作。AlmaLinux 的差異是:遇到 Permission denied 時,請先用 getenforce 看 SELinux 有沒有擋;版本比較用的 dpkg 在 AlmaLinux 沒有,可改用 sort -V 💻

🧰 先貼一次工具箱

決策原理:HestiaCP 每個網站都屬於不同的 Linux 使用者,檔案放在 /home/使用者/web/網域/public_html。用 root 直接跑 WP-CLI 會產生 root 擁有的檔案,網站自己之後寫入就會失敗,所以先定義四個小函式:找出所有網站、用檔案擁有者的身分執行 WP-CLI、取出網域名稱、逐站執行並在失敗時停下。Debian 最小安裝可能沒有 sudo,這裡改用內建的 runuser。所有路徑變數都用雙引號包起來,路徑含空白也不會出錯 🧱

# ㊀ 先貼這一段(同一個 SSH 工作階段只要貼一次,後面所有步驟都會用到)
PLUGIN="ameliabooking"
FIXED="2.4.12"   # CVE 與 Patchstack 列出的最低修補版;實際更新時一律以官方最新版為準

# 找出本機所有 WordPress(HestiaCP 預設放在 /home/使用者/web/網域/public_html)
list_wp_sites() {
  find /home -maxdepth 7 -type f -name "wp-config.php" \
    -path "*/web/*/public_html/*" -not -path "*/wp-content/*" -print0 2>/dev/null
}

# 以「網站檔案擁有者」的身分執行 WP-CLI,避免產生屬於 root 的檔案
# 最後的 < /dev/null 是刻意的:避免 wp 把迴圈正在讀的清單吃掉
wpx() {
  local wp_path="$1" owner home_dir
  shift
  owner="$(stat -c '%U' -- "$wp_path/wp-config.php")" || return 1
  home_dir="$(getent passwd "$owner" | cut -d: -f6)"
  runuser -u "$owner" -- env HOME="${home_dir:-/tmp}" wp --path="$wp_path" "$@" < /dev/null
}

# 從路徑取出 HestiaCP 的網域名稱(網站裝在子目錄也算得對)
domain_of() {
  local rest="${1#*/web/}"
  printf '%s\n' "${rest%%/*}"
}

# 逐站執行:任何一站失敗就停下,避免連鎖出事
each_site() {
  local callback="$1" cfg
  while IFS= read -r -d '' -u 3 cfg; do
    "$callback" "$(dirname -- "$cfg")" || {
      printf '⛔ 在 %s 停下,請先處理這一站\n' "$(dirname -- "$cfg")"
      return 1
    }
  done 3< <(list_wp_sites)
}

# 防呆:三個條件缺一個,後面的指令都會失敗
command -v wp >/dev/null      || echo "⚠️ 找不到 wp 指令,請先安裝 WP-CLI"
command -v runuser >/dev/null || echo "⚠️ 找不到 runuser,請確認你是在 Debian/AlmaLinux 的 root 身分下操作"
[ "$(id -u)" -eq 0 ]          || echo "⚠️ 目前不是 root,請先用 su - 或 sudo -i 切換"

預期輸出:貼完沒有任何輸出就是正常的;看到 ⚠️ 代表缺了 WP-CLI、runuser 或不是 root,先補上再往下。

  • 🟢 沒有警告 👉 往下進入環境判斷。
  • 🟠 找不到 wp 👉 先安裝 WP-CLI,用 wp –info 確認,不要硬跑後面的指令。
  • 🟡 command not found 出現在 runuser 👉 通常是 PATH 不完整,改用 su – 取得完整 root 環境再貼一次。

🧭 🧭 🧭

🧭 ㊀ 環境判斷:你在管一個 WordPress,還是一整台?

決策原理:單一站、獨立多站、Multisite 三者的處理範圍完全不同。用官方的 core is-installed –network 判斷 Multisite,比去 grep 設定檔可靠,因為註解裡的字樣也會被 grep 抓到 🤔

detect_one() {
  local wp_path="$1" kind
  if wpx "$wp_path" core is-installed --network 2>/dev/null; then
    kind="✴️ Multisite"
  elif wpx "$wp_path" core is-installed 2>/dev/null; then
    kind="💠 單一安裝"
  else
    kind="❓ 載入失敗"
  fi
  printf '%s\t%s\n' "$kind" "$wp_path"
  return 0
}

# 把整台主機掃一遍
each_site detect_one

預期輸出範例:

💠 單一安裝	/home/user1/web/example.com/public_html
✴️ Multisite	/home/user2/web/network.example.com/public_html
  • ⭕ 只有一行 💠 👉 💠 單一網站。
  • ⭕ 多行 💠、路徑各自不同 👉 ❇️ 獨立多站,之後每站各自處理。
  • ⭕ 出現 ✴️ 👉 ✴️ Multisite,外掛檔案是全網共用的。
  • 🟠 出現 ❓ 載入失敗 👉 先排查 PHP 或資料庫連線,不要直接對這站動手。
  • 🟡 路徑看起來像備份或測試副本(例如 public_html.bak) 👉 人工確認哪個才是正式環境。

[41]


🔎 🔎 🔎

🔎 ㊁ 盤點版本:哪些站裝了它、現在哪一版?

決策原理:版本比對用 Debian 內建的 dpkg –compare-versions,不要用字串比大小,否則 2.4.9 會被誤判成比 2.4.12 大。判斷條件是「低於 2.4.12 就是受影響」,這是公告的最低門檻,達標不代表你已經是最新版,所以腳本把名稱剛好是 ameliabooking 的才自動判斷,名稱含 amelia 但不一樣的(例如其他變體或付費版本)一律提醒人工比對,因為不同發行管道的版本號體系可能不同,避免「數字看起來比較大」的誤判 🔢

inventory_one() {
  local wp_path="$1" name status ver risk found=0
  if ! wpx "$wp_path" core is-installed 2>/dev/null; then
    printf '❓ WordPress 載入失敗,請先排查\t-\t-\t-\t%s\n' "$wp_path"
    return 0
  fi
  # 名稱含 amelia 的外掛全部列出來(官方版、其他變體都抓得到)
  while IFS=, read -r name status ver; do
    found=1
    if [ "$name" = "$PLUGIN" ]; then
      if dpkg --compare-versions "$ver" lt "$FIXED"; then
        risk="🔴 受影響"
      else
        risk="🟢 已達最低修補版"
      fi
    else
      risk="❓ 名稱不同,請人工比對"
    fi
    printf '%s\t%s\t%s\t%s\t%s\n' "$risk" "$name" "$ver" "$status" "$wp_path"
  done < <(wpx "$wp_path" plugin list --fields=name,status,version --format=csv | grep -i 'amelia')
  [ "$found" -eq 1 ] || printf '⚪ 未安裝\t-\t-\t-\t%s\n' "$wp_path"
  return 0
}

# 💠 單一網站(路徑換成你的)
inventory_one "/home/user1/web/example.com/public_html"

# ❇️ 獨立多站:整台主機一次盤點
each_site inventory_one

# ✴️ Multisite:外掛檔案全網共用,查主站一次即可(status 顯示 active-network 代表全網啟用)
inventory_one "/home/user2/web/network.example.com/public_html"

預期輸出範例:

🔴 受影響	ameliabooking	2.4.11	active	/home/user1/web/example.com/public_html
🟢 已達最低修補版	ameliabooking	2.4.13	active	/home/user2/web/network.example.com/public_html
⚪ 未安裝	-	-	-	/home/user3/web/blog.example.net/public_html
  • 🔴 受影響 👉 排入第六章的備份、預覽、更新流程;公開已多日的站,同時做 ㊂、㊃。
  • 🟢 已達最低修補版 👉 版本條件已解除,但如果不是最新版,仍建議更新到最新(第六章 C)。
  • ❓ 名稱不同 👉 把外掛名稱與版本拿去官方網站公告對照,不要用腳本猜。
  • 🟡 status 顯示 inactive 👉 外掛已停用,攻擊面暫時不在,但仍建議更新或刪除。

🟥 🟥 🟥

👤 ㊂ 資料量與帳號:先算「最壞情況有多少筆資料在風險裡」

決策原理:這個漏洞的主要風險是「讀」,所以排查的第一步不是找帳號,而是知道「資料庫裡有多少預約資料」,這決定你要不要啟動通知流程。第一段用 information_schema 列出名稱含 amelia 的資料表與筆數估計(InnoDB 的筆數是估計值,不是精確數字)。接著補看管理員名單、網站層級設定(通知信箱、是否開放註冊、新註冊者預設角色)、最近註冊的帳號,以及每位管理員目前的登入階段。資料表前綴(wp_)每站可能不同,不能寫死:先用 wp db prefix 取得,再把 SQL 用單引號包住的 heredoc 寫進 mktemp 的暫存檔,最後用 sed 把前綴填進去,Shell 就不會誤解 SQL 裡的特殊字元。暫存檔用完立刻刪掉 🕵️‍♀️

audit_data_one() {
  local wp_path="$1" prefix qf uid
  printf '\n===== %s =====\n' "$wp_path"

  echo "--- ❶ Amelia 資料表與資料量(估計值,用來判斷「可能被讀走多少」)---"
  wpx "$wp_path" db query "SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE '%amelia%' ORDER BY table_name;"

  echo "--- ❷ 管理員名單 ---"
  wpx "$wp_path" user list --role=administrator \
    --fields=ID,user_login,user_email,user_registered --format=table

  echo "--- ❸ 網站層級設定(通知信箱、是否開放註冊、新註冊者預設角色)---"
  printf 'admin_email=%s\n' "$(wpx "$wp_path" option get admin_email)"
  printf 'users_can_register=%s\n' "$(wpx "$wp_path" option get users_can_register)"
  printf 'default_role=%s\n' "$(wpx "$wp_path" option get default_role)"

  echo "--- ❹ 最近註冊的 10 個帳號(任何角色)---"
  prefix="$(wpx "$wp_path" db prefix)" || return 0
  qf="$(mktemp)" || return 1
  cat > "$qf" <<'SQL'
SELECT ID, user_login, user_email, user_registered
FROM __PREFIX__users
ORDER BY user_registered DESC
LIMIT 10;
SQL
  sed -i "s|__PREFIX__|${prefix}|g" "$qf"
  wpx "$wp_path" db query "$(cat "$qf")"
  rm -f -- "$qf"

  echo "--- ❺ 每位管理員目前的登入階段(看 IP 與瀏覽器是否陌生)---"
  while IFS= read -r uid; do
    printf '管理員 ID=%s\n' "$uid"
    wpx "$wp_path" user session list "$uid" 2>&1 | head -n 20
  done < <(wpx "$wp_path" user list --role=administrator --field=ID)
  return 0
}

# 💠 單一網站
audit_data_one "/home/user1/web/example.com/public_html"

# ❇️ 獨立多站
each_site audit_data_one

# ✴️ Multisite:除了管理員,另外看一眼「超級管理員」
audit_data_one "/home/user2/web/network.example.com/public_html"
wpx "/home/user2/web/network.example.com/public_html" super-admin list

預期輸出範例(資料表名稱僅為示範,實際名稱以你的輸出為準【❓待確認】):

===== /home/user1/web/example.com/public_html =====
--- ❶ Amelia 資料表與資料量(估計值,用來判斷「可能被讀走多少」)---
+---------------------------------+------------+
| table_name                      | table_rows |
+---------------------------------+------------+
| wp_amelia_appointments          |       1840 |
| wp_amelia_customer_bookings     |       2013 |
| wp_amelia_payments              |       1522 |
| wp_amelia_users                 |        486 |
+---------------------------------+------------+
--- ❷ 管理員名單 ---
+----+------------+-------------------+---------------------+
| ID | user_login | user_email        | user_registered     |
+----+------------+-------------------+---------------------+
|  1 | admin      | admin@example.com | 2024-01-15 08:00:00 |
+----+------------+-------------------+---------------------+
--- ❸ 網站層級設定(通知信箱、是否開放註冊、新註冊者預設角色)---
admin_email=admin@example.com
users_can_register=0
default_role=subscriber
  • 🟠 ❶ 的筆數很大(例如上千筆客戶紀錄) 👉 把這個數字記下來,後面第七章評估通知範圍會用到。
  • 🔴 管理員名單出現註冊時間在漏洞公開前後、你不認識的帳號 👉 高度可疑,進入第六章「A」。
  • 🔴 users_can_register 是 1 且 default_role 是 administrator 👉 嚴重異常,進入「A」並立即修正。
  • 🟡 登入階段出現陌生 IP 或奇怪的瀏覽器資訊 👉 弱訊號,需搭配其他線索,輸出欄位格式依 WP-CLI 版本略有差異【❓待確認】。
  • 🟢 一切正常 👉 這一輪沒發現異常。「陌生」不等於「惡意」,也可能是外包或維運商的帳號,先問過再判斷。

[42]


🟣 🟣 🟣

🔍 ㊃ 檔案與日誌:有沒有後續的連鎖反應?

決策原理:官方評分把完整性影響列為「無」,所以這一步的目的是「排除」而不是「找證據」:核心與外掛檔案的官方檢查碼、mu-plugins(不用在後台啟用就會自動載入)、uploads 裡不該出現的 PHP、近 14 天被改過的 PHP、外掛清單,以及日誌裡誰最常敲 admin-ajax.php 和 wp-json。最後兩項只能當弱訊號:Nginx 預設日誌不會記錄 POST 的內容,所以我也不寫「用 grep 搜尋攻擊字串」這類看起來很專業、其實抓不到東西的規則。唯一例外是 ❼:如果攻擊者(或掃描器)把 SQL 字眼放在網址列用 GET 傳送,日誌是看得到的,所以腳本會撈一次,但命中不等於被入侵,沒命中也不等於安全。網站掛在 Cloudflare 後面的話,日誌裡的 IP 可能是 Cloudflare 的,需要改看真實 IP 欄位 🎯

file_scan_one() {
  local wp_path="$1" domain
  local -a logs
  domain="$(domain_of "$wp_path")"
  printf '\n===== %s(%s)=====\n' "$wp_path" "$domain"

  echo "--- ❶ 核心與外掛檔案是否跟官方版本一致 ---"
  wpx "$wp_path" core verify-checksums 2>&1 | tail -n 10
  wpx "$wp_path" plugin verify-checksums --all 2>&1 | tail -n 20

  echo "--- ❷ mu-plugins(會自動載入、後台外掛頁看不到)---"
  find "$wp_path/wp-content/mu-plugins" -maxdepth 2 -type f -name '*.php' -ls 2>/dev/null

  echo "--- ❸ uploads 底下的 PHP 類檔案(正常應為空)---"
  find "$wp_path/wp-content/uploads" -type f \
    \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' -o -iname '*.php[0-9]' \) \
    -printf '%TY-%Tm-%Td %TH:%TM %u:%g %m %p\n' 2>/dev/null

  echo "--- ❹ 近 14 天被改過的 PHP(排除快取,最多 50 筆)---"
  find "$wp_path" -type f -name '*.php' -mtime -14 \
    -not -path '*/wp-content/cache/*' -ls 2>/dev/null | head -n 50

  echo "--- ❺ 目前所有外掛清單(找不認得的)---"
  wpx "$wp_path" plugin list --fields=name,status,version --format=table

  logs=( "/var/log/nginx/domains/${domain}.log"* )
  if [ -e "${logs[0]}" ]; then
    echo "--- ❻ 日誌:誰打 admin-ajax.php 與 wp-json 最頻繁(POST 內容不會被記錄,只能當弱訊號)---"
    zgrep -ahE 'POST /wp-admin/admin-ajax\.php|/wp-json/' "${logs[@]}" 2>/dev/null \
      | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 10
    echo "--- ❼ 日誌:網址列(GET)裡出現 SQL 常見字眼的請求,最多 20 筆(可能誤判,別單獨定罪)---"
    zgrep -aiE 'union[^a-z]{1,6}select|sleep(\(|%28)[0-9]|information_schema|benchmark(\(|%28)' "${logs[@]}" 2>/dev/null \
      | cut -c1-200 | head -n 20
  else
    echo "(找不到 nginx 日誌,請確認網域名稱)"
  fi
  return 0
}

# 💠 單一網站
file_scan_one "/home/user1/web/example.com/public_html"

# ❇️ 獨立多站
each_site file_scan_one

# ✴️ Multisite:mu-plugins 與外掛屬於整個網路,查主站一次
file_scan_one "/home/user2/web/network.example.com/public_html"

預期輸出範例(正常網站):

--- ❶ 核心與外掛檔案是否跟官方版本一致 ---
Success: WordPress installation verifies against checksums.
Success: Verified 12 of 12 plugins.
--- ❷ mu-plugins(會自動載入、後台外掛頁看不到)---
--- ❸ uploads 底下的 PHP 類檔案(正常應為空)---
--- ❻ 日誌:誰打 admin-ajax.php 與 wp-json 最頻繁(POST 內容不會被記錄,只能當弱訊號)---
    312 203.0.113.10
     27 198.51.100.7
--- ❼ 日誌:網址列(GET)裡出現 SQL 常見字眼的請求,最多 20 筆(可能誤判,別單獨定罪)---
  • 🔴 mu-plugins 出現不是你部署的 PHP、或 uploads 裡出現 PHP 檔 👉 先不要刪,保留時間與副本做鑑識,進入第六章「A」。
  • 🔴 外掛清單裡有你沒裝過的外掛,或 checksum 驗證回報檔案被修改 👉 進入「A」。
  • 🟡 有些外掛會在 uploads 放 index.php 這種「空白保護檔」 👉 先別當成後門,打開確認內容是不是只有一行註解。
  • 🟡 非 wordpress.org 發布的外掛(例如付費版)做 checksum 驗證會出現「無法取得檢查碼」的警告 👉 這很常見,不代表被改過;Amelia 付費版是否有此現象【❓待確認】。
  • 🟡 某個 IP 的請求次數遠高於其他人,或 ❼ 有零星命中 👉 弱訊號,對照資料量與帳號異常的時間再判斷,別單獨定罪,也別因為文字命中就直接封 IP。
  • 🟢 都沒有輸出 👉 沒有直接證據,但 POST 內容不在日誌裡,仍建議做完 ㊂ 與第七章的評估。

🚨 Checksum 驗證的侷限:
它只能證明外掛目錄裡的檔案跟官方發布版一致,檢查不到資料庫裡的資料有沒有被讀走。對這個漏洞來說,「檔案沒被改」本來就是預期結果,並不能證明資料沒外洩。請把它當成完整性檢查的其中一環,不是唯一的安全保證 🔐


🔒 🔒 🔒

🔒 ㊄ 加固:讓「下一個」類似的洞傷害小一點

決策原理:更新外掛是根本。SQL 注入的加固重點是「縮小資料庫帳號的權限」與「縮短暴露時間」:自動更新能縮短暴露時間;檢查資料庫帳號是否只擁有自己那一個資料庫的權限,則能讓「注入成功之後能碰到的範圍」變小。HestiaCP 預設每個網站有獨立的資料庫帳號,檢查的目標是確認它沒有被放大成全域權限。下面兩步都沒有改動資料庫設定,第二步只讀取權限清單 🔧 wp plugin auto-updates enable 需要較新的 WP-CLI,指令不存在時請改用後台的「啟用自動更新」連結【❓待確認】。

# ❶ 開啟 Amelia 自動更新,並用清單確認
WP_PATH="/home/user1/web/example.com/public_html"
wpx "$WP_PATH" plugin auto-updates enable "$PLUGIN"
wpx "$WP_PATH" plugin list --name="$PLUGIN" --fields=name,auto_update --format=table

# ❷ 檢查 WordPress 資料庫帳號的權限(只讀取,不改任何設定)
wpx "$WP_PATH" db query "SHOW GRANTS FOR CURRENT_USER();"

預期輸出範例(不同版本的 WP-CLI、MariaDB 訊息文字可能略有差異):

Success: Enabled 1 plugin auto-update.
name           auto_update
ameliabooking  on
+---------------------------------------------------------------------------+
| Grants for example_wp@localhost                                           |
+---------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `example_wp`@`localhost` IDENTIFIED BY PASSWORD '*' |
| GRANT ALL PRIVILEGES ON `example_wp`.* TO `example_wp`@`localhost`        |
+---------------------------------------------------------------------------+
  • 🟢 auto_update 顯示 on,且授權清單只有 USAGE ON *.*(等於沒有全域權限)加上自己那個資料庫的權限 👉 加固到位。
  • 🟠 出現 ALL PRIVILEGES ON *.* 或其他資料庫的權限 👉 資料庫帳號權限過大,請由熟悉資料庫的人收斂,不要自己亂改。
  • 🟠 auto-updates 指令不存在 👉 改用後台的「啟用自動更新」,再用 plugin list –fields=name,auto_update 確認。
  • 🟡 如果你有訂閱 Patchstack 或其他網站防火牆(WAF),確認對應的規則是否已啟用,沒有訂閱不用特地買,更新才是根本。
階段 💠 單一網站 ❇️ 獨立多站 ✴️ Multisite
① 環境判斷 each_site detect_one 只有一行 多行 💠,路徑各自不同 出現 ✴️
② 盤點版本 inventory_one 路徑 each_site inventory_one 查主站一次,status 看 active-network
③ 資料量與帳號 單一網站 逐站各自檢查 另加 super-admin list
④ 檔案與日誌 單一網域 逐網域各看各的 共用日誌,外掛屬於整個網路
⑤ 加固 逐站設定 逐站設定,資料庫帳號各自檢查 資料庫共用,檢查一次

🔥 核心原則:「批次」不等於「把所有東西塞進一條迴圈」。真正安全的批次排查,是先判斷環境、再逐層縮小範圍、一次處理一站,而且每一步失敗就停下 🎯

[43]


🪛 🪛 🪛

🛠️ 第六章:止血、補洞、驗收——一站一個閉環

前一章解決「怎麼判斷」,這一章是實際動手。順序都是同一套:備份 👉 預覽(Dry-run)👉 更新 👉 驗證,只是已經有可疑跡象時,要先插入「保留證據與止血」。以下都沿用第五章的工具箱函式,沒貼的話請先回去貼一次。每個會改動系統狀態的步驟,後面都接了驗證,不用憑感覺猜有沒有成功 🧹


🔴 🔴 🔴

🚨 ㊀ A. 疑似中招:先留證據,再止血,最後才修補

決策原理:這個漏洞的主要風險是「資料被讀」,所以什麼樣算「疑似中招」?符合下列任一項:日誌或其他線索顯示有人大量、反覆敲打預約相關入口;客人反映收到不明來源、卻帶有自己預約資訊的詐騙訊息;或第五章出現陌生管理員、可疑檔案。符合的話,第一優先不是急著更新,而是「先保留現場」,因為更新會覆寫外掛檔案,破壞部分鑑識線索。順序是:資料庫快照與日誌 👉 停用外掛 👉 更換 wp-config.php 的金鑰與鹽值 👉 重設管理員密碼 👉 清掉所有登入階段。證據資料夾裡有資料庫匯出,權限會設成 700,請別外傳 🧯

❶ 保留證據

這一段用 heredoc 產生一份 sed 腳本,把 wp-config.php 裡的資料庫密碼與 8 組金鑰、鹽值遮成 REDACTED 後才存檔,這樣你把證據交給資安人員時,不會把網站機密一起交出去。腳本會自己檢查遮蔽有沒有命中 🗂️

# ⚠️ 只對「有疑慮的那一站」執行。證據資料夾含資料庫匯出,權限設 700,請勿外傳
STAMP="$(date +%Y%m%d-%H%M%S)"
EVIDENCE_DIR="${HOME}/amelia-incident-${STAMP}"
mkdir -p -- "$EVIDENCE_DIR" && chmod 700 -- "$EVIDENCE_DIR"

# 用 heredoc 產生「遮蔽密碼與鹽值」的 sed 腳本,留存 wp-config.php 時不把機密一起存進去
REDACT_SED="${EVIDENCE_DIR}/redact.sed"
cat > "$REDACT_SED" <<'EOF2'
s/^(define\([[:space:]]*['"](DB_PASSWORD|AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|NONCE_KEY|AUTH_SALT|SECURE_AUTH_SALT|LOGGED_IN_SALT|NONCE_SALT)['"][[:space:]]*,[[:space:]]*)(['"]).*(['"])[[:space:]]*\)/\1\3REDACTED\4)/
EOF2

evidence_one() {
  local wp_path="$1" domain out cfg
  local -a logs
  domain="$(domain_of "$wp_path")"
  out="${EVIDENCE_DIR}/${domain}"
  mkdir -p -- "$out" && chmod 700 -- "$out"

  # ❶ 資料庫快照(失敗就中斷,沒有快照不要往下做)
  wpx "$wp_path" db export - --single-transaction > "$out/db_${STAMP}.sql" || return 1
  [ -s "$out/db_${STAMP}.sql" ] || { echo "❌ 資料庫匯出是空的"; return 1; }

  # ❷ 帳號、外掛清單快照
  wpx "$wp_path" user list --fields=ID,user_login,user_email,roles,user_registered --format=csv > "$out/users.csv"
  wpx "$wp_path" plugin list --fields=name,status,version --format=csv > "$out/plugins.csv"

  # ❸ 日誌(含輪替檔)
  logs=( "/var/log/nginx/domains/${domain}.log"* )
  [ -e "${logs[0]}" ] && cp -a -- "${logs[@]}" "$out/"

  # ❹ mu-plugins 與 uploads 裡的 PHP 類檔案(只打包可疑目標,不整包備份)
  if [ -d "$wp_path/wp-content/mu-plugins" ]; then
    tar -czf "$out/mu-plugins_${STAMP}.tar.gz" -C "$wp_path/wp-content" mu-plugins
  fi
  find "$wp_path/wp-content/uploads" -type f \
    \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) -print0 2>/dev/null \
    | tar -czf "$out/uploads-php_${STAMP}.tar.gz" --null -T - 2>/dev/null

  # ❺ wp-config.php 的遮蔽版(金鑰、資料庫密碼都已被 REDACTED 取代)
  cfg="$(wpx "$wp_path" config path)" || return 1
  sed -E -f "$REDACT_SED" -- "$cfg" > "$out/wp-config.redacted.php"
  if grep -qE "DB_PASSWORD'[[:space:]]*,[[:space:]]*['\"]REDACTED" "$out/wp-config.redacted.php"; then
    echo "✅ 已留存遮蔽版 wp-config.php"
  else
    echo "⚠️ 遮蔽規則沒有命中 DB_PASSWORD,請手動檢查後再外傳 $out/wp-config.redacted.php"
  fi

  chmod -R go-rwx -- "$out"
  printf '✅ %s 的證據已保存在 %s\n' "$domain" "$out"
  return 0
}

evidence_one "/home/user1/web/example.com/public_html"

預期輸出範例:

✅ 已留存遮蔽版 wp-config.php
✅ example.com 的證據已保存在 /root/amelia-incident-20261024-170339/example.com
  • ⭕ 兩行都是 ✅ 👉 進入下一步「止血」。
  • 🟠 資料庫匯出失敗或檔案是空的 👉 函式會立刻中斷,先處理磁碟空間或資料庫權限(df -h 看空間),不要在沒有快照的情況下繼續。
  • 🟠 出現「遮蔽規則沒有命中 DB_PASSWORD」 👉 你的設定檔格式特殊,請手動檢查後再外傳該檔案。
  • 🟡 找不到日誌 👉 日誌可能被輪替或網域名稱不同,不影響後續步驟。

❷ 止血:隔離、換鹽值、重設密碼、清登入階段

決策原理:要先停用外掛,不然你處理到一半,對方又敲一次就前功盡棄。接著用 wp config shuffle-salts 更換 8 組金鑰與鹽值,所有已登入的人(包含你自己)會立即被登出。這顆漏洞是「資料可能被讀走」,而資料庫裡的管理員密碼雜湊也是資料,🟡 合理推論:如果有人讀到雜湊,可能拿回去離線嘗試破解,所以管理員密碼要換。鹽值能讓舊的登入 Cookie 失效,但不能讓已經外流的密碼雜湊作廢,那一項要靠換密碼。驗證的方式是比對金鑰區塊與密碼雜湊的指紋:前後不同才算成功,而且不會把金鑰與密碼印在螢幕上。管理員密碼被重設成隨機值,由管理員用登入頁的「忘記密碼」自己設定新的 🔐

# ㊁ 隔離:停用外掛(Multisite 連全網與各子站一起處理)
isolate_one() {
  local wp_path="$1" url status
  if wpx "$wp_path" core is-installed --network 2>/dev/null; then
    wpx "$wp_path" plugin deactivate "$PLUGIN" --network
    while IFS= read -r url; do
      wpx "$wp_path" --url="$url" plugin deactivate "$PLUGIN" 2>/dev/null
    done < <(wpx "$wp_path" site list --field=url)
  else
    wpx "$wp_path" plugin deactivate "$PLUGIN"
  fi
  status="$(wpx "$wp_path" plugin get "$PLUGIN" --field=status)" || return 1
  case "$status" in
    inactive) echo "✅ $PLUGIN 目前狀態:inactive" ;;
    *)        echo "❌ $PLUGIN 目前狀態:$status,沒有停用成功"; return 1 ;;
  esac
}

# ㊂ 強制重置 wp-config.php 的 8 組金鑰與鹽值,舊的登入 Cookie 全部失效
salts_fingerprint() {
  local wp_path="$1" cfg
  cfg="$(wpx "$wp_path" config path)" || return 1
  grep -E "^[[:space:]]*define\([[:space:]]*['\"](AUTH_KEY|SECURE_AUTH_KEY|LOGGED_IN_KEY|NONCE_KEY|AUTH_SALT|SECURE_AUTH_SALT|LOGGED_IN_SALT|NONCE_SALT)['\"]" "$cfg" \
    | sha256sum | cut -d' ' -f1
}

rotate_salts_one() {
  local wp_path="$1" before after
  before="$(salts_fingerprint "$wp_path")" || return 1
  wpx "$wp_path" config shuffle-salts || return 1
  after="$(salts_fingerprint "$wp_path")" || return 1
  if [ "$before" != "$after" ]; then
    echo "✅ 金鑰與鹽值已更換(指紋不同)"
  else
    echo "❌ 指紋沒變:wp-config.php 可能唯讀,或金鑰定義在別的檔案"
    return 1
  fi
}

# ㊃ 管理員密碼全部重設成隨機值(不顯示在螢幕上,改走「忘記密碼」流程)
reset_admins_one() {
  local wp_path="$1" admin_id before after
  while IFS= read -r admin_id; do
    before="$(wpx "$wp_path" user get "$admin_id" --field=user_pass | sha256sum | cut -d' ' -f1)"
    wpx "$wp_path" user reset-password "$admin_id" --skip-email >/dev/null || {
      printf '❌ ID=%s 重設失敗\n' "$admin_id"; return 1; }
    after="$(wpx "$wp_path" user get "$admin_id" --field=user_pass | sha256sum | cut -d' ' -f1)"
    if [ "$before" != "$after" ]; then
      printf '✅ 管理員 ID=%s 的密碼雜湊已改變\n' "$admin_id"
    else
      printf '❌ 管理員 ID=%s 的密碼雜湊沒變\n' "$admin_id"; return 1
    fi
  done < <(wpx "$wp_path" user list --role=administrator --field=ID)
}

# ㊄ 清掉所有人的登入階段
destroy_sessions_one() {
  local wp_path="$1" user_id
  while IFS= read -r user_id; do
    wpx "$wp_path" user session destroy "$user_id" --all >/dev/null 2>&1 \
      || printf '⚠️ ID=%s 沒有階段可清,或指令不支援,略過\n' "$user_id"
  done < <(wpx "$wp_path" user list --field=ID)
  echo "✅ 登入階段清除流程跑完"
}

# 綁成一站一個閉環:任何一步失敗就停
contain_one() {
  local wp_path="$1"
  isolate_one "$wp_path" \
    && rotate_salts_one "$wp_path" \
    && reset_admins_one "$wp_path" \
    && destroy_sessions_one "$wp_path" \
    || { printf '❌ 止血停在 %s,請先排查\n' "$wp_path"; return 1; }
}

contain_one "/home/user1/web/example.com/public_html"

預期輸出範例:

Plugin 'ameliabooking' deactivated.
✅ ameliabooking 目前狀態:inactive
Success: Shuffled the salt keys.
✅ 金鑰與鹽值已更換(指紋不同)
✅ 管理員 ID=1 的密碼雜湊已改變
✅ 登入階段清除流程跑完

以上是典型輸出,不同版本的 WP-CLI 訊息文字可能略有差異,判斷標準是每一行的 ✅ 與 ❌。

  • ⭕ 全部 ✅ 👉 保留證據資料夾,接著走「C. 正式修補」。企業或會員制網站請同步通知內部資安或維運負責人。
  • 🟠 「指紋沒變」 👉 常見原因有兩個:wp-config.php 是唯讀(先用 ls -l 看擁有者與權限),或金鑰其實定義在別的檔案裡。函式會中斷,後面的步驟不會執行。
  • 🟠 管理員密碼雜湊沒變 👉 先確認該帳號是否存在,錯誤訊息會在上面一行,不要重複硬跑。
  • 🟡 登入階段清除顯示「略過」 👉 該使用者本來就沒有階段,或你的 WP-CLI 不支援,這一步的效果主要已被換鹽值涵蓋【❓待確認】。
  • 🔴 郵件功能壞掉,管理員收不到「忘記密碼」信 👉 才考慮在 reset-password 後面加上 –show-password,但新密碼會顯示在螢幕、可能留在終端機捲動紀錄,請在私密環境操作,並要求管理員登入後立刻自行更換。

🚨 畫重點:換鹽值會把你自己也登出
這是正常現象,不是出錯。重新登入後,其他還在線上的人(包含攻擊者手上的舊 Cookie)也都需要重新登入,這正是目的。有設定「保持登入」的外部工具(例如行動版管理 App),也要重新登入 🔁

確認陌生帳號、可疑檔案後的處理(刪帳號、清後門),牽涉到鑑識與責任歸屬,建議交給資安事件應變人員。保留證據、不要自己直接刪,是這一章最重要的底線。若確認後門可能讀過 wp-config.php,請連資料庫密碼一起更換(HestiaCP 有 v-change-database-password 指令,用法請先查官方文件,更換後也要同步更新設定檔【❓待確認】)🗑️

🧑‍🔧 DevOps 小知識:金鑰換完,別忘了「資料庫裡的密碼」
預約外掛通常會在設定裡存放第三方服務的連線資訊,例如寄信用的 SMTP 帳密、Google 或 Outlook 日曆的授權、付款閘道的金鑰。如果你判斷資料庫「可能被讀過」,這些也要到各自的服務後台重新產生,換完再回填外掛設定。Amelia 實際把哪些資訊存在資料庫、是否加密,公開資料沒有說明【❓待確認】,請以你實際啟用的整合為準 🔑

[44]


🟡 🟡 🟡

🧯 ㊁ B. 短期應急:還沒確認中招,但現在還不能更新

🚨 短期應急 ≠ 正式修補。
停用外掛只是緩衝,目的是縮短暴露時間,真正的修補仍然是更新到官方目前最新版 🧱

決策原理:這顆漏洞的惡意內容出現在請求的內容裡,Nginx 的規則通常只看得到網址和標頭,看不到 POST 內容,而且公開資料沒有指出確切端點,所以我不建議用寬泛的 Nginx 或 Apache 規則去擋:擋太廣會把正常的預約、後台與 API 一起擋掉,擋太窄又擋不到。最乾淨的做法就是直接停用外掛(Multisite 加 –network)。代價是前台預約功能會暫停,如果你的業務離不開它,請盡快安排更新時段,並在空窗期考慮用公休公告或電話預約替代 🔒

# 暫時止血:直接停用外掛(Multisite 加 --network),然後確認狀態
WP_PATH="/home/user1/web/example.com/public_html"
wpx "$WP_PATH" plugin deactivate "$PLUGIN"
wpx "$WP_PATH" plugin get "$PLUGIN" --field=status

預期輸出範例:

Plugin 'ameliabooking' deactivated.
inactive
  • 🟢 最後一行顯示 inactive 👉 外掛已經停用,漏洞入口暫時關閉。
  • 🟠 顯示 active 或 active-network 👉 停用沒成功,Multisite 請加上 –network 再試,不要重複硬跑。
  • 🟡 停用後前台預約表單消失 👉 預期現象,更新完再重新啟用。
  • 🟡 Patchstack 公告提到他們的服務有緩解規則 👉 前提是你有訂閱並已啟用,確認防護產品的實際規則狀態,不要假設「有裝就有保護」。

🚀 🚀 🚀

㊂ C. 正式修補:備份 👉 預覽 👉 更新 👉 驗證

決策原理:備份檔放在 root 才讀得到的資料夾,不放網站目錄,並把權限設成 600,避免被外部直接下載(資料庫備份含客人個資)。預覽用 plugin update –dry-run,只看會更新什麼,不真的動手。更新時,原本啟用的維持啟用,原本停用的(例如剛做完 A 的止血)維持停用,等你確認沒問題再手動啟用。驗證分三層:版本、檔案完整性、前台首頁回應;預約流程本身則要你自己測一次,腳本替你測不了 💾

BACKUP_DIR="${HOME}/amelia-backup"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p -- "$BACKUP_DIR" && chmod 700 -- "$BACKUP_DIR"

# ❶ 備份:資料庫+Amelia 外掛目錄,放在 root 才讀得到的位置(不要放網站目錄)
backup_one() {
  local wp_path="$1" domain
  domain="$(domain_of "$wp_path")"
  wpx "$wp_path" db export - --single-transaction > "$BACKUP_DIR/${domain}_db_${STAMP}.sql" || return 1
  [ -s "$BACKUP_DIR/${domain}_db_${STAMP}.sql" ] || { echo "❌ 資料庫備份是空的"; return 1; }
  [ -d "$wp_path/wp-content/plugins/$PLUGIN" ] || { echo "❌ 找不到外掛目錄"; return 1; }
  tar -czf "$BACKUP_DIR/${domain}_${PLUGIN}_${STAMP}.tar.gz" -C "$wp_path/wp-content/plugins" "$PLUGIN" || return 1
  chmod 600 -- "$BACKUP_DIR/${domain}_"*"${STAMP}"*
  ls -lh -- "$BACKUP_DIR/${domain}_"*"${STAMP}"*
}

# ❷ 預覽:只看會更新什麼,不真的動手
dryrun_one() {
  local wp_path="$1"
  printf '▶ %s\n' "$wp_path"
  wpx "$wp_path" plugin update "$PLUGIN" --dry-run
}

# ❸ 更新:原本啟用的維持啟用,原本停用(例如剛止血完)的維持停用
update_one() {
  local wp_path="$1"
  if ! wpx "$wp_path" plugin get "$PLUGIN" --field=status >/dev/null 2>&1; then
    printf '⚪ %s 沒裝 %s,略過\n' "$wp_path" "$PLUGIN"
    return 0
  fi
  wpx "$wp_path" plugin update "$PLUGIN"
}

# ❹ 驗證:版本、檔案完整性、前台首頁回應三層都要過
verify_one() {
  local wp_path="$1" ver home_url code
  ver="$(wpx "$wp_path" plugin get "$PLUGIN" --field=version 2>/dev/null)" || {
    printf '⚪ %s 沒裝 %s,略過\n' "$wp_path" "$PLUGIN"; return 0; }
  if dpkg --compare-versions "$ver" lt "$FIXED"; then
    printf '❌ %s 版本仍是 %s,還沒修補\n' "$wp_path" "$ver"
    return 1
  fi
  wpx "$wp_path" plugin verify-checksums "$PLUGIN" || return 1
  home_url="$(wpx "$wp_path" option get home)" || return 1
  code="$(curl -sSL -o /dev/null -w '%{http_code}' --max-time 20 "${home_url%/}/")"
  case "$code" in
    2*|3*) printf '✅ %s → 版本 %s、首頁 HTTP %s\n' "$wp_path" "$ver" "$code" ;;
    *)     printf '❌ %s → 首頁 HTTP %s,請立刻檢查\n' "$wp_path" "$code"; return 1 ;;
  esac
}

# ❺ 綁成一站一個閉環
patch_one() {
  local wp_path="$1"
  printf '\n🚀 開始處理 %s\n' "$wp_path"
  backup_one "$wp_path" \
    && dryrun_one "$wp_path" \
    && update_one "$wp_path" \
    && verify_one "$wp_path" \
    || { printf '❌ 停在 %s,請先排查再處理下一站\n' "$wp_path"; return 1; }
}

# 💠 單一網站
patch_one "/home/user1/web/example.com/public_html"

# ❇️ 獨立多站:一次一站,遇到失敗就停
each_site patch_one

預期輸出範例:

🚀 開始處理 /home/user1/web/example.com/public_html
-rw------- 1 root root 8.4M ... example.com_db_20261024-170341.sql
-rw------- 1 root root  31M ... example.com_ameliabooking_20261024-170341.tar.gz
▶ /home/user1/web/example.com/public_html
(預覽:顯示 ameliabooking 2.4.11 → 目前官方最新版)
Success: Updated 1 of 1 plugins.
Success: Verified 1 of 1 plugins.
✅ /home/user1/web/example.com/public_html → 版本 2.4.13、首頁 HTTP 200
  • 🟢 最後一行 ✅ 👉 這一站完成。接著手動走一次預約、確認通知信與(若有)付款、日曆整合;如果剛做過止血,確認沒問題後用 wp plugin activate ameliabooking 重新啟用,再用 wp plugin get ameliabooking –field=status 確認是 active。
  • 🟠 備份失敗 👉 函式中斷,先看磁碟空間與權限,不要進入更新。
  • 🟠 預覽看不到可用更新,但版本還是 2.4.11 以下 👉 不要當成安全。常見原因是主機連不到 WordPress.org、外掛不是從官方目錄安裝,或是付費版需要從官網下載。請改走官網的更新流程,或洽主機商與外掛支援。
  • 🟠 verify-checksums 報錯 👉 如果你用的是付費版,無法取得官方檢查碼是常見現象,請手動比對檔案來源,確認後再繼續;官方版出現檔案不一致,先保存輸出與異常檔案副本再判讀,不要直接刪除【❓待確認】。
  • 🔴 更新後首頁不是 2xx/3xx,或前台白畫面 👉 不要降回舊版(舊版就是有洞的那一版),先把外掛停用,再用備份與錯誤日誌排查。

🧑‍🔧 多站環境的實務原則:一次只處理一站,至少第一批這樣做。先完成一站的「備份 👉 預覽 👉 更新 👉 驗證」,確認沒有 PHP Fatal Error、白畫面或前台表單異常,再處理下一站。each_site 遇到失敗會自動停下,不會把後面的站也一起帶下水。全部完成後,別忘了清一次網站快取與 CDN 快取。Multisite 的外掛檔案是全網共用的,更新一次即可,但資料庫備份要包含整個網路 🎛️

🧊 冷知識:為什麼備份要放 root,不放網站使用者?
萬一網站被入侵,攻擊者通常拿到的是「網站使用者」的權限。如果備份放在他寫得到的位置,備份也可能一起被動手腳。放在只有 root 讀寫得到的資料夾,是最簡單的一層保護 🗄️

🔥 一句話總結:先判斷環境,再盤點、備份、預覽,確認無誤才逐站更新,最後驗證。有中招跡象,就把「保留證據 → 止血」排在更新之前。換成下一個外掛、下一個 CVE,這套閉環一樣可以直接套用 🦸

[45]


📣 📣 📣

📣 第七章:資料可能被讀走了,客人那邊要不要說?通知前先做的三件事

前面的章節處理的是「網站」,這一章處理的是「人」。SQL 注入最麻煩的地方在於:就算你更新得很快,也沒辦法百分之百證明這段期間沒有人讀過資料,因為日誌通常不記錄 POST 內容。這時候你需要的是一套冷靜的判斷流程,而不是情緒反應 🧭

  • ㊀ 算出「暴露期間」:
    暴露期間大致是「Patchstack 記載的通報日(10/18)或你的外掛實際落後的時間」到「你更新完成」之間。更新時間你自己最清楚,建議寫下確切的日期時間。注意:漏洞是否在公開前就被有心人知道,我們無從得知【❓待確認】,所以暴露期間以「你的版本曾在 2.4.11 以下」為準,不要只算公告日之後 📅
  • ㊁ 算出「資料範圍」:
    用第五章 ㊂ 的資料量指令,把 Amelia 相關資料表的筆數記下來。客人的姓名、電話、信箱、預約內容算不算「個人資料」,答案是算;付款資訊是否存在這些表,要看你有沒有啟用站內付款以及外掛怎麼存,公開資料沒有說明【❓待確認】🔢
  • ㊂ 交給對的人判斷:
    是否構成「個資外洩」、是否需要通報主管機關或通知當事人,取決於證據、資料內容與你的行業別,需要法務或專業顧問判斷【❓待確認:實際法律責任需由專業法務判定】。你的工作是保留證據、把前兩項整理成一頁紙,交給判斷的人 🩺

🚥 畫重點:沒有證據顯示資料被讀,不等於資料沒被讀;有「可能性」不等於一定要發公告。把「已確認」、「可能」、「無證據」三層寫清楚,是對客人和對自己最負責的做法。第一時間不確定的事,不要用肯定句對外說明 🙅‍♂️

🩵 小資族荷包試算:沒預算做完整鑑識怎麼辦?
至少做到三件事:保留日誌與資料庫備份(硬碟錢而已)、換掉所有管理員密碼與第三方金鑰(時間成本)、把更新時間與處理過程寫成一頁紀錄。這三項花的是 NT$0 加上一個下午,之後如果真的需要專業鑑識或法務,這份紀錄會替你省下很多費用 💸


🟢 🟢 🟢

🤔 第八章:讀到快喘不過氣?三個常見焦慮,加上一套「舉一反三」的防禦心法

  • 🏮 Q:更新後預約會不會壞掉?
    A:官方更新日誌寫的是錯誤修正與安全修正,理論上不應該動到功能,但第三方整合(日曆、付款、簡訊)我無法保證完全不變【❓待確認】。所以更新前先備份、更新後自己走一次預約,是最穩的做法 💾
  • 🏮 Q:外包說「我們只用 2.4.12,應該沒事」,我該聽嗎?
    A:2.4.12 在三個漏洞資料來源都被列為修補版,這點沒問題。但官方日誌對 2.4.13 才寫了安全修正,建議請對方說明為什麼不更新到最新,或直接要求更新。資訊不足時,往最新靠攏最安全 💣
  • 🏮 Q:看不懂第五、六章,是不是就沒救了?
    A:不會。第三章全程用滑鼠就能做完最基本的防護;第四到第六章是給有 SSH 權限、想深入排查的人看的補充 🤗

🧩 舉一反三:SQL 注入這一類漏洞的通用防禦心法

以下是這個漏洞類型的通用防禦知識,雖然不是本 CVE 的官方要求,但下次遇到同類問題就能用得上 🦸

  • ⭐ 用「密封袋」而不是「拼接字串」。程式在把外部輸入交給資料庫之前,應該使用參數化查詢,把「指令的結構」和「填進去的資料」分開處理。只靠黑名單刪掉幾個特殊字元,通常不是完整的防線。
  • ⭐ 資料庫帳號給最小權限。WordPress 用的資料庫帳號只需要自己那個資料庫的讀寫權限,不該有全域管理權限,萬一被注入,能碰到的範圍就小很多。
  • ⭐ 一個站一個資料庫帳號。多站共用同一組帳號,一個站出事,其他站的資料也跟著暴露。
  • ⭐ 開自動更新,並設定通知。這類漏洞從公開到有人拿來掃描,窗口很短,自動更新是最便宜的一道防線。
  • ⭐ 敏感資料能不存就不存。預約網站不需要記下客人的身分證字號或完整卡號;少存一份資料,就少一份被讀走的風險。

🏁 🏁 🏁

🏆 第九章:這次到底該多緊張?事件綜合評估

評估維度(10 分制,數字越高代表風險或應對品質越正向) 分數 一句話評語
漏洞嚴重程度(越高代表風險越大) 8.5 不用登入、不用互動、網路直達,資料庫資料可能被讀;CVSS 兩種版本落在 7.5 到 9.3,我取中間偏高,因為預約資料的個資價值高
官方應變速度 7 修補版已釋出、官方日誌也有加強提醒;但 2.4.12 的日誌沒有安全字樣,訊息一致性略扣分
一般站長自救可行性 9 後台點「更新」就能解決,要多做的是備份與測試預約流程
DevOps 技術文件完整度 4 沒有公開端點、參數與修補差異,排查只能靠「結果特徵」;各家報告細節也互相矛盾
🏆 加權綜合建議 立即更新 最終建議:【今天排進行程,最慢 24 小時內更新到官方最新版;對外公開的預約網站優先,並同步檢查管理員帳號與評估資料暴露範圍】

🔥 這起事件提醒我們:漏洞不一定藏在複雜的核心系統,有時候就藏在「客人填一個預約表單」這種最日常的功能裡。細節少、攻擊程式不明,不等於可以放著不管,因為真正有差別的,是你在別人拿到攻擊方法之前,有沒有把門關好。與其等客人收到詐騙訊息才發現不對勁,不如現在花五分鐘,打開後台看一眼版本號碼 🔢

[46]


📎 📎 📎

📎 附錄:把容易混淆的地方一次對清楚

附錄 A:版本與時間對照表

你看到的版本 代表什麼 建議動作
2.4.11 以下 受影響範圍 備份後立即更新到官方最新版
2.4.12 CVE 與資安公司公告的最低修補版 建議再升到最新版
2.4.13 或更新 官方日誌明確寫含安全修正 確認檔案完整性與預約流程即可
官網付費版的版本號 版本號體系與官方外掛頁不同,是否受影響【❓待確認】 以廠商官網公告對照,不要直接套用本文版本數字

附錄 B:來源互相打架的地方,我怎麼處理

  • 🏮 分數 9.3 或 7.5:兩份都列出,不選邊站,兩者都屬於需要盡快修補的等級。
  • 🏮 修補版 2.4.12 或 2.4.13:列出兩種說法,建議採用較新者。
  • 🏮 「攻擊者可能竄改資料」:部分報告這樣寫,但公開評分是 I:N,所以本文只放在推論層級,不當事實。
  • 🏮 專用偵測規則:公開資料沒有端點與參數,所以不列特定的 AJAX 動作名稱或封鎖規則。
  • 🏮 是否在野利用:次級來源(CVE 資料庫的 SSVC 欄位、CVETodo)顯示未確認,我沒能親自讀到 NVD 頁面的 SSVC 分頁,所以只能當作 🟡 參考【❓待確認】。

附錄 C:還沒搞清楚的事(待確認清單)

  • 🔎 漏洞的確切端點、參數與修補程式差異。
  • 🔎 2.4.13 修補的範圍,以及與 2.4.12 的關係。
  • 🔎 付費版(官網版本)是否受影響、版本對照。
  • 🔎 Amelia 把哪些第三方金鑰存在資料庫、是否加密。
  • 🔎 plugin verify-checksums 對 Amelia 付費版的實際行為。
  • 🔎 2.4.13 的精確發布日期。

附錄 D:給主機商或工程師的一句話

🫂 「請檢查我的 WordPress 網站,外掛 Amelia(ameliabooking)是否在 2.4.11 以下,漏洞編號 CVE-2026-112327。請先備份資料庫與外掛目錄,再更新到官方最新版,更新後測試預約流程,並協助檢查管理員帳號與日誌。」複製貼上就能用 📋

📚 參考資料與延伸資源

列印本文 [58] 👨‍👩‍👧‍👦 78 次瀏覽