網址隨便加幾個字,網站站長就換人當?🗓️ My Calendar 超高危險漏洞大解密(CVE-2026-104798)
🔥 懶人包:
超多人用的活動行事曆外掛 My Calendar – Accessible Event Manager(在 WordPress 官方可是有 20,000+ 活躍安裝數喔!)最近被貼上了一張超大張的通緝令:CVE-2026-104798《什麼是CVE❓這就像是通緝犯的專屬身分證號碼:讓全世界的警察(資安人員)都知道現在在抓哪一個壞蛋》。更可怕的是它的 CVSS 分數高達 9.8 嚴重等級《什麼是CVSS❓你可以把它想成颱風警報的威力等級:滿分10分,9.8分就相當於強烈颱風直接登陸,破壞力直接拉滿 🌪️》簡單來說,駭客不需要有你網站的帳號密碼、也不用騙你點什麼奇怪的釣魚連結,只要在網址後面加上一段特製的「通關密語」,就有極大的機率可以直接把網站第一號管理員(通常就是你!)的密碼給改掉。受影響的版本非常廣泛,目前最新的 3.8 版本線只要是 3.8.7 以下(含)通通中標,官方已經緊急推出 3.8.8 修補包。只要花你五分鐘,趕快確認版本、按個更新,順便檢查一下家裡有沒有被偷塞陌生人 🔒
🌐 點我直達 WordPress 官方外掛頁確認最新版本 [1]
內容目錄
🪧 第一章:案發現場還原!為什麼會議室門口的預約白板,最後會變成打開保險箱的遙控器?
想像一下這個畫面:你們公司走廊上掛著一塊超方便的「會議室預約白板」,不管是誰路過都可以隨手在備註欄寫字。這時候,總機小姐有一個很熱心(但有點要命)的習慣:只要看到備註欄裡有用中括號 [某某名稱] 框起來的字,她就會當成是「分機快速撥號鍵」直接幫你轉接 ☎️ 平常大家寫「[下午三點] A會議室」,相安無事。直到有一天,有個路過的陌生人,在白板上寫了一串偽裝成快速撥號的「密碼重設指令」,指名要總機去找人事部,把老闆的門禁密碼換掉。結果總機小姐居然連問都沒問「你是誰?」,就直接照辦了 🗓️
在 WordPress 的世界裡,用中括號包起來的 [某某名稱] 被稱為短代碼。它的存在就像是總機電話上的快速鍵:按 3 就接客服,省去背一長串號碼的麻煩。但這套聰明設計的背後,藏著一個天真的假設:「會按按鍵的人,都是自己人」。這次的災難就在於,陌生人居然也能把「要按哪顆鍵」直接寫進網址裡,而外掛還真的乖乖照按了 ☎️
這次的 CVE-2026-104798 漏洞,玩的就是這種「借刀殺人」的把戲。出包的主角 My Calendar 是一款專門幫 WordPress 網站製作活動行事曆的外掛。真正破洞的地方,其實根本不在你看到的漂亮日曆畫面上,而是在網址最後面一個名為 mcs 的小參數裡 🔍
🧊 冷知識:短代碼其實是個「骨灰級」的老古董?
Shortcode API 其實早在 2008 年的 WordPress 2.5 版本就已經加入核心功能了!十幾年來,它簡直就像外掛界的「萬能強力膠」,到處都能黏。但也正因為它無所不在,一旦「誰有資格發送短代碼」這道防線沒守好,引發的災情往往比單一外掛本身還要慘烈 🧩
官方在更新日誌裡只輕描淡寫地寫了一句:「修補了一個嚴重的、免登入的權限提升漏洞《什麼是權限提升❓你可以把它想成拿著一般員工的門禁卡,卻刷開了董事長辦公室的金庫大門:這就是駭客夢寐以求的終極目標!》」。因為官方沒有公佈太多細節,所以以下的攻擊過程,是我從各大資安報告中拼湊出來的輪廓 🧾
- ㊀ 壞人在網址塞進帶有中括號的 mcs 參數:🟡 根據報告,這個參數的內容會被直接放進網頁裡,而且中括號居然沒有被過濾掉 🔗
- ㊁ 網頁內容被送進短代碼處理器:🟡 於是,裡面夾帶的 [my_calendar] 短代碼,就被系統當成正常的合法指令開始執行 ⚙️
- ㊂ 短代碼的 above 屬性被拿去呼叫重要函式:🟡 報告指出,外掛把它拆開後直接交給了 PHP 內建的 call_user_func(),不但沒檢查發送者有沒有登入,甚至沒有限制只能呼叫哪些函式 📞
- ㊃ 呼叫一路暢通無阻,抵達了修改使用者的核心函式:🟡 帶進去的參數被系統自動轉換成了整數 1。登愣!結果就是 ID 為 1 的帳號(通常是網站的第一個管理員)密碼跟信箱就被改掉了 🔑
🚨 畫重點:我絕對不會給你完整的攻擊字串
雖然有些報告寫得很露骨,但我這篇絕對不會照搬!這篇文章的目的是要幫你當個「聰明的防盜專家」,告訴你鎖在哪裡壞了、該怎麼補,而不是教你怎麼當小偷。你只要記住一個大原則:這個漏洞的攻擊大門是「公開網址」,而且駭客完全不需要擁有你網站的帳號就能動手 🙅
🧊 冷知識:為什麼遭殃的總是「ID 1」?
因為 PHP 語言有個奇妙的特性:當它試圖把一個「非空陣列」硬轉成整數時,得到的結果固定就是 1。如果報告裡描述的機制沒錯,這就完美解釋了為什麼受害的苦主永遠是 ID 1(也就是絕大多數網站剛架設時,建立的第一個創始管理員帳號)。不過,你網站的 ID 1 現在到底還是不是管理員,這點得看你自己的帳號管理習慣,不能一概而論 🪪
⏱️ 炸彈是什麼時候被引爆的?事件時間軸一次看懂
| 時間點 | 發生了什麼大事 | 站長可信度判定 |
|---|---|---|
| 2026-10-15 | 知名資安機構 Wordfence 首度揭露,揪出漏洞的英雄署名為 @daroo | 🟡 多份報告內容吻合;不過 Wordfence 原始頁面有阻擋,無法直接100%核對細節【❓待確認】 |
| 2026-10-16 | CVE-2026-104798 漏洞紀錄正式被公開在陽光下 | 🟡 同上【❓待確認】 |
| 官方修補版 | WordPress 官方外掛區目前最新的穩定版已經來到 3.8.8,更新日誌也承認修補了嚴重的免登入權限提升漏洞 | ✅ 站長親自去官方頁面核對無誤! |
| 到底有沒有人受害? | 我目前取得的資料裡,還沒有看到明確的大規模災情紀錄;CISA KEV 等國家級警報我暫時無法進一步驗證 | 【❓待確認】 |
大家千萬不要有一種鴕鳥心態:「既然沒看到災情,那我是不是可以再等等?」
🙅 錯!這種「免登入、光靠網址就能打」的漏洞,一旦被公開,駭客的自動掃描機器人絕對是 24 小時不眠不休地在網路上瘋狂敲門。所以,時間軸上的空白,絕對不是讓你拿來拖延更新的藉口 ⏳
- ✅ 已確認事實:目前 WordPress.org 上面 My Calendar 的最新穩定版是 3.8.8,官方也白紙黑字寫明修補了這個嚴重漏洞;另外,新版外掛要求網站環境必須是 WordPress 6.5 以上、PHP 7.4 以上。
- 🟡 合理推論:資安圈給它的攻擊向量代號是 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H。翻成白話文就是:「只要連上網路就能打、技術門檻有夠低、不需要任何帳號、不需要受害者點擊,而且網站的機密性、資料完整性跟系統可用性,會被一次『滿門抄斬』」。
- 🔴 假設情境:如果一個壞人成功改掉了 ID 1 的信箱和密碼 👉 他就能大搖大擺登入後台 👉 接著裝上各種惡意後門外掛 👉 最後整碗端走,把你的網站變成他的肉雞。請注意,這是一連串的連續動作,並不是單靠這顆 CVE 就能一鍵完成,所以我特別把它拉出來當作假設情境。
🩵 小資族荷包試算:按個更新到底要花多少錢?
這可是 WordPress 官方的免費外掛!按下安全更新的費用是 NT$ 0 元。你只需要在後台輕輕點一下滑鼠就能解決。但你知道嗎?如果真的「出事了」,你要花錢請資安專家來緊急救援、重建被弄亂的帳號資料,還要承擔網站被 Google 貼上「危險網站」黑名單的流量損失。這些後續的人力與商譽成本,隨便算都遠遠超過一次免費更新啊 💸
🎭 第二章:先別自己嚇自己,對號入座看看你是哪一種網站主?
看到同一份資安警告信,負責發文的小編、喜歡自己買主機架站的硬核玩家、還有大公司的 IT 部門,該做的事情完全不一樣!先看看自己屬於下面哪一種,再決定你要把文章往下讀到哪裡 🧭
🙋 我只是負責發文、顧後台的小編或無辜的老闆
別怕,你完全不需要懂什麼是 PHP 程式碼!你要做的只有四個超簡單的動作:看版本、按備份、點更新、最後看一眼管理員名單。第三章我會手把手、全程帶你用滑鼠解決。但請特別注意一件事:如果你現在發現自己「突然登不進後台」了,千萬不要手賤去按「忘記密碼」!原因我會在第三章解釋給你聽 🧑💼
👨💻 我是自己租主機架站的硬派玩家,手上有好幾個客戶的網站
你需要的是「整台主機一次掃描」的爽快感!直接跳到第五章的工具箱,我準備了批次指令,能幫你找出每一個 WordPress,還會貼心地用各網站擁有者的身分去跑 WP-CLI,絕對不弄髒你的 root 家目錄。請注意,這顆漏洞同時開了五條版本線的副本,盤點的時候一定要逐站對比,千萬別看到「3.8.x」就以為天下太平了 🔧
🧑🔧 DevOps 小知識:為什麼這次的漏洞修補要「逐分支」來判斷?
平常我們看漏洞,通常是 低於某個版本號 = 危險。但這次原廠非常有誠意(也很讓人頭痛),他們替過去的五條舊版本線都各自推出了專屬修補包:像是 3.7.17 還是受害者,要到 3.7.18 才安全;而 3.8.7 同樣也是受害者。如果你只傻傻地拿最新的 3.8.8 當作唯一的安全標準,那你可能會把已經乖乖打好補丁的 3.7.18 誤判成高危險群!這就是為什麼我們需要更聰明的判斷腳本 🧱
🏢 我們是企業網站,裡面存了一大堆會員和訂單個資
免費外掛最大的痛點,就是沒有所謂的「SLA 保固條款」,出事了沒人能保證多久會修好。雖然這次廠商很負責地補了五條分支,但翻開過去的黑歷史,這款外掛在權限控管上跌倒已經不是第一次了。如果貴公司的會員個資有可能已經外洩,依照台灣現行的《個人資料保護法》,你們對會員可能會有主動通知的法律義務!這部分請務必盡快交給公司法務判斷【❓待確認】。站長這邊只是幫大家統整技術情報,重大決定還是要請教專業顧問喔 🩺
🚨 冤枉啊!我早就沒在用那個行事曆了,外掛只是懶得刪而已
某個社團三年前用這個外掛辦了一場超大型的尾牙報名,活動結束後就把報名網頁給刪了,但是外掛卻一直維持在「啟用」的狀態。管理員心想:「反正前台都沒放日曆了,駭客總不可能憑空打進來吧?」
🙅大錯特錯!根據資安報告,這個漏洞的觸發點,是藏在外掛處理「網址參數」的底層流程裡,根本不管你的網頁上有沒有真的擺出行事曆。只要外掛還是「啟用」狀態,你家後門就等於沒上鎖!這就像你家有扇三年沒打開過的後門,你以為很安全,其實鎖頭早就壞了 🆘!
🚥 站長拿螢光筆畫重點!千萬別踩雷:
「更新」跟「停用」,你今天最少最少要選一個做!如果暫時不能更新,就先把它停用。記住:「我沒在用」絕對不是你不處理的藉口,「已經停用」或「徹底刪除」才算是真正把駭客擋在門外 🙅♂️
🧮 來對獎囉!你的版本到底安不安全?五條版本線對照表
這張表怎麼看?很簡單:先看看你的版本號是「3.幾」開頭,然後對照表格。只要你的數字小於右邊那欄的「修到這版才安全」,抱歉,你就是受災戶。這些版號數據都是從資安報告和官方日誌交叉比對出來的,雖然 Wordfence 的原始頁面被擋住了有點可惜,但大方向絕對錯不了 🧾
| 你的主線版本 | 受影響的危險區間 | ✅ 修到這版才安全 |
|---|---|---|
| 3.8 | 3.8 ~ 3.8.7 | 3.8.8 |
| 3.7 | 3.7 ~ 3.7.17 | 3.7.18 |
| 3.6 | 3.6 ~ 3.6.17 | 3.6.18 |
| 3.5 | 3.5 ~ 3.5.21 | 3.5.22 |
| 3.4 及更早古董 | 3.4.24 以下(含) | 3.4.25;更舊的版本拜託你直接升到最新版啦! |
想像一下,熱水器大廠突然發現瓦斯閥有瑕疵。廠商佛心來著,沒有強迫大家都買最新型號,而是幫過去五年的老機型都各自出了專屬的「修正包」。如果你家的是 2022 年款,就乖乖裝 2022 年的修正包;硬裝別年的反而會出事。但如果你家是用十年前的老古董,那廠商也只能雙手一攤,請你直接買一台新機器了。上面那張表,就是你的「熱水器型號對應修正包」清單 🔧
順帶一提,這個外掛也是會挑環境的:最新版要求你的主機必須具備 WordPress 6.5 以上,以及 PHP 7.4 以上的規格。如果你的主機環境太老舊,更新可能會卡關,這時候請先跳到第六章採取短期應急措施,然後趕快把主機升級吧 🧰
🕰️ 考古時間:這外掛以前出過事嗎?黑歷史一次翻給你
我們不隨便抹黑別人,直接讓公開數據說話。下面這四筆紀錄,每一筆都是有拿到正式 CVE 身分證的資安事件,有興趣的可以自己去 Google 查證 🔍
| 公開曝光時間 | 受波及版本 | 出包類型與嚴重度 | 專屬 CVE 編號 |
|---|---|---|---|
| 2023-05-22 | ≤ 3.4.3 | 跨站請求偽造(CSRF),已於 3.4.4 修補 | CVE-2023-23813 |
| 2025-12-09 | ≤ 3.6.16 | 缺少授權檢查(CWE-862),CVSS 4.3,已於 3.6.17 修補 | CVE-2025-67592 |
| 2026-07-02 | ≤ 3.7.14 | 不安全的物件參照,免登入就能把非公開活動的 iCalendar 給偷匯出來,CVSS 5.3;修補版號不明 | CVE-2026-11896 |
| 時間成謎【❓待確認】 | ≤ 3.7.8 | 免登入的時間盲注 SQL 注入漏洞(mc_auth 參數惹禍),CVSS 7.5;目前只有單一來源證實 | CVE-2026-6854 |
| 2026-10-16 | ≤ 3.8.7 | 免登入權限提升(就是我們今天的主角!),CVSS 9.8,已於 3.8.8 修補 | CVE-2026-104798 |
看出來了嗎?近兩年來,這款外掛在「誰有資格做什麼事」的檢查防線上,已經摔跤好幾次了,而這次更是摔得最慘的一次。這不是叫你立刻把外掛刪掉不用,而是提醒你:它絕對不是那種可以讓你「裝了就忘、有空再更新」的佛系外掛 🧯
🛟 第三章:電腦小白看過來!五分鐘無痛自救指南
💬 溫馨小叮嚀:更新外掛就像是「把壞掉的鎖換新」,它只能保證從現在開始小偷進不來,但沒辦法幫你倒帶檢查「昨天小偷有沒有已經偷偷跑進來逛過」。所以最後那兩步(查名單、留意登入狀況)千萬不能偷懶跳過;如果你們家網站有存客戶訂單或是會員資料,請務必往上報備給主管,由他們來判斷要不要做更深度的資安健檢 🩺
不管你是公司老闆、小編還是接案仔,這一章拜託你一定要跟著走一遍。每一個步驟我都幫你列出「正常的樣子」跟「覺得怪怪的該怎麼辦」,全程只要動滑鼠就好 🤏
- ㊀ 先查水表,看看有沒有裝:
登入你的 WordPress 後台,看左邊黑色的選單找到「外掛」→「已安裝的外掛」,在右上角的搜尋框打上 My Calendar。如果看到一個叫「My Calendar – Accessible Event Manager」的東西,恭喜你中獎了;如果搜不到,那就代表沒裝,這篇文章你可以當作聽鬼故事提早下課啦 🔎 - ㊁ 核對版本號,拿去跟第二章的表格對答案:
版本號就寫在外掛名稱的正下方。拿去跟第二章的表格對照,只要你的數字比「修到這版才安全」還要小,那就是危險群。如果懶得對表,直接看有沒有出現 3.8.8 或是更新的版本數字,有的話就直接給它升級上去,最省事 🕵️♀️ - ㊂ 動手前,先給自己留條後路(備份):
去主機商的後台點一下「一鍵備份」,或是用你平常用的備份外掛先存個檔。如果你連「備份」兩個字怎麼寫都不知道,聽我的,直接把這篇文章的網址 Line 給幫你維護網站的工程師,這本來就是他們的工作,大膽地要求他們處理就對了 💾 - ㊃ 深呼吸,按下「立即更新」:
更新跑完之後重新整理一下網頁,確認版本號已經跳到安全範圍,然後去你的網站前台逛一下,看看日曆有沒有破圖。注意!如果更新失敗或是畫面變成一片慘白,千萬別像中邪一樣狂按重整,趕快進後台先把外掛「停用」,這樣至少可以先躲過駭客的攻擊 🔄 - ㊄ 懶人福音:開啟自動更新:
在外掛清單裡,每個外掛的右邊都有一個「啟用自動更新」的藍色小字,點它一下,以後有安全修補包它就會自己偷偷裝好,省得你整天提心吊膽 ⏰ - ㊅ 化身名偵探,揪出後台的陌生人:
點左邊選單的「使用者」,然後點最上面的「管理員」分頁。正常情況:裡面全都是你認識的同事,信箱也都是你們家自己的網域。見鬼的情況:信箱變成了什麼 .ru 結尾的奇怪網域、或是多出一個你根本沒印象的帳號!這時候請把目光鎖定在「最早建立的那個帳號」(通常是網站建置時的第一個管理員),這就是這次漏洞的主要狙擊目標。如果真的看到髒東西,先別急著自己動手刪,截圖下來交給懂資安的專家處理 🔀 - ㊆ ⚠️ 救命!我突然登不進後台了:
冷靜!手離開滑鼠!絕對不要按「忘記密碼」!
這次的漏洞就是專門改掉你的帳號信箱跟密碼,如果你傻傻地按了重設,系統只會把熱騰騰的新密碼寄去給駭客剛改好的信箱,等於你雙手把鑰匙奉上。這時候請直接找你的主機商或維護工程師,請他們從伺服器底層幫你搶回主導權 🚫
🫂 新手求助專區:看到這些專有名詞就頭暈怎麼辦?
沒關係,站長教你一招「複製貼上之術」。直接把下面這段話傳給你的主機客服或外包商:「你好,我的網站可能有裝 My Calendar 外掛,請幫我確認版本是否安全,漏洞編號是 CVE-2026-104798,並請幫我檢查管理員名單有沒有被竄改。」對方只要看到這串英文加數字,就知道該怎麼辦了。切記,在沒有備份的情況下,千萬別自己去亂刪東西喔 🤗
🧊 冷知識:「停用」跟「刪除」到底差在哪裡?
這就像是你家買了一台微波爐。「停用」就是把插頭拔掉,機器還佔著位子,但只要不通電就不會起火(暫時擋住漏洞),哪天想用插頭插回去就好。「刪除」則是直接把整台微波爐丟到資源回收車,連影子都不剩。如果你的網頁上原本有放行事曆,一旦「刪除」,那個區塊就會直接破一個大洞給你看 🧰
🤿 第四章:技術深潛——陌生人的網址參數,是怎麼一路走到「改帳號」的?
這一章給想知道「為什麼會這樣」的人。只想補洞,前三章已經夠用;想看細節,往下拆。先說清楚資料狀況:官方沒公開程式差異,Wordfence 與作者網站我也無法直接開啟,下面的機制全部來自多份來源報告彼此一致的描述,標 🟡,程式碼層級的細節一律【❓待確認】⚔️
| 評估指標 | 具體資訊 |
|---|---|
| 外掛名稱 | My Calendar – Accessible Event Manager |
| 外掛代號(slug) | my-calendar |
| CVE 編號 | CVE-2026-104798 |
| CVSS 3.1 | 9.8(Critical) |
| CVSS 向量 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 弱點類型 | CWE-269:權限管理不當(免登入權限提升) |
| 受影響與修補版本 | 五條版本線各自不同,見第二章對照表;3.8 線為 ≤ 3.8.7,3.8.8 修補 |
| 發現者 | @daroo 與 Wordfence(依 WordPress.org 更新日誌致謝) |
| 觸發入口 | 公開網址的 mcs 參數(🟡 來源報告) |
| 外掛要求 | WordPress 6.5 以上、PHP 7.4 以上(WordPress.org 外掛頁) |
🧩 三個環節串成一條完整的鏈
- 🏮 環節一:外部輸入被塞進頁面內容。網址參數的值在處理短代碼之前,就先被放進了要渲染的文字裡,而中括號沒被清掉。這是經典的「先加料、後洗菜」順序問題 🥬
- 🏮 環節二:短代碼屬性直接變成函式呼叫。報告描述 mc_generate_calendar_nav() 會拆解 above 屬性,再丟給 call_user_func()。這個函式的用途是「依名稱呼叫某個函式」,如果名稱來自外部又沒有白名單,攻擊者就等於拿到一個遙控器 🎮
- 🏮 環節三:沒有任何身分檢查擋在中間。依報告,整條路徑沒有登入檢查,也沒有權限判斷。所以 CVSS 向量裡 PR(所需權限)是 N,UI(使用者互動)也是 N 🚪
櫃台助理可以替人轉接分機,這沒問題。問題是這次助理不看名單,來電的人報什麼號碼就轉什麼號碼,連「人事部密碼管理」那種內線也轉。白名單的做法很單純:櫃台手上有一張寫了十個分機的紙,不在紙上的一律不轉。外掛少做的就是這張紙 📋
🧑🔧 DevOps 小知識:這顆漏洞不是 RCE,為什麼還是 9.8?
它的直接後果是「改寫 ID 1 的憑證」,不是在伺服器上執行任意程式。但 WordPress 管理員能裝外掛、改佈景主題檔案,拿到管理員幾乎等於拿到程式執行能力。所以事後的重點不只是「更新外掛」,還要查「有沒有人以管理員身分裝了東西」。這也是第七章的 disable_functions 只能當第二道防線、擋不住這顆漏洞本身的原因 🛡️
🧊 冷知識:9.8 其實是這個向量的天花板
CVSS 3.1 在 S:U(影響範圍未改變)、而且 C/I/A 三項全高的情況下,最高就是 9.8;想拿到 10.0,影響範圍必須跨越安全邊界(S:C)。所以 9.8 的意思是「在不跨出外掛本身權限範圍的前提下,已經是最嚴重的一檔」,不是「還差一點才滿分」🎯
🔧 3.8.8 到底修了什麼?
官方更新日誌只寫了一句安全修補,沒有公開差異。有來源報告推測是加上了呼叫函式前的白名單與參數清洗,我沒能在官方程式碼核對,所以只當 🟡 合理推論,不當事實寫死【❓待確認】。你唯一確定能做的判斷是:版本落在第二章表格的安全欄,就是官方認定的已修補狀態 🧾
🚨 畫重點:「觸發條件的完整細節」目前查不到【❓待確認】
哪些頁面、哪些設定組合一定能觸發,官方沒公開,各家報告的描述也不完全一致。所以第五章的排查我刻意不依賴某一條特定攻擊字串,而是看兩種結果特徵:網址裡有帶方括號的 mcs 請求,以及 ID 1 帳號有沒有被動過。代價是會有一些雜訊,好處是不會因為猜錯入口而漏掉真正的異常 🧭
🕵️ 第五章:怎麼確認你家有沒有被摸過?排查 Checklist
🤖 重要提醒:以下指令由五份來源報告(ChatGPT、DeepSeek、Gemini、Grok、Meta AI)彙整,經 Claude 複查修正與整理,並依 HestiaCP 官方文件與 WP-CLI 官方文件逐項複查,所有程式碼區塊都做過 bash 語法檢查
但你的主機環境不一定跟範例一樣。正式環境的高風險操作,包含更新、停用、改設定檔、重設密碼或執行批次腳本,請務必由熟悉該站架構的專業人員做最後確認與執行,切勿直接複製貼上就按 Enter。範例中的 /home/admin/web/example.com/public_html 與 admin 都是示範用名稱,請換成你自己的環境。腳本預設以 root 身分在 Debian 12/13 上執行 🧙
這一章的流程是:工具箱 👉 盤點版本 👉 日誌排查 👉 帳號與檔案稽核 四步,每一步都拆成「為什麼這樣做、指令、預期輸出、看到什麼怎麼辦」。以上全部只讀不寫,放心跑,不會改任何東西。預期輸出只是典型範例,版本不同可能略有差異 📜
🧰 ㊀ 先貼這一段:整章都會用到的小工具
為什麼這樣做:HestiaCP 的每個網站屬於不同的 Linux 使用者。用 root 直接跑 WP-CLI 會產生 root 擁有的檔案,之後網站自己寫入就會失敗。所以先定義幾個小函式:自動偵測 WP-CLI 的名稱(有的主機叫 wp,有的叫 wp-cli)、以檔案擁有者身分執行、逐站迴圈,以及最重要的 is_patched,專門處理五條版本線各自的修補門檻。所有路徑變數都有用雙引號包起來,路徑含空白也不會斷 🧱
# ㊀ 工具箱:同一個 SSH 連線貼一次就好,連線中斷後要重貼
PLUGIN="my-calendar"
LATEST="3.8.8"
# 找出 WP-CLI(有的主機叫 wp,有的叫 wp-cli)
WPCLI="$(command -v wp || command -v wp-cli || true)"
if [ -z "$WPCLI" ]; then
echo "⚠️ 找不到 wp 或 wp-cli,請先安裝 WP-CLI 再往下"
fi
command -v sudo > /dev/null || echo "⚠️ 找不到 sudo(root 可改用 runuser -u 使用者 -- 指令)"
# 列出主機上每一個 WordPress(HestiaCP 標準位置:/home/使用者/web/網域/public_html)
list_wp_configs() {
find /home -maxdepth 5 -type f -path '*/web/*/public_html/wp-config.php' -print0 2> /dev/null
}
site_owner() { stat -c '%U' -- "$1/wp-config.php"; }
site_domain() { basename -- "$(dirname -- "$1")"; }
# 以「網站檔案擁有者」的身分執行 WP-CLI:wpx 網站路徑 wp指令...
wpx() {
local wp_path="$1" owner
shift
owner="$(site_owner "$wp_path")" || return 1
sudo -u "$owner" -H -- "$WPCLI" --path="$wp_path" "$@"
}
# 逐站執行:任何一站失敗就停下,不會連鎖出事
each_site() {
local callback="$1" cfg
while IFS= read -r -d '' cfg <&3; do
"$callback" "$(dirname -- "$cfg")" || {
printf '⛔ 在 %s 停下,請先處理這一站\n' "$(dirname -- "$cfg")"
return 1
}
done 3< <(list_wp_configs)
}
# 版本比較:version_lt A B 成立代表 A 比 B 舊(不用字串比大小)
version_lt() {
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]
}
# 這顆 CVE 有「各分支各自的修補版」,所以要先看分支、再比對該分支的修補版
# 回傳 0=已在修補版本線上,1=受影響
is_patched() {
local v="$1" branch fixed
if version_lt "$v" "3.4"; then
return 1
fi
branch="$(printf '%s' "$v" | cut -d. -f1,2)"
case "$branch" in
3.4) fixed="3.4.25" ;;
3.5) fixed="3.5.22" ;;
3.6) fixed="3.6.18" ;;
3.7) fixed="3.7.18" ;;
3.8) fixed="3.8.8" ;;
*) return 0 ;;
esac
! version_lt "$v" "$fixed"
}
classify_version() {
local v="$1"
if [ -z "$v" ]; then
echo "⚪ 未安裝"
elif is_patched "$v"; then
echo "🟢 已在修補版本線上"
else
echo "🔴 受影響"
fi
}
預期輸出:貼完沒有任何輸出就是成功。看到 ⚠️ 代表缺 WP-CLI 或 sudo,先補上再往下,不要硬跑。
驗證(確認 is_patched 判斷正確):貼完後執行下面這行,預期輸出是每個版本一行,3.7.17 與 3.8.7 要是「受影響」,3.7.18 與 3.8.8 要是「已修補」。這行只做判斷、不碰任何網站 🧪
for v in 3.4.24 3.4.25 3.7.17 3.7.18 3.8.7 3.8.8; do if is_patched "$v"; then echo "$v 已修補"; else echo "$v 受影響"; fi done
- 🟢 結果符合預期 👉 工具箱沒貼錯,往下進行。
- 🟠 出現 command not found 👉 函式沒貼成功,重新貼一次第一段。
- 🟡 3.4.24 以下一律判為受影響,是刻意的設計:我取得的資料沒有列出更舊版本的專屬修補版,請直接升到最新版。
🧑🔧 DevOps 小知識:為什麼迴圈長成 3< <(find …) 這個樣子?
常見寫法 find | while read 有個陷阱:迴圈裡只要有指令會讀標準輸入(sudo、mysql 都可能),就會把清單吃掉一半,結果只處理到第一個網站。改成把 find 的輸出接到檔案描述符 3,再用 read … <&3 讀,迴圈內就不會互搶輸入;搭配 -print0 與 read -d ”,路徑含空白也安全 🔧
🔎 ㊁ 盤點版本:哪些站裝了它、現在哪一版?
為什麼這樣做:一台主機上有十個獨立 WordPress,跟一個 Multisite 底下有十個子站,長得像但處理方式不同。前者要逐一找每個 wp-config.php,後者外掛檔案全網只有一份。這個函式同時判斷安裝類型與版本,輸出一行一站,只讀不寫 🤔
inventory_one() {
local wp_path="$1" kind ver status
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
printf '❓ 載入失敗\t-\t-\t%s\n' "$wp_path"
return 0
fi
ver="$(wpx "$wp_path" plugin get "$PLUGIN" --field=version 2> /dev/null || true)"
status="$(wpx "$wp_path" plugin get "$PLUGIN" --field=status 2> /dev/null || true)"
printf '%s\t%s\t版本:%s\t狀態:%s\t%s\n' \
"$(classify_version "$ver")" "$kind" "${ver:--}" "${status:--}" "$wp_path"
return 0
}
# 💠 單一網站(路徑換成你的)
inventory_one "/home/admin/web/example.com/public_html"
# ❇️ 獨立多站:整台主機一次盤點(只讀不寫)
each_site inventory_one
預期輸出範例:
🔴 受影響 💠 一般安裝 版本:3.8.7 狀態:active /home/admin/web/site-a.com/public_html 🟢 已在修補版本線上 💠 一般安裝 版本:3.7.18 狀態:active /home/bob/web/shop.example/public_html ⚪ 未安裝 💠 一般安裝 版本:- 狀態:- /home/bob/web/blog.example/public_html 🟢 已在修補版本線上 ✴️ Multisite 版本:3.8.8 狀態:active-network /home/bob/web/network.example/public_html
分支判斷邏輯:
- 🔴 受影響 👉 排入第六章的備份與更新,並接著做 ㊂、㊃,確認舊版時期有沒有被碰過。
- 🟢 已在修補版本線上 👉 版本條件已解除,但舊版若曾暴露一段時間,仍建議做 ㊂、㊃。
- 🟡 狀態是 inactive 👉 外掛已停用,攻擊面暫時不在,但仍建議更新或刪除,不要放著。
- ✴️ Multisite 出現 active-network 👉 外掛檔案全網共用,查主站一次就代表整個網路,更新也只做一次。
- 🟠 出現 ❓ 載入失敗 👉 先排查 PHP 或資料庫連線,不要直接對這站動手。路徑看起來像 public_html.bak 之類的備份副本,請人工確認哪一個才是正式站。
📊 ㊂ 日誌排查:找「mcs 參數帶方括號」的請求
為什麼這樣做:官方沒公開 payload,標準存取日誌也不記錄 POST 內容。能抓的只有一個必然會留下的痕跡:請求網址裡有 mcs 參數,而且值或名稱帶了方括號,不論是字面的 [ 還是編碼過的 %5B。腳本只切出「請求行」那一欄比對,時間戳記裡的方括號不會誤報;用 zcat -f 一併讀取輪替後的 .log.1 與 .log.2.gz。HestiaCP 預設的網站日誌在 /var/log/nginx/domains/網域.log。如果你前面還有 CDN,日誌看到的可能是 CDN 的 IP 🎯
# 取出某網域「目前+輪替後(含 .gz)」的 nginx 存取日誌
scan_domain_log() {
local domain="$1"
find /var/log/nginx/domains -maxdepth 1 -type f -name "${domain}.log*" -print0 \
| xargs -0 -r zcat -f --
}
log_scan_one() {
local wp_path="$1" domain all total hits
domain="$(site_domain "$wp_path")"
printf '\n===== %s =====\n' "$domain"
# 只看「請求行」那一欄(用雙引號切欄),時間戳記裡的方括號不會誤報
all="$(scan_domain_log "$domain" | awk -F'"' 'tolower($2) ~ /[?&]mcs(=|\[|%5b)/')"
total="$(printf '%s' "$all" | grep -c . || true)"
hits="$(printf '%s\n' "$all" | awk -F'"' 'tolower($2) ~ /(\[|%5b|%255b)/' | tail -n 20)"
echo "--- ❶ 請求行帶有 mcs 參數的總筆數 ---"
printf '%s\n' "$total"
echo "--- ❷ 其中 mcs 帶有方括號(含編碼寫法)的請求,最多 20 筆(最值得看)---"
if [ -n "$hits" ]; then
printf '%s\n' "$hits"
echo "--- ❸ 這些請求的來源 IP 排行 ---"
printf '%s\n' "$hits" | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 10
else
echo "(無命中)"
fi
return 0
}
# 💠 單一網站
log_scan_one "/home/admin/web/example.com/public_html"
# ❇️ 獨立多站/✴️ Multisite:掃主機上每一個網域
each_site log_scan_one
預期輸出範例(可疑的樣子):
===== example.com =====
--- ❶ 請求行帶有 mcs 參數的總筆數 ---
2
--- ❷ 其中 mcs 帶有方括號(含編碼寫法)的請求,最多 20 筆(最值得看)---
203.0.113.42 - - [16/Oct/2026:10:00:00 +0800] "GET /?mcs=%5B... HTTP/1.1" 200 5 "-" "python-requests/2.31.0"
--- ❸ 這些請求的來源 IP 排行 ---
1 203.0.113.42
分支判斷邏輯:
- 🔴 ❷ 有命中,且來源是陌生 IP、User-Agent 像腳本工具 👉 視為有人在試探或已經利用,記下 IP 與時間,把 ㊃ 做完整;若 ㊃ 也有異常,直接進第六章 A。
- 🟡 ❶ 有數字但 ❷ 沒命中 👉 有人用過 mcs 參數但沒帶方括號,可能是正常使用。這個參數平常會不會出現在正常流量裡,我無法確認【❓待確認】,請對照你的網站實際行為。
- 🟢 都沒命中 👉 只能說「目前沒有直接證據」,不能證明沒被打過:日誌可能已輪替、前面有 CDN,或攻擊根本沒走你查的這個日誌。繼續做 ㊃。
🕳️ ㊃ 帳號與檔案稽核:有沒有人拿著這把鑰匙進來過
為什麼這樣做:這顆漏洞的直接後果是帳號被改,所以第一個要看的是 ID 1 的資料與登入階段,接著看管理員名單與最近註冊的帳號。管理員一旦被接管,能做的事很多,所以也看檔案那一側:uploads 裡的 PHP、會自動載入的 mu-plugins、揭露日之後被改過的 PHP,以及外掛與核心檔案跟官方版本的比對。資料表前綴(wp_)每站可能不同,所以先用 wp db prefix 取得並檢查格式,再把 SQL 寫進 mktemp 的暫存檔,用單引號包住的 heredoc 避免 Shell 解讀 SQL 裡的特殊字元 🕵️♀️
# 掃描起點:Wordfence 揭露日(2026-10-15)。想更保守,可以往前調幾天
SINCE="2026-10-15"
audit_one() {
local wp_path="$1" prefix qf opt re
re='^[A-Za-z0-9_]+$'
printf '\n===== %s(擁有者:%s)=====\n' "$wp_path" "$(site_owner "$wp_path")"
echo "--- ❶ ID 1 的帳號資料(這顆漏洞鎖定的就是它)---"
wpx "$wp_path" user get 1 --fields=ID,user_login,user_email,display_name,user_registered,roles --format=table
echo "--- ❷ ID 1 目前的登入階段(不認識的 IP 或裝置就是警訊)---"
wpx "$wp_path" user session list 1 --fields=login_time,expiration_time,ip,ua --format=table || true
echo "--- ❸ 全部管理員 ---"
wpx "$wp_path" user list --role=administrator \
--fields=ID,user_login,user_email,user_registered --format=table
echo "--- ❹ 最近註冊的 10 個帳號(任何角色)---"
prefix="$(wpx "$wp_path" db prefix)" || return 1
if ! [[ "$prefix" =~ $re ]]; then
echo "資料表前綴長得不對勁,略過 SQL 查詢"
else
qf="$(mktemp)"
cat > "$qf" <<'EOF'
SELECT ID, user_login, user_email, user_registered
FROM __PREFIX__users
ORDER BY user_registered DESC
LIMIT 10;
EOF
sed -i "s/__PREFIX__/${prefix}/g" "$qf"
wpx "$wp_path" db query < "$qf"
rm -f -- "$qf"
fi
echo "--- ❺ uploads 底下的 PHP 類檔案(正常網站應該是空的)---"
find "$wp_path/wp-content/uploads" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.pht' -o -iname '*.phar' \) \
-printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2> /dev/null | sort
echo "--- ❻ mu-plugins(不用啟用就會自動載入,後台外掛頁看不到)---"
find "$wp_path/wp-content/mu-plugins" -maxdepth 2 -type f \
-printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2> /dev/null | sort
echo "--- ❼ ${SINCE} 之後被改過的 PHP(外掛正常更新也會出現,需人工比對)---"
find "$wp_path/wp-content" -type f -name '*.php' -newermt "$SINCE" \
-not -path '*/cache/*' \
-printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2> /dev/null | sort | tail -n 100
echo "--- ❽ 外掛與核心檔案跟官方版本比對 ---"
wpx "$wp_path" plugin verify-checksums --all || true
wpx "$wp_path" core verify-checksums || true
echo "--- ❾ 幾個常被動手腳的網站設定 ---"
for opt in siteurl home admin_email users_can_register default_role; do
printf '%s = %s\n' "$opt" "$(wpx "$wp_path" option get "$opt")"
done
return 0
}
# 💠 單一網站
audit_one "/home/admin/web/example.com/public_html"
# ❇️ 獨立多站:逐站各自檢查
each_site audit_one
# ✴️ Multisite:帳號要多看一層「超級管理員」
wpx "/home/admin/web/network.example.com/public_html" super-admin list
預期輸出範例(正常的樣子,節錄):
--- ❶ ID 1 的帳號資料(這顆漏洞鎖定的就是它)--- +----+------------+-------------------+--------------+---------------------+---------------+ | ID | user_login | user_email | display_name | user_registered | roles | +----+------------+-------------------+--------------+---------------------+---------------+ | 1 | admin | admin@example.com | admin | 2024-01-15 08:00:00 | administrator | +----+------------+-------------------+--------------+---------------------+---------------+ --- ❺ uploads 底下的 PHP 類檔案(正常網站應該是空的)--- --- ❽ 外掛與核心檔案跟官方版本比對 --- Success: Verified 12 of 12 plugins. Success: WordPress installation verifies against checksums.
分支判斷邏輯:
- 🔴 ❶ 的信箱不是你們的,或 ID 1 的登入階段出現你不認識的 IP 👉 高度可疑,直接進第六章 A。你本人登不進後台,也算這一類。
- 🔴 ❸❹ 出現註冊時間在 2026-10-15 之後、你不認識的帳號 👉 高度可疑。但「陌生」不等於「惡意」,也可能是外包或維運商留的,先問清楚再處理。
- 🔴 ❺ 找到 PHP 類檔案,或 ❻ 的 mu-plugins 出現不是你部署的檔案 👉 視為已中招,先別刪,保留證據,進第六章 A。
- 🟡 ❼ 揭露日之後被改過的 PHP 👉 外掛正常更新也會出現,要對照你自己的維運紀錄,不要看到就當成入侵。
- 🟡 ❽ 出現 File was added 之類的差異 👉 先確認是不是你自己放的客製檔案;不確定就當成可疑。
- 🔴 ❾ 的 users_can_register 變成 1、default_role 變成 administrator,或 siteurl/home 被改成陌生網域 👉 網站設定被動過手腳,這類改動很少是正常維運造成的。
- 🟢 全部乾淨 👉 這一輪沒有發現明確跡象,仍建議做第六章的更新與第七章的加固,並多觀察一週。
📋 ㊄ 三環境速查表:把四步濃縮成一張
| 階段 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| ① 工具箱 | 貼一次,整個 SSH 連線通用 | ||
| ② 盤點版本 | inventory_one 路徑 | each_site inventory_one | 查主站一次,狀態看 active-network |
| ③ 日誌排查 | 單一網域 | 掃所有網域日誌 | 共用網域日誌,再依 Host 回推子站 |
| ④ 帳號與檔案 | ID 1+管理員+檔案 | 逐站各自檢查,不能拿 A 站代表 B 站 | 另加 super-admin list |
🛠️ 第六章:疑似中招、還不能更新、準備修補——三條路各走各的
前一章解決「怎麼判斷」,這一章才是動手。主線固定是 備份 👉 預覽 👉 更新 👉 驗證,只是第五章若出現跡象,要先走 A(取證與止血)。這一章會改動系統狀態,所以每個動作後面都接一個「怎麼確認真的生效」的驗證,以及失敗時下一步怎麼辦。以下都沿用第五章貼好的工具箱,沒貼的話請先回去貼一次 🧹
💾 ㊀ 兩個共用零件:備份與刷新鹽值
為什麼這樣做:A 和 C 都需要先備份,A 還需要刷新鹽值,所以先定義成函式。備份檔放在 root 家目錄下、權限 700,不放在網站目錄,避免被網址直接下載。鹽值(Salts)就是 wp-config.php 裡那八組長字串,WordPress 靠它們替登入 Cookie 簽章。這次漏洞的受害目標就是登入憑證,所以我的判斷是:舊版曾對外暴露、又有任何可疑跡象,就用 wp config shuffle-salts 強制刷新,所有人,包含可能已經拿到 Cookie 的攻擊者,立刻被登出。代價是正常使用者包括你自己也要重新登入 🔑
你只改了一號員工的密碼,但攻擊者手上可能已經有一張還沒過期的通行證,照樣進得來。刷新鹽值等於宣布「舊版通行證今天起全部作廢」,不管誰手上拿著哪一張,都得重新辦。這比單純改密碼徹底得多 🪪
# ㊀ 共用零件一:備份(資料庫+wp-content),任何一步失敗就回傳失敗
BACKUP_DIR="${HOME}/wp-security-backup"
backup_one() {
local wp_path="$1" domain stamp db_file
domain="$(site_domain "$wp_path")"
stamp="$(date +%Y%m%d-%H%M%S)"
db_file="${BACKUP_DIR}/${domain}_db_${stamp}.sql"
install -d -m 700 -- "$BACKUP_DIR" || return 1
df -h -- "$BACKUP_DIR" | tail -n 1
( umask 077; wpx "$wp_path" db export - --single-transaction > "$db_file" )
if [ ! -s "$db_file" ]; then
echo "❌ 資料庫備份失敗或是空檔,停止,不進行任何變更"
return 1
fi
if ! tail -n 1 -- "$db_file" | grep -q 'Dump completed'; then
echo "⚠️ 備份檔結尾沒有 Dump completed 字樣,請人工確認檔案是否完整"
fi
tar -czf "${BACKUP_DIR}/${domain}_wp-content_${stamp}.tar.gz" \
-C "$wp_path" wp-content || { echo "❌ 檔案備份失敗,停止"; return 1; }
chmod 600 -- "${BACKUP_DIR}/${domain}_wp-content_${stamp}.tar.gz"
ls -lh -- "${BACKUP_DIR}/${domain}_"*"_${stamp}".*
return 0
}
# ㊁ 共用零件二:刷新 wp-config.php 裡八組金鑰與鹽值(所有人的登入狀態立刻作廢)
rotate_salts_one() {
local wp_path="$1" domain stamp before after
domain="$(site_domain "$wp_path")"
stamp="$(date +%Y%m%d-%H%M%S)"
install -d -m 700 -- "$BACKUP_DIR" || return 1
# 備份放在網站目錄「外面」,因為裡面有資料庫密碼
install -m 600 -- "${wp_path}/wp-config.php" \
"${BACKUP_DIR}/${domain}_wp-config.php.${stamp}" || return 1
# 只留短雜湊做前後比對,不把真正的金鑰印在螢幕上
before="$(wpx "$wp_path" config get AUTH_KEY | sha256sum | cut -c1-12)"
wpx "$wp_path" config shuffle-salts || return 1
after="$(wpx "$wp_path" config get AUTH_KEY | sha256sum | cut -c1-12)"
if [ "$before" = "$after" ]; then
printf '❌ %s 的鹽值沒有改變,請檢查 wp-config.php 是否可寫\n' "$domain"
return 1
fi
printf '✅ %s 鹽值已更新(%s → %s),wp-config.php 權限:' "$domain" "$before" "$after"
stat -c '%a %U:%G' -- "${wp_path}/wp-config.php"
return 0
}
預期輸出範例:備份函式會列出兩個檔案與大小;刷新鹽值會印出前後兩段短雜湊。
/dev/vda1 80G 22G 54G 29% / -rw------- 1 root root 485M Oct 16 10:30 /root/wp-security-backup/example.com_db_20261016-103022.sql -rw------- 1 root root 2.4G Oct 16 10:31 /root/wp-security-backup/example.com_wp-content_20261016-103022.tar.gz Success: Shuffled the salt keys. ✅ example.com 鹽值已更新(3fa9c1d2e8b4 → 91c07a5be2d6),wp-config.php 權限:640 admin:admin
驗證與失敗分支:
- 🟢 備份:兩個檔案都在、大小合理,資料庫檔結尾有 Dump completed 👉 可以進下一步。這個結尾字樣是 mysqldump 與 MariaDB 的慣例輸出,若你的環境用別的工具匯出,字樣可能不同【❓待確認】,腳本只會警告,不會擋你。
- 🟠 備份失敗或是空檔 👉 函式立刻停下,先查磁碟空間(腳本已印出 df 結果)與資料庫權限,不要在沒備份的情況下往下。
- 🟢 鹽值:前後兩段雜湊不同 👉 已生效,你會被登出,重新登入即可。
- 🟠 出現 ❌「鹽值沒有改變」或指令本身失敗 👉 先確認 wp-config.php 的擁有者可寫;這個指令會連線到 WordPress.org 的 API 取得新鹽值,主機若連不出去也會失敗,先查網路,不要急著換別的來源。
- 🔴 刷完網站出現「建立資料庫連線時發生錯誤」 👉 用備份還原:cp -p “$BACKUP_DIR/網域_wp-config.php.時間” “網站路徑/wp-config.php”,備份檔名在上面的 ls 輸出裡。備份裡有資料庫密碼,確認一切正常後再決定要不要保留,刪除前請再三確認。
🚨 ㊁ A. 疑似中招:先留證據,再止血,最後才修補
為什麼這樣做:第五章出現明確跡象時,第一優先不是急著更新,而是保留現場。順序是:資料庫快照與日誌 👉 停用外掛 👉 刷新鹽值 👉 先把 ID 1 的信箱改回本人的 👉 再重設管理員密碼並登出所有階段。這個順序很重要:如果信箱還是攻擊者留下的,任何「寄重設信」的做法都會把新密碼送到對方手上,所以腳本一律用 –skip-email –show-password 把新密碼顯示在螢幕上,並且先處理信箱 🧯
這支腳本會動到帳號與設定,請先確認路徑,而且只對有疑慮的那一站執行,不要整台主機無差別跑。新密碼會留在終端機的捲動紀錄裡,請在私密環境操作,用安全管道交給管理員本人,並請他登入後立刻自行更換,不要貼進聊天視窗或腳本 🔐
# 前提:已貼過「工具箱」與「共用零件」。取證完成前,什麼都不要刪
incident_one() {
local wp_path="$1" domain stamp evid out uid url new_email re
re='^[^@[:space:]]+@[^@[:space:]]+$'
domain="$(site_domain "$wp_path")"
stamp="$(date +%Y%m%d-%H%M%S)"
evid="${HOME}/mc-incident-${stamp}"
out="${evid}/${domain}"
install -d -m 700 -- "$evid" "$out" || return 1
echo "=== ❶ 取證:資料庫、日誌、帳號、可疑檔案 ==="
( umask 077; wpx "$wp_path" db export - --single-transaction > "${out}/db_${stamp}.sql" )
if [ ! -s "${out}/db_${stamp}.sql" ]; then
echo "資料庫快照失敗或是空檔,停止"
return 1
fi
find /var/log/nginx/domains -maxdepth 1 -type f -name "${domain}.log*" \
-exec cp -p -- {} "$out/" \;
wpx "$wp_path" user list --role=administrator \
--fields=ID,user_login,user_email,user_registered --format=table > "${out}/admins.txt"
wpx "$wp_path" user get 1 --fields=ID,user_login,user_email,roles --format=table > "${out}/user1.txt"
wpx "$wp_path" user session list 1 --fields=login_time,expiration_time,ip,ua \
--format=table > "${out}/user1-sessions.txt" 2> /dev/null || true
find "${wp_path}/wp-content/uploads" "${wp_path}/wp-content/mu-plugins" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.phar' \) -print0 2> /dev/null \
| xargs -0 -r sha256sum > "${out}/suspect-sha256.txt"
install -m 600 -- "${wp_path}/wp-config.php" "${out}/wp-config.php.BAK.${stamp}"
echo "=== ❷ 隔離:停用外掛(Multisite 連全網與各子站一起處理)==="
if wpx "$wp_path" core is-installed --network 2> /dev/null; then
wpx "$wp_path" plugin deactivate "$PLUGIN" --network || true
while IFS= read -r url <&3; do
wpx "$wp_path" --url="$url" plugin deactivate "$PLUGIN" 2> /dev/null || true
done 3< <(wpx "$wp_path" site list --field=url)
else
wpx "$wp_path" plugin deactivate "$PLUGIN" || return 1
fi
echo "=== ❸ 強制刷新金鑰與鹽值(含攻擊者手上的 Cookie 一併失效)==="
rotate_salts_one "$wp_path" || return 1
echo "=== ❹ 先把 ID 1 的信箱改回「確定是你本人的」,再重設密碼 ==="
wpx "$wp_path" user get 1 --fields=ID,user_login,user_email --format=table
read -r -p "ID 1 本人可收信的 Email(目前顯示的就是正確的,直接按 Enter 略過):" new_email
if [ -n "$new_email" ]; then
if ! [[ "$new_email" =~ $re ]]; then
echo "Email 格式不對,停止"
return 1
fi
wpx "$wp_path" user update 1 --user_email="$new_email" || return 1
fi
echo "=== ❺ 逐一重設管理員密碼(新密碼會顯示在螢幕上)並登出所有階段 ==="
while IFS= read -r uid <&3; do
wpx "$wp_path" user reset-password "$uid" --skip-email --show-password || return 1
wpx "$wp_path" user session destroy "$uid" --all || true
done 3< <(wpx "$wp_path" user list --role=administrator --field=ID)
wpx "$wp_path" cache flush || true
printf '\n✅ %s 止血完成,證據放在 %s\n' "$domain" "$out"
ls -lh -- "$out"
return 0
}
# 只對「有疑慮的那一站」執行,不要整台主機一起跑
incident_one "/home/admin/web/example.com/public_html"
預期輸出範例:
=== ❷ 隔離:停用外掛(Multisite 連全網與各子站一起處理)=== Plugin 'my-calendar' deactivated. Success: Deactivated 1 of 1 plugins. === ❸ 強制刷新金鑰與鹽值(含攻擊者手上的 Cookie 一併失效)=== Success: Shuffled the salt keys. ✅ example.com 鹽值已更新(3fa9c1d2e8b4 → 91c07a5be2d6),wp-config.php 權限:640 admin:admin === ❺ 逐一重設管理員密碼(新密碼會顯示在螢幕上)並登出所有階段 === (每位管理員各一組隨機新密碼) ✅ example.com 止血完成,證據放在 /root/mc-incident-20261016-103500/example.com
驗證(確認真的止血了):跑完後執行下面三行,預期結果是「status 顯示 inactive」「ID 1 的信箱是你的」「ID 1 的登入階段清單只剩你剛登入的、或是空的」。三項有任何一項不符,就代表那一步沒成功,回頭看對應輸出。這三行只讀不寫 🧪
WP_PATH="/home/admin/web/example.com/public_html" wpx "$WP_PATH" plugin get "$PLUGIN" --field=status wpx "$WP_PATH" user get 1 --fields=ID,user_login,user_email --format=table wpx "$WP_PATH" user session list 1 --fields=login_time,ip,ua --format=table
- ⭕ 全部成功 👉 保留證據資料夾,接著走 C 正式修補;公司或有會員資料的網站,同步通知內部資安或主管。
- 🟠 資料庫快照失敗 👉 函式立刻中斷,先處理磁碟空間或資料庫權限,沒有快照不要往下。
- 🟠 停用外掛時出現 PHP Fatal Error 👉 外掛檔案可能被改過,請交給資安專業人員做完整清理,不要自己硬刪。
- 🔴 發現後門檔案、不認識的管理員,或網站有個資與線上收款且確認曾遭觸發 👉 這已超出更新外掛的層級,請啟動內部資安事件流程,並把 SMTP、API 金鑰等存在資料庫或設定檔的憑證一併換掉。
- 🔴 更新前的備份可能已經含有後門,不能當成「乾淨的還原來源」;要還原請用事發前、已知乾淨的備份。
🧯 ㊂ B. 短期應急:還沒確認中招,但現在還不能更新
🚨 短期應急 ≠ 正式修補。
停用外掛與擋網址都只是縮短暴露時間,真正的修補仍然是更新到第二章表格的安全版本。擋網址有副作用:mcs 參數帶方括號的正常請求也會被擋;停用外掛則是行事曆功能會暫時消失 🧱
選項一(最乾淨):直接停用外掛。Multisite 請加 –network。停用後 plugin get 必須顯示 inactive,否則就是沒成功,回頭看第一行的錯誤訊息 🧯
WP_PATH="/home/admin/web/example.com/public_html" wpx "$WP_PATH" plugin deactivate "$PLUGIN" wpx "$WP_PATH" plugin get "$PLUGIN" --field=status
選項二:行事曆不能中斷時,用 nginx 擋掉 mcs 參數裡的方括號。HestiaCP 的網站設定檔會在重建網域或續約 SSL 時被樣板覆蓋,所以千萬不要直接改 nginx.conf;官方做法是在 /home/使用者/conf/web/網域/ 放 nginx.conf_任意名稱(HTTP)與 nginx.ssl.conf_任意名稱(HTTPS),樣板會自動載入。下面的函式在 nginx -t 沒過時會自動撤回剛建的兩個檔案,不會讓 nginx 掛掉。規則比對的是未解碼的原始參數,所以同時涵蓋字面的 [、%5B 與雙重編碼的 %255B 🧱
# 用 HestiaCP 認得的自訂檔擋掉 mcs 參數裡的方括號(面板重建設定時不會被蓋掉)
block_mcs() {
local owner="$1" domain="$2"
local conf_dir="/home/${owner}/conf/web/${domain}"
local http_inc="${conf_dir}/nginx.conf_mc_cve_2026_104798"
local ssl_inc="${conf_dir}/nginx.ssl.conf_mc_cve_2026_104798"
if [ ! -d "$conf_dir" ]; then
printf '找不到 %s,略過\n' "$conf_dir"
return 1
fi
tee "$http_inc" > /dev/null <<'EOF'
# 暫時封鎖 CVE-2026-104798:mcs 參數帶方括號(含編碼寫法)一律回 403
# My Calendar 更新完成後請移除這個檔案
if ($args ~* "(^|&)mcs(=[^&]*)?(\[|%5b|%255b)") {
return 403;
}
EOF
ln -sf "$http_inc" "$ssl_inc"
# 語法檢查通過才重新載入;沒過就把剛建的兩個檔案撤掉
if nginx -t; then
systemctl reload nginx
printf '已套用:%s\n' "$domain"
else
rm -f -- "$http_inc" "$ssl_inc"
printf 'nginx 設定檢查沒過,已撤回規則:%s\n' "$domain"
return 1
fi
}
# ⚠️ 拆除規則(會刪檔,只刪我們自己建的兩個檔案)
unblock_mcs() {
local owner="$1" domain="$2"
local conf_dir="/home/${owner}/conf/web/${domain}"
rm -f -- "${conf_dir}/nginx.conf_mc_cve_2026_104798" "${conf_dir}/nginx.ssl.conf_mc_cve_2026_104798"
if nginx -t; then
systemctl reload nginx
printf '已解除:%s\n' "$domain"
else
printf 'nginx 設定檢查沒過,請先查原因,不要 reload\n'
return 1
fi
}
# 驗證:帶方括號的要 403,一般網址不受影響(curl 不會留下任何暫存檔)
check_block() {
local domain="$1"
curl -sS -o /dev/null -w '帶方括號的 mcs:HTTP %{http_code}\n' --max-time 20 "https://${domain}/?mcs=%5Btest%5D"
curl -sS -o /dev/null -w '一般網址:HTTP %{http_code}\n' --max-time 20 "https://${domain}/"
}
# 套用(把使用者與網域換成你的)
block_mcs "admin" "example.com"
check_block "example.com"
# 外掛更新完成後,把規則拆掉,並再驗證一次
unblock_mcs "admin" "example.com"
check_block "example.com"
預期輸出範例:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful 已套用:example.com 帶方括號的 mcs:HTTP 403 一般網址:HTTP 200 (更新完成、解除規則後) 已解除:example.com 帶方括號的 mcs:HTTP 200 一般網址:HTTP 200
- 🟢 套用後帶方括號回 403、一般網址回 200 👉 規則生效。
- 🟠 兩個都是 403 👉 規則寫太寬或與別的設定衝突,立刻執行 unblock_mcs。⚠️ 這個函式會刪檔,但只刪我們自己建的那兩個。
- 🟠 nginx -t 報錯 👉 函式已自動撤回,請保留錯誤訊息給維運人員。
- 🟡 拆除後帶方括號的請求回到 200 或 404 👉 確認已拆乾淨;若還是 403,檢查那兩個檔案是否真的被刪除。
🧑🔧 DevOps 小知識:這條規則擋不到什麼?
它只看網址參數,如果有別的入口不經過 mcs,這條規則看不到。依來源報告,目前已知的入口是這個參數,所以方向對,但我沒有實際打過、也沒驗證過「完整擋住」,不能把它當成修補的替代品【❓待確認】。前面若有 CDN 或 WAF,規則要同步放到那一層才有意義 🧩
🚀 ㊃ C. 正式修補:備份 👉 預覽 👉 更新 👉 驗證
為什麼這樣做:My Calendar 是 WordPress.org 上的外掛,WP-CLI 的 plugin update 可以直接更新,plugin verify-checksums 能拿官方檢查碼比對外掛檔案有沒有被動過。單一網站最穩的做法是做成一個獨立腳本:先備份、再預覽、需要你輸入 y 才更新、更新後驗證。腳本用 heredoc 建立,結束標記用單引號包住,裡面的 $ 不會被提前展開,建好後立刻做語法檢查。千萬不要為了「保險」而降版,降回舊版等於把洞重新打開 📦
cat > /usr/local/sbin/mc-patch.sh <<'MC_PATCH_EOF'
#!/usr/bin/env bash
# mc-patch.sh:單一網站的 My Calendar 修補流程(備份 → 預覽 → 確認 → 更新 → 驗證)
# 用法:mc-patch.sh 網站使用者 網域
set -euo pipefail
PLUGIN="my-calendar"
WPCLI="$(command -v wp || command -v wp-cli || true)"
if [ -z "$WPCLI" ]; then
echo "❌ 找不到 wp 或 wp-cli" >&2
exit 1
fi
if [ "$#" -ne 2 ]; then
echo "用法:$0 網站使用者 網域" >&2
exit 2
fi
SITE_USER="$1"
SITE_DOMAIN="$2"
[[ "$SITE_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] || { echo "❌ 使用者名稱格式不對" >&2; exit 2; }
[[ "$SITE_DOMAIN" =~ ^[A-Za-z0-9.-]+$ ]] || { echo "❌ 網域格式不對" >&2; exit 2; }
WP_PATH="/home/${SITE_USER}/web/${SITE_DOMAIN}/public_html"
if [ ! -f "${WP_PATH}/wp-config.php" ]; then
echo "❌ 找不到 ${WP_PATH}/wp-config.php" >&2
exit 1
fi
wpx() { sudo -u "$SITE_USER" -H -- "$WPCLI" --path="$WP_PATH" "$@"; }
version_lt() {
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]
}
# 0=已在修補版本線上,1=受影響
is_patched() {
local v="$1" branch fixed
if version_lt "$v" "3.4"; then
return 1
fi
branch="$(printf '%s' "$v" | cut -d. -f1,2)"
case "$branch" in
3.4) fixed="3.4.25" ;;
3.5) fixed="3.5.22" ;;
3.6) fixed="3.6.18" ;;
3.7) fixed="3.7.18" ;;
3.8) fixed="3.8.8" ;;
*) return 0 ;;
esac
! version_lt "$v" "$fixed"
}
ver="$(wpx plugin get "$PLUGIN" --field=version 2> /dev/null)" || {
echo "⚪ ${SITE_DOMAIN} 沒有安裝 ${PLUGIN},不需處理"
exit 0
}
if is_patched "$ver"; then
echo "🟢 ${SITE_DOMAIN} 目前 ${ver},已在修補版本線上,略過更新,只做驗證"
else
echo "🔴 ${SITE_DOMAIN} 目前 ${ver},受影響,開始處理"
BACKUP_DIR="${HOME}/wp-security-backup"
STAMP="$(date +%Y%m%d-%H%M%S)"
DB_FILE="${BACKUP_DIR}/${SITE_DOMAIN}_db_${STAMP}.sql"
install -d -m 700 -- "$BACKUP_DIR"
( umask 077; wpx db export - --single-transaction > "$DB_FILE" )
if [ ! -s "$DB_FILE" ]; then
echo "❌ 資料庫備份是空檔,停止,沒有進行任何更新" >&2
exit 1
fi
if ! tail -n 1 -- "$DB_FILE" | grep -q 'Dump completed'; then
echo "⚠️ 備份檔結尾沒有 Dump completed 字樣,請人工確認後再繼續"
fi
echo "✅ 資料庫已備份:${DB_FILE}"
echo "--- 預覽(不會真的更新)---"
wpx plugin update "$PLUGIN" --dry-run
read -r -p "確認要更新 ${SITE_DOMAIN} 嗎?(y/N):" ans
if [ "${ans:-}" != "y" ]; then
echo "已取消,沒有任何變更"
exit 0
fi
wpx plugin update "$PLUGIN"
ver="$(wpx plugin get "$PLUGIN" --field=version)"
if ! is_patched "$ver"; then
echo "❌ 更新後版本是 ${ver},仍不在修補版本線上,請把輸出交給維運人員" >&2
exit 1
fi
fi
echo "--- 驗證 ---"
wpx plugin verify-checksums "$PLUGIN"
wpx cache flush || true
home_url="$(wpx option get home)"
code="$(curl -sSL -o /dev/null -w '%{http_code}' --max-time 20 "${home_url%/}/" || true)"
case "$code" in
2*|3*) echo "✅ ${SITE_DOMAIN}:版本 ${ver}、首頁 HTTP ${code}" ;;
*) echo "❌ ${SITE_DOMAIN}:首頁 HTTP ${code:-無回應},請立刻檢查" >&2; exit 1 ;;
esac
MC_PATCH_EOF
chown root:root /usr/local/sbin/mc-patch.sh
chmod 750 /usr/local/sbin/mc-patch.sh
bash -n /usr/local/sbin/mc-patch.sh && echo "語法檢查通過"
驗證(確認腳本建好了):最後一行預期印出「語法檢查通過」。沒有這行就代表 heredoc 內容有缺漏,用 ls -l /usr/local/sbin/mc-patch.sh 看檔案是否存在,再重新貼一次整段 🧪
使用方式:一次一站,先處理一站、確認前後台正常,再處理下一站。把第五章盤點出的 🔴 站,逐一用「使用者、網域」當參數跑:
/usr/local/sbin/mc-patch.sh admin example.com
預期輸出範例:
🔴 example.com 目前 3.8.7,受影響,開始處理 ✅ 資料庫已備份:/root/wp-security-backup/example.com_db_20261016-111500.sql --- 預覽(不會真的更新)--- +-------------+-------------+-------------+-----------+ | name | old_version | new_version | status | +-------------+-------------+-------------+-----------+ | my-calendar | 3.8.7 | 3.8.8 | available | +-------------+-------------+-------------+-----------+ 確認要更新 example.com 嗎?(y/N):y Success: Updated 1 of 1 plugins. --- 驗證 --- Success: Verified 1 of 1 plugins. ✅ example.com:版本 3.8.8、首頁 HTTP 200
- 🟢 看到最後的 ✅ 👉 這一站完成。接著手動清一次網站快取與 CDN 快取,並實際開一個行事曆頁面看一眼。
- 🟠 備份失敗 👉 腳本直接停下,沒有進行任何更新,先查磁碟空間與資料庫權限。
- 🟠 更新後版本仍不在修補版本線上 👉 更新來源沒生效,把輸出交給維運人員,不要反覆硬跑。
- 🟠 verify-checksums 出現 File was added 👉 外掛資料夾裡多了官方沒有的檔案,逐一確認,不確定就視為可疑,回到 A。
- 🔴 更新後網站白畫面 👉 立刻停用外掛,再用備份還原。
- 🟡 預覽時沒有列出可用更新 👉 不代表已是最新,以最後的版本判定為準;也要確認主機能連到 WordPress.org,以及環境是否符合 WordPress 6.5、PHP 7.4 的最低要求。
🚨 驗證的侷限:
verify-checksums 只能證明外掛目錄裡的檔案和官方發布版一致,檢查不到 mu-plugins、uploads、資料庫裡的異常帳號,也看不出別的外掛有沒有被動過。所以它只是完整性檢查的其中一環,必須搭配第五章 ㊃ 的帳號與檔案稽核。另外,如果這一站已經中招,更新前的備份本身就含有後門,不能當成乾淨的還原來源 🔐
| 情境 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| 疑似中招(A) | incident_one 路徑 | 只對有疑慮的站逐一執行 | 對網路根目錄跑一次,腳本會連子站一起停用 |
| 短期應急(B) | 停用或 block_mcs | 逐站套用 | 停用加 –network |
| 正式修補(C) | mc-patch.sh 使用者 網域 | 一次一站,確認後再下一站 | 外掛檔案只有一份,更新一次即可,再逐子站驗證 |
🔥 一句話總結:「批次」不等於把所有東西塞進一條迴圈。真正安全的做法是先盤點、再備份、預覽,確認後逐站更新,最後驗證,而且每一步失敗就停下 🎯
🔒 第七章:補完洞之後,把第二道門也鎖上
更新是根本,加固是「萬一下一顆漏洞又來,讓損害小一點」。要先把話說在前面:下面這招 disable_functions 擋不住這顆 CVE 本身,這顆漏洞改的是帳號資料,不需要執行系統指令。它擋的是下一步:攻擊者拿到管理員之後,想靠 PHP 呼叫系統指令的路。屬於通用加固,不是官方要求 🛡️
為什麼這樣做:清單刻意不含 curl_exec,因為 WordPress 的對外連線要靠它,停掉會讓外掛更新與排程出問題;也不要寫 eval,它是 PHP 的語言結構,不是函式,寫進去沒有效果。備份、圖片處理類外掛有時會用到 exec,改完請實際測前後台。腳本先備份 php.ini,用 sed -i -E 替換設定行,找不到那一行才用 tee -a 補上,所以跑幾次都不會疊加;設定檢查通過才重啟,沒過就自動還原 ⚠️
# 對主機上每一個 PHP-FPM 版本做「可重複執行」的替換(跑幾次都不會疊加)
harden_php_ini() {
local ini ver bak line
line='disable_functions = exec,passthru,shell_exec,system,proc_open,popen'
while IFS= read -r -d '' ini <&3; do
ver="$(basename -- "$(dirname -- "$(dirname -- "$ini")")")"
bak="${ini}.BAK.$(date +%F_%H%M%S)"
cp -- "$ini" "$bak"
sed -i -E "s/^[;#]*[[:space:]]*disable_functions[[:space:]]*=.*/${line}/" "$ini"
# 檔案裡本來就沒有這一行的話,補在最後
if ! grep -q '^disable_functions' "$ini"; then
printf '\n%s\n' "$line" | tee -a "$ini" > /dev/null
fi
# 設定檢查通過才重啟;沒過就還原備份
if "php-fpm${ver}" -t > /dev/null 2>&1; then
systemctl restart "php${ver}-fpm"
printf 'PHP %s:已套用並重新啟動\n' "$ver"
else
cp -- "$bak" "$ini"
printf 'PHP %s:設定檢查沒過,已還原備份:%s\n' "$ver" "$bak"
fi
grep -H '^disable_functions' "$ini"
done 3< <(find /etc/php -maxdepth 3 -type f -path '*/fpm/php.ini' -print0)
}
harden_php_ini
# 檢查個別網域的 pool 設定有沒有另外覆蓋(沒輸出代表沒覆蓋)
grep -H 'disable_functions' /etc/php/*/fpm/pool.d/*.conf
預期輸出範例:
PHP 8.4:已套用並重新啟動 /etc/php/8.4/fpm/php.ini:disable_functions = exec,passthru,shell_exec,system,proc_open,popen
最後那行 grep … pool.d/*.conf 沒有輸出,代表沒有個別網域的 pool 設定在覆蓋它,這是好事。出現「設定檢查沒過,已還原備份」代表設定沒套用、網站不受影響,把輸出貼給維運人員;要復原就把 .BAK.時間 備份檔蓋回 php.ini 再重啟 PHP-FPM。
驗證加固真的有效:光看設定檔不夠,要實際從外面打一次。下面的探針用隨機檔名,內容只回報 exec 能不能用,測完立刻刪除。預期回應是 EXEC-DISABLED;若是 EXEC-AVAILABLE,代表這個網站實際用的 PHP-FPM 版本不是你剛改的那一個,或被別處覆蓋。探針只存在幾秒,請確認最後印出「探針已移除」🧪
# 驗證 disable_functions 真的生效:放一個「隨機檔名」的無害探針,從外面打一次,馬上刪掉
probe_disabled() {
local wp_path="$1" domain name probe out
domain="$(site_domain "$wp_path")"
name="__probe_$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n').php"
probe="${wp_path}/${name}"
cat > "$probe" <<'EOF'
<?php
echo function_exists('exec') ? 'EXEC-AVAILABLE' : 'EXEC-DISABLED';
EOF
out="$(curl -sS --max-time 20 "https://${domain}/${name}" || true)"
rm -f -- "$probe"
printf '探針回應:%s\n' "${out:-(無回應)}"
if [ -e "$probe" ]; then
printf '⚠️ 探針檔案還在,請手動刪除:%s\n' "$probe"
else
echo "探針已移除"
fi
}
probe_disabled "/home/admin/web/example.com/public_html"
- 🟢 回應 EXEC-DISABLED、探針已移除 👉 加固生效。
- 🟠 回應 EXEC-AVAILABLE 👉 查這個網域實際用哪個 PHP 版本,並檢查 pool 設定有沒有覆蓋。
- 🟠 印出「探針檔案還在」 👉 手動刪掉那個檔案,別留在網站根目錄。
- 🟡 沒有回應 👉 可能是憑證、防火牆或網域解析問題,先確認網站本身連得上。
🧑🔧 DevOps 小知識:open_basedir 不用你再全域設定
有些教學建議在 php.ini 全域寫死 open_basedir,但寫死某個網站的路徑,其他網站就全壞。HestiaCP 的 PHP-FPM 樣板本來就會為每個網域設好,用 grep -H open_basedir /etc/php/*/fpm/pool.d/*.conf 確認即可。它同樣擋不住這顆 CVE,只是縮小事後的傷害範圍 🔒
- ⭐ 開自動更新:這顆漏洞公開到被掃描的時間窗口很短,自動更新能把「有空再更新」的空窗壓到最小。代價是偶爾要處理更新後的相容性問題,所以重要網站建議搭配每日備份。
- ⭐ 不用的外掛直接刪:停用只是暫時離開攻擊面,刪除才是永久離開。
- ⭐ 管理員變動要有人知道:這次的受害目標是管理員帳號,所以「管理員信箱或密碼被改」這件事,最好有通知機制,而不是等你登不進去才發現。常見資安外掛有提供這類通知,可以評估是否啟用【❓待確認:依你使用的外掛而定】。
🩵 荷包試算:要不要花錢買 WAF 或虛擬修補?
付費防火牆可以在你更新前先擋一層,但這顆漏洞在後台點一下「更新」就解決,今天能更新,通常比月付方案划算。真的卡在相容性、短期無法更新時,再把付費防護當過渡手段,別反過來拿它當不更新的理由 💰
🤔 第八章:讀到快喘不過氣?常見焦慮與一個通用心法
- 🌠 Q:我已經是 3.8.8 了,就安全了嗎?
A:就這顆 CVE 來說,是的,那是官方修補版。但如果舊版曾對外暴露,更新只能擋住下一次,擋不了已經發生的事,所以還是建議做完第五章的帳號與檔案稽核 📦 - 🌟 Q:我用的是 3.7.x,要不要直接升到 3.8?
A:不一定要。3.7.18 是官方給 3.7 線的修補版,裝到它就脫離這顆漏洞。要不要跨線升級,取決於你的相容性測試,我無法替你的環境保證 🧭 - ✨ Q:我沒有行事曆頁面,還需要處理嗎?
A:需要。外掛啟用就有風險,不用的外掛請直接刪除 🙅 - 🌠 Q:看不懂那些指令,是不是沒救了?
A:不會。第三章全程用滑鼠就能完成最基本的處理,第四章以後是給有 SSH 權限的人看的補充 🤗 - ✨ Q:被改的只有 ID 1,其他管理員帳號就沒事嗎?
A:依來源報告,直接受害的是 ID 1。但若 ID 1 是管理員且已被接管,攻擊者就有能力動別的帳號,所以管理員名單要整份看,不是只看第一個 🔍
🧩 舉一反三:「把外部字串當函式名稱」這一類漏洞怎麼防
以下是通用防禦知識,不是本 CVE 的官方要求,但下次看到類似新聞能對號入座 🫡
- ⭐ 函式名稱不能由外部決定。只要程式會依字串去呼叫函式,就該先比對一張寫死的白名單,名單外的一律拒絕。
- ⭐ 網址參數也是外面來的資料。很多人只防表單,卻忘了網址列、Header、Cookie 都能被任意填寫。
- ⭐ 敏感操作要有身分檢查,而且放在最靠近動作的地方。就算前面有一層過濾,真正改帳號的那一步也應該再問一次「你是誰、你有沒有資格」。
- ⭐ 修補與事件應變是兩件事。舊版只要曾暴露在網路上,更新完還要查日誌、查帳號、查檔案,缺一不可。
🏆 第九章:這次到底該多緊張?事件綜合評估
| 評估維度(10 分制,數字越高代表風險或應對品質越正向) | 分數 | 一句話評語 |
|---|---|---|
| 漏洞嚴重程度(越高代表風險越大) | 9.8 | 免登入、免互動、網址就能觸發,可能直接奪走第一位管理員的帳號,是最危險的那一檔 |
| 官方應變速度 | 8 | 五條版本線都補了版,是加分項;確切上架日期我沒能核對,且近兩年同類授權問題反覆出現,略扣分 🟡 |
| 一般站長自救可行性 | 8 | 後台點「更新」就能修補,門檻低;扣分在於要多做「看管理員名單、登不進去別按忘記密碼」這些事後檢查 |
| DevOps 技術文件完整度 | 5 | 官方沒公開程式差異,觸發條件也不完整,排查只能靠「mcs 帶方括號」與「ID 1 有沒有被動過」這類結果特徵【❓待確認】 |
| 🏆 加權綜合建議 | 立即更新 | 最終建議:【今天內更新到對應版本線的修補版;舊版曾暴露就做第五章稽核,疑似中招走第六章 A】 |
🔥 這次提醒我們:漏洞最危險的時候,不是被發現的當天,而是「已經公開、你還沒更新」的那段空窗。攻擊字串沒公開,不代表安全,因為真正有差別的是,你有沒有在別人摸清楚之前把門關好。與其等到後台登不進去才發現不對,不如現在花五分鐘,打開外掛頁看一眼版本號 🔢
📚 參考資料與延伸資源
- 📌 WordPress.org|My Calendar – Accessible Event Manager 外掛頁與更新日誌 [1]
- 📌 Wordfence|My Calendar 漏洞資料庫(頁面需要 JavaScript) [42]
- 📌 Patchstack|My Calendar 漏洞列表 [43]
- 📌 OpenCVE|CVE-2023-23813(≤3.4.3 CSRF) [44]
- 📌 CVE 紀錄|CVE-2025-67592(≤3.6.16 缺少授權) [45]
- 📌 OpenCVE|CVE-2026-11896(≤3.7.14 不安全的物件參照) [46]
- 📌 CVE-DEV|CVE-2026-6854(≤3.7.8 SQL 注入) [47]
- 📌 WP-CLI 官方文件|wp plugin update [48]
- 📌 WP-CLI 官方文件|wp plugin verify-checksums [49]
- 📌 WP-CLI 官方文件|wp config shuffle-salts [50]
- 📌 WP-CLI 官方文件|wp user reset-password [51]
- 📌 WP-CLI 官方文件|wp user session [52]
- 📌 HestiaCP 官方文件|CLI 參考 [53]











