🗓️ 預約資料可能被人悄悄摸走?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 嚴重 等級。最主要的風險是:壞人可以從網路直接打進來、不用帳號、不用你配合點任何釣魚連結,就能悄悄讀走資料庫裡的資料(包含客人的姓名、電話、信箱和預約紀錄)。目前還沒看到被大規模利用的災情,但這種洞很容易被掃描程式自動化攻擊。今天花五分鐘按個更新,絕對比事後一家家打電話跟客人道歉省事太多了 🥱
內容目錄
📒 第一章:預約登記簿被人用「暗號」翻過?一個問法沒寫好的資料庫漏洞
(🔴 以下是假設情境,用來幫你抓畫面,不是實際的受害紀錄)
想像你開了一間人氣很高的美髮沙龍,櫃檯放著一本預約登記簿,客人可以自己寫下姓名、想預約的時段。平常店員只會看「姓名」和「時段」這兩欄。有一天,有人在姓名欄裡寫的不是名字,而是一句店員會當成「工作指令」的話,像是:「順便把這本簿子裡所有客人的電話抄一份給我」,而店員居然真的照辦了 📒
這就是大名鼎鼎的 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,後台按一下就好。有些資安服務商會說「買我們的防火牆就不怕」,但那需要你本來就有訂閱。真正貴的是出事之後:請人鑑識、通知客人、處理客訴,這些都比花五分鐘更新貴太多了 💸
⚠️ 說大白話:最壞會怎樣?把「鐵證」、「推論」、「假設」分開看
🧊 冷知識:為什麼網站被讀走資料,日誌卻像「歲月靜好」?
伺服器的存取日誌像門口警衛的簽到本,只記「誰在幾點去了哪個網址」,不記「他在表單裡寫了什麼」。所以日誌裡沒看到可疑字眼,不能當成安全的唯一證明 🕵️
資安新聞最怕自己嚇自己。這次評分有兩種版本,但都把 機密性 (Confidentiality) 評為高,完整性 (Integrity) 評為無。
《什麼是機密性與完整性❓ 想像你的網站是一間「高級保險箱出租中心」:機密性破功(高風險)代表小偷拿著手電筒,把你保險箱裡放了多少錢「全部看光光」👀;而完整性沒事(無風險)代表小偷「只能看,不能碰」,他沒辦法把你的錢偷走或把保險箱砸爛 🙅♂️》
翻成白話,官方認定的主要風險是資料被讀走,不是被改掉或刪掉 👇
- ✅ 已確認事實:漏洞類型為 SQL 注入;受影響版本 2.4.11 以下;從網路就能發動、不需要帳號;機密性影響高、完整性影響無。
- 🟡 合理推論:預約外掛通常放著客人的姓名、聯絡方式甚至付款狀態,所以資料外洩的實際代價,往往比一般外掛嚴重。但實際能讀到哪些資料,還要看漏洞確切位置與資料庫設定【❓待確認】。
- 🔴 假設情境:有人用掃描工具大量嘗試 👉 讀走客人名單 👉 拿去發詐騙簡訊或釣魚信。這是常見的「資料外洩後劇本」,不代表單靠這個洞就會自動走完全程。
🚥 畫重點:別把「完整性:無」讀成「只會被讀、不用擔心被改」!
這個「無」是評分單位對「這個漏洞本身」的判斷,不是保證。如果你發現網站資料被改、出現陌生管理員,那比較可能是另一條入侵路線,不要直接怪到這個漏洞頭上。
🎭 第二章:先別急著恐慌,看看你是哪一種「預約網站頭家」?
警報一響,不同身分要做的事差很多。先對號入座,再決定你接下來要做什麼 🧭
| 你的情境 | 要不要緊張? | 現在最該做的事 |
|---|---|---|
| 🙋 第一種:店家老闆、小編(單一網站) | 中等,更新就能解決 | 跳到第三章:確認版本 👉 備份 👉 更新 👉 測一次預約(全程免指令) |
| 👨💻 第二種:自架站玩家、接案工程師(管多個站) | 中偏高,站越多越容易漏 | 用腳本盤點整台主機,再逐站修補(詳見完整版後續章節) |
| 🏢 第三種:診所、補習班、企業(握有大量客戶資料) | 高度警戒,牽涉個資責任 | 先更新、保留日誌,並由法務評估是否須通報客戶 |
| 🚨 第四種:外掛裝了放著、預約表單還開著 | 最高,隨時可被掃到 | 不更新就停用外掛,不要兩邊都不做 |
🙋 第一種:我只是顧後台、回客人訊息的老闆或小編
你不需要懂程式碼。要做的事只有:確認版本、備份、更新、測一次預約流程。第三章把每一步「滑鼠點哪裡」都拆開了。更新是免費的,大膽按下去就對了 ✅
👨💻 第二種:我是包山包海的自架站玩家(多站點)
如果你用面板管著十幾個客戶的網站,需要的是「一次掃完整台主機」的火力。更新完記得清一次網站快取與 CDN 快取,不然前台常常還卡在舊畫面 🔧
🧑🔧 DevOps 小知識:資料庫是共用的,傷害就是整包的
SQL 注入打的是資料庫。如果你把好幾個客戶的網站塞在同一個資料庫帳號底下,一個站出事,其他站的資料就有可能被連帶讀到。養成「一個站一個資料庫、獨立帳號」的習慣,是成本很低的保險 🧱
🏢 第三種:我們是診所、補習班或企業,握有一大把客戶資料
預約資料就是個人資料。如果日後確認有資料外洩,台灣《個人資料保護法》的責任歸屬與通報義務需要由專業法務判定【❓待確認】。你現在能做的,就是盡快更新、保留日誌證據、評估資料量 🩺
🚨 第四種:地雷區!「活動早就結束了,放著沒差吧?」
(🔴 假設情境)某間工作室兩年前用 Amelia 辦過報名活動,活動結束後網頁撤了,但外掛一直沒停用。老闆以為「沒在用就沒事」。
🙅 這就像你家兩年前裝了一扇後門,平常不走,但鎖壞了你也不知道。只要外掛還亮著「已啟用」,它對外開放的入口就還在,掃描程式照樣能敲門 🆘
🚥 畫重點:更新與停用,今天至少選一個做!如果因為相容性不敢更新,退而求其次就是立刻停用。「沒在用」不代表沒風險,「已停用」才是真正拉下鐵門 🙅♂️
🛟 第三章:電腦小白專屬!五分鐘無痛自救法(全程滑鼠,免敲指令)
不管你是哪一種網站主,都建議照這幾步走一次。每一步都有手把手的防呆說明 🤏
🫂 新手求助:我連後台都進不去、也看不懂版本號,怎麼辦?
直接把這段話複製給你的主機商或接案工程師:「我的網站可能裝了 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 沒改變,代表壞人最多只能動到這間店裡的東西;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 的原因 🧱
🕵️ 第五章:怎麼知道有沒有人先進來看過?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) 👉 人工確認哪個才是正式環境。
🔎 ㊁ 盤點版本:哪些站裝了它、現在哪一版?
決策原理:版本比對用 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 版本略有差異【❓待確認】。
- 🟢 一切正常 👉 這一輪沒發現異常。「陌生」不等於「惡意」,也可能是外包或維運商的帳號,先問過再判斷。
🔍 ㊃ 檔案與日誌:有沒有後續的連鎖反應?
決策原理:官方評分把完整性影響列為「無」,所以這一步的目的是「排除」而不是「找證據」:核心與外掛檔案的官方檢查碼、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 |
| ④ 檔案與日誌 | 單一網域 | 逐網域各看各的 | 共用日誌,外掛屬於整個網路 |
| ⑤ 加固 | 逐站設定 | 逐站設定,資料庫帳號各自檢查 | 資料庫共用,檢查一次 |
🔥 核心原則:「批次」不等於「把所有東西塞進一條迴圈」。真正安全的批次排查,是先判斷環境、再逐層縮小範圍、一次處理一站,而且每一步失敗就停下 🎯
🛠️ 第六章:止血、補洞、驗收——一站一個閉環
前一章解決「怎麼判斷」,這一章是實際動手。順序都是同一套:備份 👉 預覽(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 實際把哪些資訊存在資料庫、是否加密,公開資料沒有說明【❓待確認】,請以你實際啟用的整合為準 🔑
🧯 ㊁ 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,這套閉環一樣可以直接套用 🦸
📣 第七章:資料可能被讀走了,客人那邊要不要說?通知前先做的三件事
前面的章節處理的是「網站」,這一章處理的是「人」。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 小時內更新到官方最新版;對外公開的預約網站優先,並同步檢查管理員帳號與評估資料暴露範圍】 |
🔥 這起事件提醒我們:漏洞不一定藏在複雜的核心系統,有時候就藏在「客人填一個預約表單」這種最日常的功能裡。細節少、攻擊程式不明,不等於可以放著不管,因為真正有差別的,是你在別人拿到攻擊方法之前,有沒有把門關好。與其等客人收到詐騙訊息才發現不對勁,不如現在花五分鐘,打開後台看一眼版本號碼 🔢
📎 附錄:把容易混淆的地方一次對清楚
附錄 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。請先備份資料庫與外掛目錄,再更新到官方最新版,更新後測試預約流程,並協助檢查管理員帳號與日誌。」複製貼上就能用 📋
📚 參考資料與延伸資源
- 📌 CVE.org|CVE-2026-112327 官方紀錄 [47]
- 📌 NVD|CVE-2026-112327 [48]
- 📌 Patchstack|WordPress Amelia Plugin <= 2.4.11 SQL Injection [49]
- 📌 Wordfence|Amelia <= 2.4.11 未授權 SQL 注入 [50]
- 📌 WordPress.org|Amelia 官方外掛頁與更新日誌 [1]
- 📌 CVEFeed|CVE-2026-112327(SSVC 次級資料) [51]
- 📌 WP-CLI 官方文件|wp config shuffle-salts [52]
- 📌 WP-CLI 官方文件|wp plugin update [53]
- 📌 WP-CLI 官方文件|wp user reset-password [54]
- 📌 WP-CLI 官方文件|wp plugin verify-checksums [55]
- 📌 WP-CLI 官方文件|wp core verify-checksums [56]
- 📌 HestiaCP 官方文件|CLI 參考 [57]










