🔑 你的「用 X 登入」按鈕,可能正替陌生人開管理員後門?Jupiter X Core 認證繞過漏洞自救全攻略(CVE-2026-106013/CVSS 9.8)
🔥 懶人包:
WordPress.org 上有 70,000+ 活躍安裝的 Jupiter X Core 外掛(它是 Jupiter X 佈景主題的搭檔外掛),被登記了 CVE-2026-106013,CVSS 3.1 評分 9.8,弱點類型是不當驗證(CWE-287)。問題出在「用 X(前身 Twitter)帳號登入」這條流程:外掛向 X 換到訪客的憑證後,沒有把憑證換到連線物件上,後面查身分時,用的可能還是網站設定裡那組 X 應用程式擁有者的憑證,網站因此可能認錯人,把登入狀態發給不該拿到的帳號 💥
版本 4.61.0(含)以下都在影響範圍,修補版是 4.62.0。先別慌:依公開資料,網站得先設定過 X 登入才有利用條件,只是「裝了外掛」不等於「一定中標」。但 CVSS 向量寫的是免帳號、免互動、攻擊難度低,所以建議當天就更新。目前我查不到實際被攻擊的可靠證據,這點標【❓待確認】,不要拿它當成可以拖著不修的理由。這篇帶你看版本、備份、更新,再查有沒有被人摸過 🔒
🌐 點我直達 WordPress 官方外掛頁確認最新版本 [1]
內容目錄
🪧 第一章:警衛為什麼把路人認成站長?一場「查錯證件」的案發現場
想像一棟辦公大樓,櫃台警衛有個標準流程:訪客遞上自己的證件,警衛刷過讀卡機,系統回報「這位是某某先生」,警衛才放行。這套流程平常沒問題。直到某天有人發現,警衛換班後忘了把讀卡機切換成「讀訪客遞來的那張卡」,讀卡機手上一直握著大樓老闆放在機器裡的那張。不管誰站在櫃台前,系統都回答「這位是老闆」,警衛便恭恭敬敬地開了門 🏢
你在網站按下「用 X 登入」,網站不會知道你的 X 密碼,而是請 X 當公證人:「這個人是誰?」X 回答後,網站再拿對方的電子郵件去比對自己的會員。這次的問題出在「請公證人查證的那一步」,依公開資料,修補前的程式沒有把訪客的憑證換上去,查到的可能是網站自己那組 X 應用程式的擁有者 🙋♀️
先把背景交代清楚。Jupiter X Core 內建 X 登入的處理程式,使用的是 OAuth 1.0a 這種「兩段式」流程:先拿一張暫時的通行證,使用者到 X 授權後,再拿著授權碼去換正式的存取憑證。正式憑證到手之後,外掛要做的事很單純:把這組新憑證設定回連線物件,再用它去問 X「這個人是誰」。有漏洞的版本漏了第一步,後面的查詢就可能沿用網站設定裡儲存的那組憑證 🧾
下面這條攻擊鏈,整理自 CVE 官方紀錄、Wordfence 說明與 WordPress.org 外掛 SVN 的修補差異(changeset 3759312)。官方沒有公開攻擊字串,我也不會寫,所以每一環都標了信心程度 🔍
- ㊀ 網站設定過 X 登入,而且流程摸得到:🟡 依公開資料整理,這是利用的前提。CVE 的評分向量把「是否需要先有網站帳號」記為不需要(PR:N),但特定網站符不符合條件,仍要看實際設定 📝
- ㊁ 外掛換到憑證後,沒有套用到連線物件:✅ 4.62.0 的修補差異,就是補上「把回傳的使用者憑證設定回去」這一行為,反推舊版缺了它 🔧
- ㊂ 後續查身分,可能拿到應用程式擁有者的資料:🟡 這是 Wordfence 描述加上程式差異做出的推論。官方沒有逐步示範,所以我標推論 🪪
- ㊃ 外掛依電子郵件比對 WordPress 使用者,並建立登入狀態:✅ 流程使用 WordPress 的 wp_set_auth_cookie()。如果比對到的是管理員,結果就是管理員被接管 🚪
🧊 冷知識:這代表「最容易中標的」,可能剛好是最重要的那個帳號
照上面的推論,被誤認的對象是「X 應用程式的擁有者」。而申請 X 應用程式的人,通常就是站長自己,用的又常是管理員的信箱。所以最有可能被誤認的帳號,多半是管理員。這是 🟡 推論,不是官方明講,但它直接影響第七章的一項加固做法 🧐
🚨 畫重點:我不會給你攻擊字串,也不教你怎麼重現
這篇只談怎麼確認自己有沒有中標、怎麼修。你要記住的只有一件事:入口是「社群登入的回呼流程」,攻擊者不需要你的帳號密碼。想知道自己中不中標,看第五章的結果導向檢查就夠了 🙅
⏱️ 事件時間軸一次看懂
| 時間點 | 發生了什麼大事 | 站長可信度判定 |
|---|---|---|
| 2026-10-06 | CVE 紀錄顯示,漏洞已通知供應商 Artbees | ✅ CVE 官方 JSON |
| 2026-10-22 | Jupiter X Core 4.62.0 上架,社群登入相關程式被修改 | 🟡 日期來自對 WordPress.org SVN 紀錄的判讀,我沒有取得第二個獨立來源【❓待確認】 |
| 2026-10-27 | Wordfence 頁面記載的漏洞公開日期 | ✅ Wordfence 頁面 |
| 2026-10-28 | CVE-2026-106013 在 CVE 官方資料庫發布,發現者署名 daroo | ✅ CVE 官方 JSON;它和 Wordfence 相差一天,我兩個日期都保留 |
| 到底有沒有人受害? | OpenCVE 顯示尚未列入 CISA 的「已知被利用漏洞」清單(KEV),EPSS 分數也未提供;我沒找到足以確認實際被利用的可靠證據 | 【❓待確認】 |
看到「沒列入 KEV」不要鬆口氣。KEV 只收「已經確認被攻擊」且達到收錄標準的漏洞,沒收錄不能當作沒人在打。CVE 才剛在今天發布,這幾天是公開資訊傳開、自動化掃描陸續跟上的階段,拿來更新剛剛好 ⏳
- ✅ 已確認事實:受影響版本 4.61.0(含)以下,修補版 4.62.0;CVSS 3.1 為 9.8;WordPress.org 外掛頁顯示 70,000+ 活躍安裝,且為免費外掛;SVN changeset 3759312 新增三項檢查(重設憑證、比對 Cookie 內的 Token、比對 X 的 user_id)。
- 🟡 合理推論:CVSS 向量 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 翻成白話:連上網路就能打、難度低、不需要帳號、不需要受害者點任何東西,影響範圍沒有跨出同一個安全範疇,但機密性、完整性、可用性全是 High。
- 🔴 假設情境:假設有一間設計工作室,用 Jupiter X 做官網,為了讓客戶方便,開了「用 X 登入」。申請 X 應用程式時,用的是站長自己的管理員信箱。這是我用來說明的例子,不是任何真實網站的紀錄。
🎭 第二章:先別自己嚇自己,對號入座看看你是哪一種網站主?
🩵 小資族荷包試算:修這個洞要花多少錢?
Jupiter X Core 外掛在 WordPress.org 免費下載,更新到 4.62.0 的費用是 NT$ 0 元。佈景主題 Jupiter X 本身的授權與價格,我沒有核對,這裡不報數字。對照事後成本:請人清後門、處理被竄改的內容、網站被搜尋引擎標記後的流量損失,隨便一項都比點一下更新貴 💸
同一封資安警告,小編、自己租主機的玩家、企業資訊人員要做的事差很多。先找到自己,再決定往下讀到哪一章 🧭
🙋 我只是負責發文、顧後台的小編或老闆
你不需要懂程式。第三章全程用滑鼠:看版本、備份、按更新,再確認有沒有用到 X 登入。這個外掛在 WordPress.org 上,後台一般就能直接更新,不需要另外輸入授權碼 🧑💼
👨💻 我自己租主機架站,手上有好幾個客戶網站
直接跳第五章拿工具箱。它會用各網站擁有者的身分跑 WP-CLI,不會在 root 底下留下一堆權限錯亂的檔案。這顆漏洞只有一條版本門檻(低於 4.62.0),判斷比「每條版本線各有修補版」的漏洞單純很多 🔧
🧑🔧 DevOps 小知識:為什麼用 plugin update,而不是自己覆蓋檔案?
外掛掛在 WordPress.org,wp plugin update 能直接更新,wp plugin verify-checksums 也能拿官方檢查碼比對檔案有沒有被動過。手動上傳 zip 覆蓋,等於繞開這兩道保險,還可能留下舊版殘檔 🧱
🏢 我們是企業網站,存了會員與訂單資料
免費外掛沒有服務水準協議(SLA),出事時誰也不保證多久修好。這次公開資料顯示,修補版在漏洞公開前就已上架,這點是加分。但如果網站有開 X 登入、舊版又曾暴露過,就要把它當成「可能被進入過」的等級處理。個資是否外洩、要不要依《個人資料保護法》通知當事人,請交給公司法務判斷。此為資訊統整,非專業建議,重大決定前請諮詢真人專家 🩺
🚨 冤枉啊!我早就不用 X 登入了,只是懶得關
某個工作室兩年前為了一檔活動,在登入頁加了「用 X 登入」,活動結束後把按鈕從頁面上拿掉,X 應用程式的金鑰卻還留在後台設定裡,外掛也沒更新。管理員心想:「按鈕都沒了,總不能憑空打進來吧?」
我查到的資料沒有說「按鈕消失就一定打不到」。2024 年這個外掛的 CVE-2024-7781 就有類似的例子:依該筆紀錄描述,社群登入的問題在功能事後被關掉、但曾經設定過的網站上仍可能成立。這是 🟡 先例,不代表這次一樣,但足夠讓我不敢賭。與其賭,不如直接更新 🆘
🚥 螢光筆時間:更新到 4.62.0 以上,今天就做
「我沒在用 X 登入」不算處理。能確定把門關上的只有一件事:版本號 ≥ 4.62.0 🙅♂️
🧮 四種情境對照:你今天該做到哪一步?
| 你的情境 | 風險判斷 | 今天要做的事 | 看哪一章 |
|---|---|---|---|
| 外掛啟用中,而且設定過 X 登入 | 最高 | 備份 → 更新 → 檢查管理員名單與檔案有沒有被動過 | 第三、五、六章 |
| 外掛啟用中,不確定有沒有設定過 X 登入 | 中偏高(無法排除) | 照樣更新,再用第五章 ㊂ 查查看 | 第三、五、六章 |
| 外掛啟用中,確定從沒設定過 X 登入 | 低(依描述前提不成立,但無法保證) | 仍然更新,並順手把用不到的社群登入關掉 | 第三、七章 |
| 外掛已停用但沒刪除 | 低 | 確定用不到就刪除,要用就更新 | 第三章 |
🧮 版本對照:你的版本安不安全?
| 產品 | 外掛代號(slug) | 受影響範圍 | ✅ 修到這版才安全 |
|---|---|---|---|
| Jupiter X Core | jupiterx-core | 4.61.0(含)以下 | 4.62.0 |
最早從哪一版開始受影響,我取得的資料沒有寫明【❓待確認】,所以請當成「低於 4.62.0 一律有風險」處理。順帶一提,版本號的比較要一段一段看:4.9.9 比 4.62.0 舊,4.100.0 比 4.62.0 新。後面的腳本用 sort -V 逐段比大小,不會被字串排序騙到 🧰
😬 更新前先看這個:真實踩過的雷
更新是對的,但別當成「按下去就一定平安」。我在 WordPress.org 支援論壇找到三個公開案例,都不是這次 CVE 造成的,放在這裡是提醒你:更新前要有備份、更新後要馬上開網站看一眼 🧪
| 案例 | 發生了什麼 | 給你的提醒 |
|---|---|---|
| 2022-04-07 論壇主題「Broke my website」(已關閉) | 發文者表示更新後 Jupiter X 的儀表板當掉,後來一次更新更讓整個網站掛掉,把外掛退回舊版才恢復。他也抱怨廠商的處理方式。串文後續有人轉貼廠商論壇的做法,發文者說照做仍有問題,串文最後沒有明確結論。 | 更新前備份,並先確認你知道怎麼退回去。這裡的「退回」指的是出狀況時的急救,這次漏洞修完之後不要退回舊版。 |
| 2022-06-01 論壇主題「Version 2.0.9 is broken」(已關閉) | 多位使用者回報 2.0.9 版因缺少必要檔案而讓網站出錯,有人退回 2.0.8。頁面上沒有看到外掛作者的回覆,後續回覆多半指向廠商論壇上的民間解法。 | 更新出狀況時,頁面上不一定有官方回應,有備份的人處理起來快很多。 |
| 2026-08-13 論壇評論「Update is overdue」(未解決) | 發文者表示新版弄壞了分類迴圈小工具,也拿掉部分客製功能,他提到之前客服答應的更新時程沒有兌現,頁面上沒有廠商回覆。 | 更新後請逐一檢查用到的小工具與版型。這是單一使用者的說法,我沒辦法驗證。 |
🚨 更新出問題時,優先「找原因」,不要「降版本」
把外掛降回 4.61.0 以下,等於重新打開漏洞。正式站來不及處理,就先走第六章 B(停用或關閉 X 登入),用備份到測試站慢慢查 🧯
🛟 第三章:電腦小白看過來!五分鐘無痛自救指南
💬 溫馨小叮嚀:更新外掛像是把壞掉的鎖換新,它保證從現在開始新的小偷進不來,但沒辦法倒帶,檢查昨天有沒有人已經進來逛過。所以最後的「查名單」一定要做。網站如果存有會員或訂單資料,請把這篇轉給主管或維護人員,由他們判斷要不要做更深的健檢。覺得看不懂、不敢按,也不是你的錯,本來就不是每個人都該懂這些,開口請人幫忙是正確的選擇 🩺
每一步我都寫了「正常的樣子」跟「覺得怪怪的該怎麼辦」,全程只需要滑鼠 🤏
- ㊀ 先查水表,看看有沒有裝:
登入後台,左邊選單點「外掛」→「已安裝的外掛」,在右上角搜尋框輸入 Jupiter。找到「Jupiter X Core」就是這次的主角。注意:Jupiter X 佈景主題是另一個東西,這次公告的是 Core 外掛,主題本身的版本不在這則 CVE 的範圍內。找不到 Jupiter X Core,這篇就可以略過了,當作別人家的新聞 🔎 - ㊁ 核對版本號:
版本號寫在外掛名稱下方。看到 4.62.0 或更高就是已修補;比它低就是受影響,繼續往下做 🕵️♀️ - ㊂ 動手前先留後路(備份):
到主機商後台點「一鍵備份」,或用你平常的備份外掛先存一份。不知道什麼是備份的話,把這篇網址傳給幫你維護網站的人,請他代勞,這本來就是他的工作。我查到的公開案例裡,更新出狀況時能快速救回來的,都是事前有備份的人 💾 - ㊃ 按下更新:
在外掛清單的 Jupiter X Core 那一列點「立即更新」。更新完重新整理頁面,確認版本號跳到 4.62.0 以上。如果你看不到更新按鈕,先到後台「儀表板 → 更新」,按一下「再檢查一次」,因為 WordPress 的更新檢查有快取,不一定即時。仍然沒有的話,把畫面截圖給維護人員 🔄 - ㊄ 更新完,馬上開網站看一眼:
用另一個瀏覽器視窗(最好是沒登入的無痕視窗)開首頁、一個內頁、登入頁,確認版面沒跑掉。論壇案例顯示,這個外掛更新後出狀況,常見是版面或小工具壞掉。有問題先不要降版,往下看第六章 B,再交給維護人員 🧪 - ㊅ 開啟自動更新(建議):
回到「已安裝的外掛」,在 Jupiter X Core 那一列右側,找「啟用自動更新」的連結點下去。好處是下次再出安全修補,不必等你有空;代價是偶爾會碰到更新後相容性問題,所以有備份的前提下才建議開 🤖 - ㊆ 找出網站有沒有設定過 X 登入:
這是判斷「你有沒有被盯上」的關鍵。做兩件事:一是打開你的登入頁、註冊頁,看有沒有「用 X 登入」或 Twitter 圖示的按鈕;二是進 Jupiter X 的設定頁面,找找有沒有 Twitter/X 的 API 金鑰、密鑰這類欄位。選單名稱會因版本不同而有差異【❓待確認:我沒有逐版核對】,找不到也不要硬猜,把這個問題交給維護人員,用第五章 ㊂ 的指令查。確定用不到的社群登入,就請維護人員一起關掉,少一個入口就少一分風險 🔍 - ㊇ 化身名偵探,查查後台有沒有陌生人:
點「使用者」→「管理員」分頁,裡面應該都是你認識的人、用你們公司的信箱。再回到「已安裝的外掛」,看有沒有你從沒裝過的外掛名稱 🕵️♀️ 見鬼的情況:多出沒印象的管理員、陌生外掛,或網站首頁出現奇怪的廣告與導向。看到這類狀況請不要自己刪,先截圖,立刻找主機商或維護人員,並告訴他們依本文第六章 A 處理 🔀 - ㊈ 今天真的更新不了,怎麼辦:
最乾淨的做法是把 X 登入關掉(做法同 ㊆)。如果你連關都不確定怎麼關,最後手段是把外掛「停用」。但請注意:Jupiter X 佈景主題很依賴這個外掛,停用後網站很可能整個報錯或版面全亂。停用後馬上打開首頁看一眼;掛了就重新啟用,並在今天內排好更新時間。這只是縮短風險時間,不是修補 🧯
🫂 新手求助專區:看到專有名詞就頭暈怎麼辦?
直接把下面這段話傳給你的主機客服或維護人員:「你好,我的網站有安裝 Jupiter X Core 外掛,請幫我確認版本是否已更新到 4.62.0 以上,漏洞編號是 CVE-2026-106013。也請協助檢查網站有沒有設定過 Twitter/X 登入,以及後台管理員名單有沒有異常。」對方看到這串編號就知道該做什麼。沒有備份之前,千萬不要自己亂刪東西 🤗
🧊 冷知識:「停用」跟「刪除」差在哪?
想像家裡有一桶瓦斯。「停用」是把閥門關起來,桶子還放在原位,需要時轉開就能用,也代表它還在,哪天誰不小心轉開閥門,問題就回來了。「刪除」是把整桶搬出家門。很久不用的外掛,搬走比關閥安全;還在用的,就老實更新 🧰
🤿 第四章:技術深潛,一次「忘了換鑰匙」怎麼變成管理員登入?
想知道為什麼的人往下看,只想補洞的前三章已經夠用。先講資料狀況:官方沒有公開攻擊細節,下面的機制整理自 CVE 官方紀錄、Wordfence 描述與 WordPress.org 外掛 SVN 的修補差異,推論的部分我都標了 🟡,查不到的標【❓待確認】⚔️
| 評估指標 | 具體資訊 |
|---|---|
| 外掛名稱 | Jupiter X Core(廠商 Artbees) |
| 外掛代號(slug) | jupiterx-core |
| CVE 編號 | CVE-2026-106013 |
| 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-287:不當驗證(Improper Authentication) |
| 受影響與修補版本 | 4.61.0(含)以下受影響,4.62.0 修補 |
| 發現者 | daroo(CVE 紀錄署名) |
| 公開時間 | Wordfence 2026-10-27,CVE 官方 2026-10-28 |
| 啟用規模 | WordPress.org 外掛頁顯示 70,000+ 活躍安裝 |
| 出問題的程式位置 | X(Twitter)社群登入的回呼驗證流程 verify_twitter_callback |
| 是否已被利用 | 【❓待確認】未找到足以確認的可靠證據 |
🧩 四個環節串成一條完整的鏈
- 🏮 環節一:向 X 換憑證。使用者在 X 授權後,網站拿著回傳的授權碼,去換一組「這位使用者專屬」的存取憑證。這一步是正常的 ✅
- 🏮 環節二:憑證沒有換到連線物件上。舊版程式拿到憑證後,沒有把它設定回負責跟 X 溝通的 TwitterOAuth 連線物件。4.62.0 補上的第一項檢查,就是「把使用者憑證設定回去」,所以反推舊版缺了這一行 ✅
- 🏮 環節三:查身分時沿用了網站自己的憑證。連線物件手上還是網站設定中儲存的 Access Token 與 Access Token Secret,所以向 X 查「目前登入的是誰」,回答的可能是應用程式擁有者,而不是眼前的訪客 🟡
- 🏮 環節四:依電子郵件找人、發登入狀態。外掛拿查到的電子郵件對應 WordPress 使用者,再用 wp_set_auth_cookie() 建立登入狀態。被對應到的是管理員,就等於管理員被接管 🎯
4.62.0 加了三項檢查:第一,把訪客遞來的證件正確放上讀卡機;第二,確認回呼帶回來的通行證,跟一開始發給這位訪客、記在他 Cookie 裡的那張是同一張;第三,確認讀卡機查到的 X 使用者編號,跟換證件時拿到的編號一致。三道關卡缺一不可,少了任何一道,警衛都可能被騙 📋
🧑🔧 DevOps 小知識:CVSS 9.8 是怎麼來的?
AV:N 網路可達、AC:L 低複雜度、PR:N 不需權限、UI:N 不需使用者互動、S:U 範圍未改變,加上 C/I/A 全為 High,剛好是這種公式下的上限 9.8。要留意的是:CVSS 描述「漏洞本身」,不會考慮你的網站有沒有開 X 登入,所以實際風險要看第五章的檢查結果 🛡️
🧊 冷知識:「裝了外掛」不等於「一定中標」,但也別因此放心
公開資料顯示,利用需要 X 登入流程已被設定。換句話說,從沒設定過的網站,攻擊面較小。但我沒辦法保證「沒設定就一定打不到」,而且同一個外掛在 2024 年也出過社群登入的認證繞過紀錄(CVE-2024-7781,CVSS 8.1,4.7.8 才完整修補),社群登入這塊在這個外掛上算是反覆出事的區域 🧐
🚨 畫重點:「完整觸發條件」目前查不到
哪些設定組合一定能被打中、需不需要其他前提,公開資料只有簡短描述。所以第五章的排查我刻意不依賴某一條攻擊字串,改看結果特徵:站上有沒有 X 登入設定、管理員名單有沒有多出人、檔案有沒有被動過。代價是會有一些雜訊,好處是不會因為猜錯入口而漏看真正的異常 🧭
🕵️ 第五章:怎麼確認你家有沒有被摸過?排查 Checklist
🤖 重要提醒:以下指令整合五份來源報告(ChatGPT、DeepSeek、Gemini、Grok、Meta AI)的做法,經 Claude 逐段複查、改寫,所有程式碼區塊都做過 bash 語法檢查,其中版本比對、修補腳本與日誌篩選另以模擬環境跑過 🫡
但你的主機環境不一定跟範例一樣。正式環境(尤其是正在營業中的網站)的高風險操作——包含更新、停用、刪除檔案、變更權限、重設密碼或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter。範例中的 /home/admin/web/example.com/public_html 與 admin 都是示範用名稱,請換成你自己的環境。另外,指令輸出可能出現網站使用者名稱、信箱與路徑等個資,貼給別人求助之前請先遮蔽 🧙
這一章的流程是:工具箱 👉 盤點版本 👉 找出 X 登入入口 👉 日誌排查 👉 帳號與檔案稽核。這一章的指令全部只讀不寫,不會改任何東西。預期輸出只是典型範例,版本不同可能略有差異 📜
環境假設:Debian 12/13 或 AlmaLinux 搭配 HestiaCP,網站放在標準的 /home/使用者/web/網域/public_html。你的環境如果不同,請先把路徑換掉 🗺️
🧰 ㊀ 先貼這一段:整章都會用到的小工具
為什麼這樣做:HestiaCP 的每個網站屬於不同的 Linux 使用者。用 root 直接跑 WP-CLI 會留下 root 擁有的檔案,之後網站自己寫入就會失敗。所以先定義幾個小函式:自動找 WP-CLI(有的主機叫 wp,有的叫 wp-cli)、以檔案擁有者身分執行、逐站迴圈,以及專門比版本號的 is_fixed。所有路徑變數都用雙引號包起來,路徑含空白也不會斷 🧱
# ㊀ 工具箱:同一個 SSH 連線貼一次就好,斷線後要重貼
PLUGINS=("jupiterx-core")
FIXED="4.62.0"
# 找出 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 runuser > /dev/null || echo "⚠️ 找不到 runuser,請改請維運人員協助"
# 列出主機上每一個 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 home_dir
shift
owner="$(site_owner "$wp_path")" || return 1
home_dir="$(getent passwd "$owner" | cut -d: -f6)"
runuser -u "$owner" -- env HOME="${home_dir:-/tmp}" "$WPCLI" --path="$wp_path" "$@" < /dev/null
}
# 逐站執行:任何一站失敗就停下,不會連鎖出事
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 舊(用 sort -V,不用字串比大小)
version_lt() {
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]
}
# 這顆 CVE 只有一道門檻:低於 4.62.0 就受影響
is_fixed() { ! version_lt "$1" "$FIXED"; }
預期輸出:貼完沒有任何輸出就是成功。看到 ⚠️ 代表缺 WP-CLI 或 runuser,先補上再往下,不要硬跑。
驗證(確認 is_fixed 判斷正確):貼完後跑下面這段,它只做版本判斷,不碰任何網站 🧪
for v in 4.61.0 4.62.0 4.62.1 4.100.0 4.9.9; do if is_fixed "$v"; then echo "$v 已修補"; else echo "$v 受影響"; fi done
預期輸出(4.61.0 與 4.9.9 要是「受影響」,其餘「已修補」):
4.61.0 受影響 4.62.0 已修補 4.62.1 已修補 4.100.0 已修補 4.9.9 受影響
- 🟢 結果符合預期 👉 工具箱沒貼錯,往下進行。
- 🟠 出現 command not found 👉 函式沒貼成功,重新貼一次第一段。
- 🟡 4.100.0 被判成已修補、4.9.9 被判成受影響是刻意的:用 sort -V 逐段比數字,不是比字串。
🧑🔧 DevOps 小知識:為什麼迴圈長成 3< <(find …) 這個樣子?
常見寫法 find | while read 有個陷阱:迴圈裡只要有指令會讀標準輸入(WP-CLI 就會),就會把清單吃掉一半,結果只處理到第一個網站。改成把 find 的輸出接到檔案描述符 3、用 read … <&3 讀,迴圈內的指令就搶不到輸入;搭配 -print0 與 read -d ”,路徑含空白也安全。wpx 裡的 < /dev/null 是第二道保險 🔧
🔎 ㊁ 盤點版本:哪些站裝了它、現在哪一版?
為什麼這樣做:一台主機上有十個獨立 WordPress,跟一個 Multisite 底下有十個子站,看起來像但處理方式不同:前者要逐一找每個 wp-config.php,後者外掛檔案全網只有一份。這個函式同時判斷安裝類型,輸出一行一個結果,只讀不寫 🤔
inventory_one() {
local wp_path="$1" kind slug ver status verdict found=0
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-\t-\t%s\n' "$wp_path"
return 0
fi
for slug in "${PLUGINS[@]}"; do
ver="$(wpx "$wp_path" plugin get "$slug" --field=version 2> /dev/null || true)"
[ -n "$ver" ] || continue
found=1
status="$(wpx "$wp_path" plugin get "$slug" --field=status 2> /dev/null || true)"
if is_fixed "$ver"; then verdict="🟢 已修補"; else verdict="🔴 受影響"; fi
printf '%s\t%s\t%s\t版本:%s\t狀態:%s\t%s\n' \
"$verdict" "$kind" "$slug" "$ver" "${status:--}" "$wp_path"
done
if [ "$found" -eq 0 ]; then
printf '⚪ 未安裝\t%s\t-\t-\t-\t%s\n' "$kind" "$wp_path"
fi
return 0
}
# 💠 單一網站(路徑換成你的)
inventory_one "/home/admin/web/example.com/public_html"
# ❇️ 獨立多站:整台主機一次盤點(只讀不寫)
each_site inventory_one
預期輸出範例:
🔴 受影響 💠 一般安裝 jupiterx-core 版本:4.61.0 狀態:active /home/admin/web/site-a.com/public_html 🟢 已修補 💠 一般安裝 jupiterx-core 版本:4.62.0 狀態:active /home/bob/web/shop.example/public_html ⚪ 未安裝 💠 一般安裝 - - - /home/bob/web/blog.example/public_html 🟢 已修補 ✴️ Multisite jupiterx-core 版本:4.62.0 狀態:active-network /home/bob/web/network.example/public_html
分支判斷邏輯:
- 🔴 受影響 👉 排入第六章的備份與更新,並接著做 ㊂ 到 ㊄,確認舊版時期有沒有被碰過。
- 🟢 已修補 👉 版本條件已解除,但舊版若曾暴露一段時間,仍建議做 ㊂ 到 ㊄。
- 🟡 狀態是 inactive 👉 外掛已停用,攻擊面暫時不在,仍建議更新或刪除,不要放著。
- ✴️ Multisite 出現 active-network 👉 外掛檔案全網共用,查主站一次就代表整個網路,更新也只做一次。
- 🟠 出現 ❓ 載入失敗 👉 先排查 PHP 或資料庫連線,不要直接對這站動手。路徑若像 public_html.bak 之類的備份副本,請人工確認哪一個才是正式站。
🔐 ㊂ 找出 X 登入的入口:這顆漏洞的前提到底成不成立
為什麼這樣做:依公開資料,要打中這顆漏洞,網站得先有 X 登入的設定。所以比起大海撈針找攻擊字串,先弄清楚「你家有沒有這扇門」更實際。這段做三件事:列出資料庫設定裡名稱含 twitter 的項目(只印出名稱,不印內容,因為內容可能就是金鑰)、確認外掛裡處理 X 登入的程式檔在不在、數一下該檔案裡有沒有出現 setOauthToken。最後這項是我的推測:修補版新增了「把憑證設定回去」的動作,檔案裡出現這個字串,較像已含修補;沒有,較像舊版。這只是輔助判斷 🟡,最終以第五章 ㊁ 的版本號為準。另外,X 的金鑰也可能存在 Elementor 表單元件的設定裡,指令查不到,這一點【❓待確認】,所以結果為空時,請搭配第三章 ㊆ 的後台檢查 🧐
xlogin_one() {
local wp_path="$1" plugin_dir handler cnt
plugin_dir="${wp_path}/wp-content/plugins/jupiterx-core"
printf '\n===== %s =====\n' "$(site_domain "$wp_path")"
echo "--- ❶ 資料庫設定裡名稱含 twitter 的項目(只列名稱,不印內容)---"
wpx "$wp_path" option list --search='*twitter*' --fields=option_name --format=table || true
echo "--- ❷ 外掛裡負責 X 登入的程式檔在不在 ---"
find "$plugin_dir" -type f -name 'twitter.php' -path '*social-login-handler*' 2> /dev/null
echo "--- ❸ 該檔案有沒有出現 setOauthToken(有 = 較像已含修補;沒有 = 較像舊版)---"
handler="$(find "$plugin_dir" -type f -name 'twitter.php' -path '*social-login-handler*' 2> /dev/null | head -n 1)"
if [ -n "$handler" ]; then
cnt="$(grep -c 'setOauthToken' -- "$handler" || true)"
printf '%s 出現 %s 次\n' "$(basename -- "$handler")" "${cnt:-0}"
else
echo "找不到檔案(外掛可能沒裝,或目錄結構與預期不同)"
fi
return 0
}
# 💠 單一網站
xlogin_one "/home/admin/web/example.com/public_html"
# ❇️ 獨立多站
each_site xlogin_one
預期輸出範例:
===== example.com ===== --- ❶ 資料庫設定裡名稱含 twitter 的項目(只列名稱,不印內容)--- --- ❷ 外掛裡負責 X 登入的程式檔在不在 --- /home/admin/web/example.com/public_html/wp-content/plugins/jupiterx-core/includes/extensions/raven/includes/modules/forms/classes/social-login-handler/twitter.php --- ❸ 該檔案有沒有出現 setOauthToken(有 = 較像已含修補;沒有 = 較像舊版)--- twitter.php 出現 2 次
範例的 ❶ 下面是空的,代表資料庫設定裡沒找到名稱含 twitter 的項目;有找到的話,會多出一張列出項目名稱的表格,不會顯示內容。實際輸出格式依 WP-CLI 版本略有差異。
分支判斷邏輯:
- 🔴 ❶ 有 twitter 相關項目,且版本是 4.61.0 以下 👉 你很可能就是這顆漏洞描述的那類網站,更新是第一優先,㊃、㊄ 一定要做。
- 🟡 ❶ 沒有項目,但 ❷ 檔案存在 👉 只代表「資料庫設定裡沒找到」,不代表沒設定過;請搭配第三章 ㊆ 的後台檢查,仍然要更新。
- 🟡 ❸ 出現 0 次,且版本不是 4.62.0 以上 👉 較像舊版,與版本號互相印證。
- 🟠 ❸ 出現 0 次,但版本號已是 4.62.0 以上 👉 兩者矛盾,可能是外掛檔案沒有更新乾淨,或我的字串推測不成立;請以官方檢查碼為準,執行 wp plugin verify-checksums jupiterx-core,並交給維護人員看。
- 🟠 ❷ 找不到檔案 👉 外掛結構可能與預期不同,不要因此判定安全,回頭用版本號判斷。
📊 ㊃ 日誌排查:看誰一直在敲「社群登入的回呼」
為什麼這樣做:標準存取日誌不記錄 POST 的內容,只記「請求行」:方法、網址、狀態碼。X 登入的回呼,網址上一般會帶著 oauth_verifier 這個參數。所以日誌能回答的問題有限,我只抓三個結果特徵:哪些來源 IP 打了這類請求、最近的明細長什麼樣、有沒有人碰過 uploads 裡的 PHP 類檔案。回呼網址的實際格式我沒有逐版核對【❓待確認】,因此是用參數名稱去找,不是比對整條網址。HestiaCP 預設的網站日誌在 /var/log/nginx/domains/網域.log,函式用 zcat -f 一併讀輪替後的 .log.1 與 .log.2.gz。前面如果有 CDN,日誌裡看到的 IP 可能是 CDN 的 🎯
# 取出某網域「目前+輪替後(含 .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
domain="$(site_domain "$wp_path")"
printf '\n===== %s =====\n' "$domain"
echo "--- ❶ 網址裡帶 oauth_verifier 的請求:來源 IP 排行(前 15 名)---"
scan_domain_log "$domain" \
| awk -F'"' 'index($2, "oauth_verifier=") { split($1, a, " "); print a[1] }' \
| sort | uniq -c | sort -rn | head -n 15
echo "--- ❷ 這類請求的最近 20 筆明細 ---"
scan_domain_log "$domain" \
| awk -F'"' 'index($2, "oauth_verifier=")' \
| tail -n 20
echo "--- ❸ 任何人碰過 uploads 底下的 PHP 類檔案(最近 20 筆)---"
scan_domain_log "$domain" \
| awk -F'"' 'tolower($2) ~ /\/wp-content\/uploads\/[^ ?]*\.(php|phtml|phar)/' \
| tail -n 20
return 0
}
# 💠 單一網站
log_scan_one "/home/admin/web/example.com/public_html"
# ❇️ 獨立多站/✴️ Multisite:掃主機上每一個網域
each_site log_scan_one
預期輸出範例(有可疑的樣子):
===== example.com =====
--- ❶ 網址裡帶 oauth_verifier 的請求:來源 IP 排行(前 15 名)---
48 203.0.113.5
2 198.51.100.1
--- ❷ 這類請求的最近 20 筆明細 ---
203.0.113.5 - - [27/Oct/2026:10:00:01 +0800] "GET /?oauth_token=xxxx&oauth_verifier=yyyy HTTP/1.1" 302 5 "-" "curl/8.5.0"
--- ❸ 任何人碰過 uploads 底下的 PHP 類檔案(最近 20 筆)---
203.0.113.7 - - [27/Oct/2026:10:00:03 +0800] "GET /wp-content/uploads/2026/10/x.php HTTP/1.1" 200 5 "-" "curl/8.5.0"
上面的 IP 是文件示範專用的保留位址,不是真實來源。
分支判斷邏輯:
- 🔴 ❸ 有命中 👉 uploads 裡有人執行過 PHP,視為高度可疑,直接進第六章 A,並同時做 ㊄ 的 ❹。
- 🟡 ❶ 有大量請求集中在單一陌生 IP,User-Agent 像腳本工具 👉 記下 IP 與時間,做完 ㊄;若 ㊄ 也有異常,進第六章 A。
- 🟡 ❶ 有請求,但數量不大、來源分散 👉 可能是真的有人用 X 登入。這種流量跟攻擊流量長得很像,單看日誌分不出來,我無法確認這顆漏洞在日誌裡的專屬特徵。
- 🟢 都沒命中 👉 只能說目前沒有直接證據,不能證明沒被打過:日誌可能已輪替、前面有 CDN,攻擊也可能沒走你查的這個日誌。繼續做 ㊄。
🕳️ ㊄ 帳號與檔案稽核:有沒有人拿著這把鑰匙進來過
為什麼這樣做:這顆漏洞的結果是「別人以某個使用者的身分登入」。對方一旦成為管理員,常見的下一步是新增帳號、裝外掛、改佈景主題檔案、塞進後門。所以稽核要同時看資料與檔案兩側:管理員名單、最近註冊的帳號、外掛清單、排程工作、近期被改過的文章;檔案側則看 uploads 裡的 PHP、會自動載入的 mu-plugins、wp-content 根目錄的 PHP,以及外掛與核心檔案跟官方版本的比對。全部用 WP-CLI 內建功能完成,不寫 SQL,所以資料表前綴不一樣也不怕。掃描起點 SINCE 我預設放 2026-10-01,因為最早受影響的版本不明,你的備份與日誌能追溯多遠,就往前調多遠 🕵️♀️
# 掃描起點:預設從 2026-10-01 起算。你的備份與日誌能追溯多遠,就往前調多遠
SINCE="2026-10-01"
audit_one() {
local wp_path="$1" opt
printf '\n===== %s(擁有者:%s)=====\n' "$wp_path" "$(site_owner "$wp_path")"
echo "--- ❶ 全部管理員 ---"
wpx "$wp_path" user list --role=administrator \
--fields=ID,user_login,user_email,user_registered --format=table
echo "--- ❷ 最近註冊的 10 個帳號(任何角色)---"
wpx "$wp_path" user list --orderby=registered --order=DESC --number=10 \
--fields=ID,user_login,user_email,user_registered,roles --format=table
echo "--- ❸ 外掛清單(有沒有你不認識的外掛)---"
wpx "$wp_path" plugin list --fields=name,status,version --format=table
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 與 wp-content 根目錄的 PHP(會自動載入,後台外掛頁看不到)---"
find "${wp_path}/wp-content/mu-plugins" -maxdepth 2 -type f \
-printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2> /dev/null | sort
find "${wp_path}/wp-content" -maxdepth 1 -type f -name '*.php' \
-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 "--- ❼ 排程工作(有沒有不認識的 hook)---"
wpx "$wp_path" cron event list --fields=hook,next_run_gmt,recurrence --format=table
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
echo "--- ❿ ${SINCE} 之後被修改過的文章與頁面(有沒有被塞廣告或導向連結)---"
wpx "$wp_path" post list --post_type=post,page --post_status=any \
--orderby=modified --order=DESC --posts_per_page=100 \
--fields=post_modified,ID,post_type,post_title --format=csv \
| awk -v s="$SINCE" 'NR == 1 || substr($0, 1, 10) >= s'
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 | user_login | user_email | user_registered | +----+------------+-------------------+---------------------+ | 1 | admin | admin@example.com | 2024-01-15 08:00:00 | +----+------------+-------------------+---------------------+ --- ❹ uploads 底下的 PHP 類檔案(正常網站應該是空的)--- --- ❽ 外掛與核心檔案跟官方版本比對 --- Success: Verified 12 of 13 plugins. Success: WordPress installation verifies against checksums.
❹ 沒有任何輸出,代表 uploads 裡沒有 PHP 類檔案,這是好事。❽ 若有不在 WordPress.org 的外掛,常會出現無法驗證的提示,這很正常,實際文字依 WP-CLI 版本而異 🧾
分支判斷邏輯:
- 🔴 ❶❷ 出現你不認識的帳號,或在漏洞公開前後才註冊的管理員 👉 高度可疑。陌生不等於惡意,可能是外包留的,先問清楚再處理。
- 🔴 ❹ 找到 PHP 類檔案,或 ❺ 出現不是你部署的檔案 👉 視為已中招,先別刪,保留證據,進第六章 A。
- 🟡 ❻ 掃描起點之後被改過的 PHP 👉 外掛正常更新也會出現,要對照你自己的維運紀錄,別看到就當成入侵。
- 🟡 ❸ 與 ❼ 出現不認識的外掛或排程 hook 👉 先查那是哪個外掛、誰裝的,查不出來就當成可疑。
- 🟡 ❽ 出現 File was added 之類的差異 👉 先確認是不是你自己放的客製檔案;不確定就當成可疑。
- 🔴 ❾ 的 users_can_register 變成 1、default_role 變成 administrator,或 siteurl/home 被改成陌生網域 👉 網站設定被動過手腳,這類改動很少是正常維運造成的。
- 🟡 ❿ 出現你沒編輯過的文章或頁面 👉 對照小編的編輯紀錄,有廣告或奇怪外連就是警訊。
- 🟢 全部乾淨 👉 這一輪沒有發現明確跡象,仍建議做第六章的更新與第七章的加固,並多觀察一週。
🚨 這份稽核的盲區要講清楚
WordPress 預設不記錄「誰在什麼時候登入」。如果對方只是登入看了一眼、沒有改動任何東西,上面的指令是查不出來的。你的網站若有安裝登入紀錄類的外掛,請另外去翻它的紀錄;沒有的話,這一塊就只能留白,我沒辦法替你補 🔐
📋 ㊅ 三環境速查表:把這一章濃縮成一張
| 階段 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| ① 工具箱 | 貼一次,整個 SSH 連線通用 | ||
| ② 盤點版本 | inventory_one 路徑 | each_site inventory_one | 查主站一次,狀態看 active-network |
| ③ X 登入入口 | xlogin_one 路徑 | each_site xlogin_one | 各子站的設定可能分開存放,這份指令只查主站,子站需另用 –url 逐一查 |
| ④ 日誌排查 | 單一網域 | 掃所有網域日誌 | 共用網域日誌,再依 Host 回推子站 |
| ⑤ 帳號與檔案 | 管理員/檔案/設定 | 逐站各自檢查,不能拿 A 站代表 B 站 | 另加 super-admin list |
🛠️ 第六章:疑似中招、還不能更新、準備修補,三條路各走各的
前一章解決「怎麼判斷」,這一章才動手。主線固定是 備份 👉 預覽 👉 更新 👉 驗證;第五章若出現跡象,要先走 A 取證與止血。這一章會改動系統狀態,所以每個動作後面都接著「怎麼確認真的生效」,以及失敗時的下一步。以下沿用第五章貼好的工具箱,沒貼的話請先回去貼一次 🧹
💾 ㊀ 三個共用零件:備份、刷新鹽值、停用外掛
為什麼這樣做:A、B、C 都會用到其中一兩個,所以先定義成函式,這一步只有定義,不會執行任何動作。備份檔放在 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 tar_file
domain="$(site_domain "$wp_path")"
stamp="$(date +%Y%m%d-%H%M%S)"
db_file="${BACKUP_DIR}/${domain}_db_${stamp}.sql"
tar_file="${BACKUP_DIR}/${domain}_wp-content_${stamp}.tar.gz"
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 "$tar_file" -C "$wp_path" wp-content || { echo "❌ 檔案備份失敗,停止"; return 1; }
chmod 600 -- "$tar_file"
ls -lh -- "$db_file" "$tar_file"
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
}
# ㊂ 共用零件三:停用 Jupiter X Core(Multisite 連全網與各子站一起處理)
deactivate_one() {
local wp_path="$1" slug url status home_url code ans left=0
for slug in "${PLUGINS[@]}"; do
if wpx "$wp_path" core is-installed --network 2> /dev/null; then
wpx "$wp_path" plugin deactivate "$slug" --network 2> /dev/null || true
while IFS= read -r url <&3; do
wpx "$wp_path" --url="$url" plugin deactivate "$slug" 2> /dev/null || true
done 3< <(wpx "$wp_path" site list --field=url)
else
wpx "$wp_path" plugin deactivate "$slug" 2> /dev/null || true
fi
done
# 驗證一:已安裝的版本,狀態都不能是 active 或 active-network
for slug in "${PLUGINS[@]}"; do
status="$(wpx "$wp_path" plugin get "$slug" --field=status 2> /dev/null || true)"
[ -n "$status" ] || continue
printf '%s:%s\n' "$slug" "$status"
case "$status" in
active|active-network) left=1 ;;
esac
done
# 驗證二:首頁還打得開(Jupiter X 佈景主題很依賴這個外掛,停用後整站掛掉並不意外)
home_url="$(wpx "$wp_path" option get home)"
code="$(curl -sSL -o /dev/null -w '%{http_code}' --max-time 20 "${home_url%/}/" || true)"
case "$code" in
2*|3*) printf '首頁 HTTP %s,正常\n' "$code" ;;
*)
printf '⚠️ 首頁 HTTP %s:停用後網站出問題了\n' "${code:-無回應}"
read -r -p "要立刻重新啟用 jupiterx-core 讓網站恢復嗎?(y/N):" ans
if [ "${ans:-}" = "y" ]; then
wpx "$wp_path" plugin activate "${PLUGINS[0]}" || true
echo "已重新啟用。這代表漏洞入口又打開了,請盡快排更新"
else
echo "維持停用狀態,請盡快找維護人員處理"
fi
;;
esac
if [ "$left" -eq 1 ]; then
echo "❌ 仍有版本是啟用狀態,請看上方輸出"
return 1
fi
return 0
}
預期輸出:貼完沒有任何輸出就是成功。三個函式要等到 A、B 或 C 才會被叫用,這時候的輸出範例如下:
/dev/vda1 80G 22G 54G 29% / -rw------- 1 root root 485M Oct 28 10:30 /root/wp-security-backup/example.com_db_20261028-103022.sql -rw------- 1 root root 2.4G Oct 28 10:31 /root/wp-security-backup/example.com_wp-content_20261028-103022.tar.gz Success: Shuffled the salt keys. ✅ example.com 鹽值已更新(3fa9c1d2e8b4 → 91c07a5be2d6),wp-config.php 權限:640 admin:admin Plugin 'jupiterx-core' deactivated. jupiterx-core:inactive 首頁 HTTP 200,正常
驗證與失敗分支:
- 🟢 備份:兩個檔案都在、大小合理、資料庫檔結尾有 Dump completed 👉 可以進下一步。這個結尾字樣是 mysqldump 與 MariaDB 的慣例輸出,若你的環境用別的工具匯出,字樣可能不同,腳本只會警告,不會擋你。
- 🟠 備份失敗或是空檔 👉 函式立刻停下,先查磁碟空間(已印出 df 結果)與資料庫權限,沒有備份不要往下。
- 🟢 鹽值:前後兩段雜湊不同 👉 已生效,你會被登出,重新登入即可。
- 🟠 鹽值出現 ❌「沒有改變」或指令本身失敗 👉 先確認 wp-config.php 的擁有者可寫。另外,部分版本的 WP-CLI 是連到 WordPress.org 的 API 取得新鹽值,主機連不出去時也會失敗,這一點我沒有逐版核對【❓待確認】,失敗時先查網路與 WP-CLI 版本,不要急著換別的來源。
- 🔴 刷完網站出現「建立資料庫連線時發生錯誤」 👉 用備份還原:cp -p “$BACKUP_DIR/網域_wp-config.php.時間” “網站路徑/wp-config.php”,備份檔名在上面的 ls 輸出或備份資料夾裡。這份備份含資料庫密碼,確認一切正常後再決定要不要保留。
- 🟢 停用:已安裝的外掛狀態都是 inactive,首頁 HTTP 為 2xx 或 3xx 👉 成功。
- 🟠 停用後首頁不是 2xx/3xx 👉 Jupiter X 佈景主題依賴這個外掛,這是預期中可能發生的事。函式會問你要不要立刻重新啟用;選 y 網站就恢復,但漏洞入口也重新打開,要盡快排更新。選 n 則維持停用,請維護人員處理。
🚨 ㊁ A. 疑似中招:先留證據,再止血,最後才修補
為什麼這樣做:第五章出現明確跡象時,第一優先不是急著更新,而是保留現場。順序是:資料庫快照與日誌 👉 停用外掛 👉 刷新鹽值 👉 管理員密碼重設與登出所有階段。這個順序有道理:先拍照存證,再關門,再讓所有舊憑證作廢,最後處理帳號。管理員一旦被接管,對方理論上能讀到 wp-config.php,所以腳本結束時會提醒你接著換資料庫密碼 🧯
這支腳本會動到外掛、鹽值與帳號,請先確認路徑,而且只對有疑慮的那一站執行,不要整台主機無差別跑。新密碼會留在終端機的捲動紀錄裡,請在私密環境操作,用安全管道交給管理員本人,並請他登入後立刻自行更換,不要貼進聊天視窗或腳本 🔐
# 前提:已貼過「工具箱」「備份與鹽值」「停用」三段。取證完成前,什麼都不要刪
incident_one() {
local wp_path="$1" domain stamp evid out uid ans
domain="$(site_domain "$wp_path")"
stamp="$(date +%Y%m%d-%H%M%S)"
evid="${HOME}/jupiterx-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 --fields=ID,user_login,user_email,user_registered,roles \
--format=table > "${out}/users.txt"
wpx "$wp_path" plugin list --fields=name,status,version --format=table > "${out}/plugins.txt"
wpx "$wp_path" cron event list --fields=hook,next_run_gmt,recurrence \
--format=table > "${out}/cron.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 "=== ❷ 隔離:停用 Jupiter X Core ==="
deactivate_one "$wp_path" || return 1
echo "=== ❸ 強制刷新金鑰與鹽值(攻擊者手上的登入 Cookie 一併失效)==="
rotate_salts_one "$wp_path" || return 1
echo "=== ❹ 管理員名單與密碼重設 ==="
wpx "$wp_path" user list --role=administrator \
--fields=ID,user_login,user_email --format=table
read -r -p "要替上面每一位管理員重設密碼並登出所有階段嗎?(y/N):" ans
if [ "${ans:-}" = "y" ]; then
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)
else
echo "已略過密碼重設"
fi
wpx "$wp_path" cache flush || true
printf '\n✅ %s 止血完成,證據放在 %s\n' "$domain" "$out"
ls -lh -- "$out"
echo "⚠️ 還沒做完:攻擊者若已拿到管理員權限,就可能讀過資料庫密碼,請接著換掉"
return 0
}
# 只對「有疑慮的那一站」執行,不要整台主機一起跑
incident_one "/home/admin/web/example.com/public_html"
預期輸出範例:
=== ❷ 隔離:停用 Jupiter X Core === Plugin 'jupiterx-core' deactivated. jupiterx-core:inactive 首頁 HTTP 200,正常 === ❸ 強制刷新金鑰與鹽值(攻擊者手上的登入 Cookie 一併失效)=== Success: Shuffled the salt keys. ✅ example.com 鹽值已更新(3fa9c1d2e8b4 → 91c07a5be2d6),wp-config.php 權限:640 admin:admin === ❹ 管理員名單與密碼重設 === (列出管理員後詢問是否重設,選 y 會顯示每位管理員的新隨機密碼) ✅ example.com 止血完成,證據放在 /root/jupiterx-incident-20261028-103500/example.com ⚠️ 還沒做完:攻擊者若已拿到管理員權限,就可能讀過資料庫密碼,請接著換掉
驗證(確認真的止血了):跑完後執行下面這段。預期結果是外掛顯示 inactive(沒安裝會顯示「未安裝」)、管理員名單只剩你認識的人、最後顯示 db_ok 與 1。任何一項不符,就代表那一步沒成功,回頭看對應輸出。這段只讀不寫 🧪
WP_PATH="/home/admin/web/example.com/public_html"
for slug in "${PLUGINS[@]}"; do
printf '%s:' "$slug"
wpx "$WP_PATH" plugin get "$slug" --field=status 2> /dev/null || echo "未安裝"
done
wpx "$WP_PATH" user list --role=administrator --fields=ID,user_login,user_email --format=table
wpx "$WP_PATH" db query 'SELECT 1 AS db_ok;'
- ⭕ 全部成功 👉 保留證據資料夾,接著換資料庫密碼(見下方),再走 C 正式修補。公司或有會員資料的網站,同步通知內部資安或主管。
- 🟠 資料庫快照失敗 👉 函式立刻中斷,先處理磁碟空間或資料庫權限,沒有快照不要往下。
- 🟠 停用外掛時網站出錯 👉 照 ㊀ 的分支處理;外掛檔案若疑似被改過,請交給資安專業人員做完整清理,不要自己硬刪。
- 🔴 發現後門檔案、不認識的管理員,或網站有個資與線上收款且確認曾遭觸發 👉 這已超出更新外掛的層級,請啟動內部資安事件流程,並把 SMTP、API 金鑰等放在資料庫或設定檔的憑證一併換掉。
- 🔴 更新前的備份可能已經含有後門,不能當成「乾淨的還原來源」;要還原請用事發前、已知乾淨的備份。
🔑 補一刀:換掉資料庫密碼,並讓 wp-config.php 對得起來
為什麼這樣做:wp-config.php 裡有資料庫帳密,管理員被接管後,理論上讀得到。順序很重要:先在 HestiaCP 面板把這個資料庫使用者的密碼改掉,再讓 wp-config.php 跟上,中間網站會短暫連不上資料庫,所以挑離峰時間做。面板操作我不寫成指令,因為各版本的指令參數我沒有逐一核對;面板上的「資料庫」頁面改密碼最不容易出錯。下面的函式只負責第二步:讓你輸入新密碼(輸入時不顯示),寫進 wp-config.php,再實際連一次資料庫確認 🛡️
密碼會出現在這個指令的參數裡,同一台主機上的其他使用者在極短時間內有機會從程序清單看到。這是 WP-CLI 這個指令的限制,所以請在沒有其他人登入的時段操作,並使用只含英數字元的隨機密碼,避免特殊符號被 Shell 吃掉 🙈
# 前提:已貼過「工具箱」。資料庫密碼請先在 HestiaCP 面板改好(建議用英數字元的隨機密碼)
rotate_db_password_one() {
local wp_path="$1" db_user new_pw
db_user="$(wpx "$wp_path" config get DB_USER)" || return 1
printf '資料庫使用者:%s\n' "$db_user"
read -r -s -p "請輸入剛在面板設定的新密碼(輸入時不會顯示):" new_pw
echo
if [ -z "$new_pw" ]; then
echo "沒有輸入,停止,wp-config.php 沒有變動"
return 1
fi
wpx "$wp_path" config set DB_PASSWORD "$new_pw" || return 1
wpx "$wp_path" db query 'SELECT 1 AS db_ok;'
}
rotate_db_password_one "/home/admin/web/example.com/public_html"
- 🟢 最後印出 db_ok 與 1 👉 新密碼已生效,網站恢復連線。
- 🔴 出現 Access denied 👉 面板上設定的密碼與你剛輸入的不一致。用面板重設一次、再重新輸入,或用 ㊀ 備份的 wp-config.php 還原。
- 🟡 網站正在對外服務卻還沒完成這一步 👉 會有短暫的資料庫連線錯誤,屬預期,完成後即恢復。
🧯 ㊂ B. 短期應急:還沒確認中招,但現在還不能更新
🚨 短期應急 ≠ 正式修補。
停用外掛或關掉 X 登入,只是縮短暴露時間。真正的修補仍然是更新到 4.62.0 以上。我刻意沒有提供 nginx 的擋規則:這顆漏洞走的是社群登入的正常回呼流程,沒有一個我驗證過的固定網址特徵可以擋,亂擋不是擋不到,就是把真正要登入的人一起擋掉 🧱
選項一(較溫和):在後台把 X 登入關掉。做法見第三章 ㊆:把登入、註冊表單上的 X(Twitter)登入選項關閉。這是改設定,不是改程式,網站其他功能不受影響。缺點是選單名稱依版本而異【❓待確認】,也無法保證你找得到所有放了 X 登入的表單,所以做完要用第五章 ㊂ 再查一次。這一項沒有指令,因為我無法確認設定存放的位置,不想給你一條猜的指令 🧩
選項二(最乾淨,但可能讓網站掛掉):停用外掛。直接叫用 ㊀ 貼好的 deactivate_one。它會處理 Multisite,最後驗證狀態與首頁,首頁異常時會問你要不要重新啟用 🧯
# 💠 單一網站(路徑換成你的;Multisite 請對網路根目錄跑一次) deactivate_one "/home/admin/web/example.com/public_html"
預期輸出範例:
Plugin 'jupiterx-core' deactivated. jupiterx-core:inactive 首頁 HTTP 200,正常
- 🟢 選項二:狀態 inactive、首頁 2xx 或 3xx 👉 暫時安全,立刻排更新。
- 🟠 選項二:首頁不是 2xx/3xx 👉 見 ㊀ 的失敗分支。通常是佈景主題少了外掛,改走選項一,並交給維護人員。
- 🟡 選項一:只代表降低風險,不代表安全。你無法百分之百確認每個入口都關了,仍要盡快更新。
- 🟡 要還原時,把外掛重新啟用:wp plugin activate jupiterx-core(以網站擁有者身分執行),再用 plugin get jupiterx-core –field=status 驗證回到 active。
🚀 ㊃ C. 正式修補:備份 👉 預覽 👉 更新 👉 驗證
為什麼這樣做:Jupiter X Core 掛在 WordPress.org,WP-CLI 的 plugin update 可以直接更新,plugin verify-checksums 能拿官方檢查碼比對外掛檔案有沒有被動過。所以更新管道只有一條:WordPress.org,不需要授權碼或手動 zip。腳本用 heredoc 建立,結束標記用單引號包住,裡面的 $ 不會被提前展開,建好後立刻做語法檢查。千萬不要為了「保險」而降版,降回舊版等於把洞重新打開 📦
cat > /usr/local/sbin/jupiterx-patch.sh <<'JUPITERX_PATCH_EOF'
#!/usr/bin/env bash
# jupiterx-patch.sh:單一網站的 Jupiter X Core 修補流程(備份 → 預覽 → 確認 → 更新 → 驗證)
# 用法:jupiterx-patch.sh 網站使用者 網域
set -euo pipefail
SLUG="jupiterx-core"
FIXED="4.62.0"
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
HOME_DIR="$(getent passwd "$SITE_USER" | cut -d: -f6 || true)"
wpx() {
runuser -u "$SITE_USER" -- env HOME="${HOME_DIR:-/tmp}" "$WPCLI" --path="$WP_PATH" "$@" < /dev/null
}
version_lt() {
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]
}
is_fixed() { ! version_lt "$1" "$FIXED"; }
get_ver() { wpx plugin get "$SLUG" --field=version 2> /dev/null || true; }
cur_ver="$(get_ver)"
if [ -z "$cur_ver" ]; then
echo "⚪ ${SITE_DOMAIN} 沒有安裝 Jupiter X Core,不需處理"
exit 0
fi
printf '目前版本:%s\n' "$cur_ver"
if ! is_fixed "$cur_ver"; then
echo "🔴 ${SITE_DOMAIN} 版本低於 ${FIXED},開始處理"
BACKUP_DIR="${HOME}/wp-security-backup"
DB_FILE="${BACKUP_DIR}/${SITE_DOMAIN}_db_$(date +%Y%m%d-%H%M%S).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 "$SLUG" --dry-run
read -r -p "確認要更新 Jupiter X Core 嗎?(y/N):" ans
if [ "${ans:-}" = "y" ]; then
wpx plugin update "$SLUG"
else
echo "已略過更新"
fi
fi
echo "--- 驗證 ---"
fail=0
v="$(get_ver)"
if is_fixed "$v"; then
echo "✅ ${SLUG}:${v}"
else
echo "❌ ${SLUG}:${v} 仍低於 ${FIXED}" >&2
fail=1
fi
status="$(wpx plugin get "$SLUG" --field=status 2> /dev/null || true)"
printf '外掛狀態:%s\n' "${status:--}"
wpx plugin verify-checksums "$SLUG" || fail=1
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}:首頁 HTTP ${code}" ;;
*) echo "❌ ${SITE_DOMAIN}:首頁 HTTP ${code:-無回應},請立刻檢查" >&2; fail=1 ;;
esac
exit "$fail"
JUPITERX_PATCH_EOF
chown root:root /usr/local/sbin/jupiterx-patch.sh
chmod 750 /usr/local/sbin/jupiterx-patch.sh
bash -n /usr/local/sbin/jupiterx-patch.sh && echo "語法檢查通過"
驗證(確認腳本建好了):最後一行預期印出「語法檢查通過」。沒有這行就代表 heredoc 內容有缺漏,用 ls -l /usr/local/sbin/jupiterx-patch.sh 看檔案是否存在,再重新貼一次整段 🧪
使用方式:一次一站,先處理一站、確認前後台正常,再處理下一站。把第五章盤點出的 🔴 站,逐一用「使用者、網域」當參數跑:
# 兩個參數:網站使用者、網域(一次一站) /usr/local/sbin/jupiterx-patch.sh admin example.com
預期輸出範例:
目前版本:4.61.0 🔴 example.com 版本低於 4.62.0,開始處理 ✅ 資料庫已備份:/root/wp-security-backup/example.com_db_20261028-111500.sql --- 更新預覽(不會真的更新)--- +---------------+-------------+-------------+-----------+ | name | old_version | new_version | status | +---------------+-------------+-------------+-----------+ | jupiterx-core | 4.61.0 | 4.62.0 | available | +---------------+-------------+-------------+-----------+ 確認要更新 Jupiter X Core 嗎?(y/N):y Success: Updated 1 of 1 plugins. --- 驗證 --- ✅ jupiterx-core:4.62.0 外掛狀態:active Success: Verified 1 of 1 plugins. ✅ example.com:首頁 HTTP 200
- 🟢 看到最後的 ✅ 👉 這一站修補完成。接著手動清網站快取與 CDN 快取,並照第三章 ㊄ 開幾個頁面確認版面沒壞。
- 🟠 備份失敗 👉 腳本直接停下,沒有任何更新,先查磁碟空間與資料庫權限。
- 🟠 出現 ❌「仍低於 4.62.0」 👉 更新來源沒生效。檢查主機連不連得到 WordPress.org,把輸出交給維運人員,不要反覆硬跑。
- 🟠 verify-checksums 出現 File was added 👉 外掛資料夾裡多了官方沒有的檔案,逐一確認,不確定就視為可疑,回到 A。
- 🟠 預覽時沒有列出可用更新、但版本仍是舊的 👉 更新檢查可能被快取;以最後的版本判定為準,也要確認主機連得到 WordPress.org。
- 🔴 更新後網站白畫面或版面大亂 👉 立刻停用外掛(B 選項二)讓網站先回到可存取的狀態,再用備份到測試站慢慢查,不要在正式站試錯。論壇案例顯示,這個外掛更新後出現相容性問題並非沒有先例。
🚨 驗證的侷限:
verify-checksums 只能證明外掛目錄裡的檔案和官方發布版一致,檢查不到 mu-plugins、uploads、資料庫、別的外掛或佈景主題有沒有被動過,也無法證明「沒有後門」。它只是完整性檢查的其中一環,必須搭配第五章 ㊄ 的稽核。另外,如果這一站已經中招,更新前的備份本身就含有後門,不能當成乾淨的還原來源 🔐
| 情境 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| 疑似中招(A) | incident_one 路徑 | 只對有疑慮的站逐一執行 | 對網路根目錄跑一次,腳本會連子站一起停用 |
| 短期應急(B) | deactivate_one 路徑 | 逐站套用 | 對網路根目錄跑一次 |
| 正式修補(C) | jupiterx-patch.sh 使用者 網域 | 一次一站,確認後再下一站 | 外掛檔案只有一份,更新一次即可,再逐子站驗證登入頁 |
🔥 一句話總結:「批次」不等於把所有東西塞進一條迴圈。真正安全的做法是先盤點、再備份、預覽,確認後逐站更新,最後驗證,而且每一步失敗就停下 🎯
🔒 第七章:補完洞之後,把第二道門也鎖上
更新是根本,加固是「萬一下一顆漏洞又來,讓損害小一點」。先講清楚:這一章的招式擋不住這顆 CVE 本身,它們擋的是漏洞被打中之後的下一步,屬於通用加固,不是官方要求。唯一和這顆漏洞直接相關的,是前兩項(自動更新、管好 X 登入) 🛡️
🤖 加固一:開自動更新,把空窗期縮到最短
為什麼這樣做:這次漏洞的修補版在公開前就已上架,但真正的風險是「版本一直停在舊的」。第三章 ㊅ 教過用滑鼠開;有 SSH 的人可以用下面這段,對指定的網站啟用,並用 auto_update 欄位驗證。代價是更新偶爾會和佈景主題打架,所以有每日備份才建議開 🔄
WP_PATH="/home/admin/web/example.com/public_html" # 開啟 Jupiter X Core 的自動更新(以網站擁有者身分執行) wpx "$WP_PATH" plugin auto-updates enable jupiterx-core # 驗證:auto_update 欄位要顯示 on wpx "$WP_PATH" plugin list --name=jupiterx-core --fields=name,version,auto_update --format=table
預期輸出範例:
Success: Enabled 1 of 1 plugin auto-updates. +---------------+---------+-------------+ | name | version | auto_update | +---------------+---------+-------------+ | jupiterx-core | 4.62.0 | on | +---------------+---------+-------------+
- 🟢 auto_update 欄位顯示 on 👉 已啟用。
- 🟠 指令不存在,或欄位沒顯示 👉 你的 WP-CLI 版本可能較舊【❓待確認】,改用第三章 ㊅ 的滑鼠做法,再到後台外掛清單確認連結已變成「停用自動更新」。
- 🟡 想取消:把 enable 換成 disable,再看一次欄位變成 off。
🪪 加固二:管好你的 X 登入
- ⭐ 用不到的社群登入,直接關掉。每多開一個,就多一個要維護的入口。X 登入如果你的訪客根本沒在用,關掉最省事。
- ⭐ X 應用程式改用專用信箱申請。照第一章的推論,被誤認的對象是「X 應用程式的擁有者」,再用電子郵件去對應網站使用者。如果這個信箱不屬於任何高權限帳號,萬一再出現類似問題,被對應到的就不會是管理員。這是 🟡 我的推論與防禦思路,不是官方建議;做法是到 X 開發者後台,確認應用程式擁有者的信箱,不要和網站管理員同一個。操作畫面我沒有逐一核對【❓待確認】。
- ⭐ 管理員改用獨立帳號與強密碼。日常發文用編輯帳號,管理員帳號只在需要時才登入。
- ⭐ 雙重驗證(2FA)不要當成這顆漏洞的解方。2FA 外掛多半掛在標準登入流程上,如果漏洞路徑是直接建立登入狀態,不一定會被擋下,我沒有資料能驗證這一點【❓待確認】。它依然值得開,只是不能取代更新。
- ⭐ 管理員的變動要有人知道。後台新增管理員、外掛被安裝,最好有通知機制。常見資安外掛有提供,可評估是否啟用【❓待確認:依你使用的外掛而定】。
🧱 加固三:把 PHP 的系統指令函式關掉
為什麼這樣做:管理員被接管後,常見的下一步是想辦法在伺服器上執行指令。disable_functions 可以讓 PHP 無法呼叫 exec、system 這類直接跟作業系統溝通的函式,能做的事會少一截。清單刻意不含 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 設定有沒有覆蓋。
- 🟠 印出「探針檔案還在」 👉 手動刪掉那個檔案,別留在網站根目錄。
- 🟡 沒有回應 👉 可能是憑證、防火牆或網域解析問題,先確認網站本身連得上。
🚨 要誠實講的侷限
disable_functions 擋的是「呼叫系統指令」這條路。攻擊者若已經能執行 PHP,他仍然可以用 PHP 本身的檔案函式寫入、讀取檔案。所以它只是把牆加高,不是把牆變成保險箱,不能取代更新與稽核 🧱
🚫 加固四:禁止執行 uploads 資料夾裡的 PHP
為什麼這樣做:上傳資料夾本來只該放圖片與文件。攻擊者想把後門丟進網站,最常見的落腳處就是這裡。用 nginx 規則讓 /wp-content/uploads/ 底下的 PHP 類檔案一律回 403,即使檔案被放進去也跑不起來。HestiaCP 的網站設定檔會在重建網域或續約 SSL 時被樣板覆蓋,所以千萬不要直接改 nginx.conf;官方論壇的做法是在 /home/使用者/conf/web/網域/ 放 nginx.conf_任意名稱(HTTP)與 nginx.ssl.conf_任意名稱(HTTPS),樣板會自動載入。下面的函式在 nginx -t 沒過時會自動撤回剛建的兩個檔案。規則用 ^~ 前綴,讓這個資料夾不再被其他 PHP 規則攔走。不同 HestiaCP 樣板(例如 nginx 搭配 Apache 的組合)對這條規則的反應可能不同,我沒有逐一驗證,所以最後一定要跑驗證:一般圖片還能正常顯示,才算沒副作用 🧱
# 用 HestiaCP 認得的自訂檔,禁止執行 uploads 底下的 PHP(面板重建設定時不會被蓋掉)
block_uploads_php() {
local owner="$1" domain="$2"
local conf_dir="/home/${owner}/conf/web/${domain}"
local http_inc="${conf_dir}/nginx.conf_no_uploads_php"
local ssl_inc="${conf_dir}/nginx.ssl.conf_no_uploads_php"
if [ ! -d "$conf_dir" ]; then
printf '找不到 %s,略過\n' "$conf_dir"
return 1
fi
tee "$http_inc" > /dev/null <<'EOF'
# 上傳資料夾只該放圖片與文件,PHP 類檔案一律回 403
location ^~ /wp-content/uploads/ {
location ~* \.(php|phtml|phar)$ {
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_uploads_php() {
local owner="$1" domain="$2"
local conf_dir="/home/${owner}/conf/web/${domain}"
rm -f -- "${conf_dir}/nginx.conf_no_uploads_php" "${conf_dir}/nginx.ssl.conf_no_uploads_php"
if nginx -t; then
systemctl reload nginx
printf '已解除:%s\n' "$domain"
else
echo "nginx 設定檢查沒過,請先查原因,不要 reload"
return 1
fi
}
# 驗證:PHP 探針要 403,uploads 裡的一般圖片仍然是 200(探針用完立刻刪除)
check_uploads_block() {
local wp_path="$1" image_url="$2" domain name probe
domain="$(site_domain "$wp_path")"
name="__probe_$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n').php"
probe="${wp_path}/wp-content/uploads/${name}"
printf '<?php echo "PROBE-RAN";\n' > "$probe"
curl -sS -o /dev/null -w "uploads 內的 PHP 探針:HTTP %{http_code}\n" --max-time 20 \
"https://${domain}/wp-content/uploads/${name}" || true
rm -f -- "$probe"
[ -e "$probe" ] && printf '⚠️ 探針檔案還在,請手動刪除:%s\n' "$probe"
curl -sS -o /dev/null -w "一般圖片:HTTP %{http_code}\n" --max-time 20 "$image_url" || true
}
# 套用(把使用者與網域換成你的)
block_uploads_php "admin" "example.com"
check_uploads_block "/home/admin/web/example.com/public_html" \
"https://example.com/wp-content/uploads/你網站上任一張圖片.jpg"
預期輸出範例:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful 已套用:example.com uploads 內的 PHP 探針:HTTP 403 一般圖片:HTTP 200
- 🟢 探針 403、圖片 200 👉 規則生效且沒有副作用。
- 🟠 探針回 200 或 500 👉 規則沒被載入,或被別的設定蓋掉。立刻執行 unblock_uploads_php 把規則拆掉,再請維運人員看樣板。⚠️ 這個函式會刪檔,但只刪我們自己建的那兩個。
- 🟠 圖片也變成 403 或 404 👉 規則影響到靜態檔,同樣先拆除。
- 🟠 nginx -t 報錯 👉 函式已自動撤回,請保留錯誤訊息給維運人員。
- 🟡 探針檔案還在 👉 手動刪掉,別留著。
🧑🔧 DevOps 小知識:open_basedir 不用你再全域設定
有些教學建議在 php.ini 全域寫死 open_basedir,但寫死某個網站的路徑,其他網站就全壞。HestiaCP 的 PHP-FPM 樣板本來就會為每個網域設好,用 grep -H open_basedir /etc/php/*/fpm/pool.d/*.conf 確認即可。它同樣擋不住這顆 CVE,只是縮小事後的傷害範圍 🔒
🩵 荷包試算:要不要花錢買 WAF 或虛擬修補?
付費防火牆可以在你更新前先擋一層,但這顆漏洞在後台點一下「更新」就解決,今天能更新,通常比月付方案划算。真的卡在相容性、短期無法更新時,再把付費防護當過渡手段,別反過來拿它當不更新的理由。至於 WAF 能不能擋這種「看起來像正常社群登入回呼」的請求,我沒有資料可以驗證 💰
🤔 第八章:讀到快喘不過氣?常見焦慮與幾個通用心法
- 🌠 Q:我已經是 4.62.0 了,就安全了嗎?
A:就這顆 CVE 來說,是的,那是官方的修補版。但如果舊版曾對外暴露,更新只能擋住下一次,擋不了已經發生的事,所以還是建議做完第五章的稽核 📦 - 🌟 Q:我從沒設定過 X 登入,還需要更新嗎?
A:需要。依公開資料,沒設定過就不符合利用前提,但我無法驗證「沒設定就一定打不到」,而且你也不一定記得自己或前任維護者有沒有設定過。更新不花錢,賭一把沒有意義 🧭 - ✨ Q:更新後版面壞了,可以降版嗎?
A:不建議。降版等於重新打開漏洞。先用備份在測試站找原因,正式站可以先停用 X 登入或請維護人員處理 🙅 - 🌠 Q:後台看不到更新按鈕怎麼辦?
A:先到「儀表板 → 更新」按「再檢查一次」。還是沒有,確認主機能連到 WordPress.org,或請維護人員用第六章 C 的腳本處理 🤗 - ✨ Q:看不懂那些指令,是不是沒救了?
A:不會。第三章全程用滑鼠就能完成最基本的處理,第五章以後是給有 SSH 權限的人看的補充 🔍 - 🌟 Q:我該不該通知會員改密碼?
A:這取決於你有沒有發現實際入侵的跡象,以及你的網站存了什麼資料。這個判斷請交給公司資安或法務,我沒辦法替你下結論。此為資訊統整,非專業建議,重大決定前請諮詢真人專家 ⚖️
🧩 舉一反三:「社群登入」這一類漏洞怎麼防
下面這些是通用防禦知識,不是本 CVE 的官方要求,但下次看到類似新聞能對號入座 🫡
- ⭐ OAuth 流程的每一步,都要確認「這張憑證屬於眼前這個人」。這次的問題不是 OAuth 本身不安全,而是程式拿到訪客的憑證後沒有真的用上它。官方補的三項檢查,就是把「憑證、Token、使用者編號」三個環節一一對起來。
- ⭐ 用電子郵件當唯一的帳號對應依據,風險比你想的高。只要「查到的身分」出一點差錯,就會直接落到某個真實帳號上。重要帳號可以考慮多一層確認,不要單靠信箱比對。
- ⭐ 功能「關掉」不等於「清乾淨」。外掛裡不用的功能,如果設定和程式碼還在,就可能還有入口。能整個移除的,就別只是停用。
- ⭐ 同一個功能反覆出事,要提高警覺。這個外掛的社群登入在 2024 年就有 CVE-2024-7781,這次是另一筆。碰到這類「歷史上出過事的區域」,更新要更積極,也要更勤快地看公告。
- ⭐ 修補與事件應變是兩件事。舊版只要曾暴露在網路上,更新完還要查日誌、查帳號、查檔案,缺一不可。
🏆 第九章:這次到底該多緊張?事件綜合評估
| 評估維度(10 分制,數字越高代表風險或應對品質越正向) | 分數 | 一句話評語 |
|---|---|---|
| 漏洞嚴重程度(越高代表風險越大) | 9.8 | 免登入、免互動、難度低,可能直接導向管理員被接管;扣分點只有「網站得先設定過 X 登入」這個前提 |
| 官方應變速度 | 8 | 依 CVE 紀錄,通知供應商後約兩週就有修補版(4.62.0 的上架日期僅有單一來源 🟡);扣一點是 changelog 只寫了「修補安全問題」,沒有交代細節 |
| 一般站長自救可行性 | 8 | 免費外掛,後台點「更新」就好;扣分在更新後可能和佈景主題或小工具打架,過去論壇案例不少 |
| DevOps 技術文件完整度 | 6 | 修補差異清楚,但完整觸發條件、是否已被利用都查不到,日誌也沒有專屬特徵,排查只能靠結果特徵 |
| 🏆 加權綜合建議 | 當天更新 | 最終建議:【今天內更新到 4.62.0 以上;有開 X 登入就做第五章稽核,疑似中招走第六章 A;一週內補上第七章加固】 |
🔥 這種漏洞最危險的,是「資訊公開了、你卻還沒更新」的那幾天。CVE 才剛發布,現在正是各種掃描工具開始跟上的時候。條件嚴格不代表沒人會試,因為自動化工具就是專門去試「成立但不顯眼」的組合。花五分鐘打開外掛頁看一眼版本號,比事後花五天清後門便宜太多 🔢
📎 附錄 A:一頁式決策表(印出來貼在螢幕旁)
| 你看到的狀況 | 先做什麼 | 接著做什麼 |
|---|---|---|
| 版本低於 4.62.0,沒有其他異常 | 備份 | 更新(C),開網站確認版面,再做第五章稽核 |
| 版本低於 4.62.0,而且有設定 X 登入 | 備份,若今天無法更新就走 B | 更新(C),做第五章 ㊃ ㊄ |
| 管理員、uploads 或 mu-plugins 出現異常 | 不要刪,走 A 取證與止血 | 換資料庫密碼,再走 C;有個資的網站通知內部資安與法務 |
| 更新後版面壞掉 | 不要降版,先確認是哪個頁面或小工具 | 用備份到測試站查原因;正式站先停用 X 登入或請維護人員處理 |
| 更新後網站白畫面 | 停用外掛(B 選項二)讓網站先回復 | 用備份到測試站查原因,並盡快排修補 |
| 後台沒有更新按鈕 | 儀表板 → 更新 → 再檢查一次 | 仍沒有就用第六章 C 腳本,並確認主機連得到 WordPress.org |
📖 附錄 B:名詞小辭典
- 🔹 CVE:漏洞的國際編號,CVE-2026-106013 就是這次的編號。
- 🔹 CVSS:漏洞嚴重度評分,滿分 10 分,9.0 以上屬於 Critical。
- 🔹 CWE-287:「不當驗證」類弱點的分類編號。
- 🔹 Checksum(檢查碼):用來比對檔案是否和官方版本一致的指紋,檢查不到「官方沒發布過」的資料與檔案。
- 🔹 EPSS:預測漏洞未來被利用機率的分數,這次尚未提供。
- 🔹 KEV:美國 CISA 維護的「已知被利用漏洞」清單,沒有收錄不代表沒人在攻擊。
- 🔹 OAuth 1.0a:社群登入使用的授權協定,流程是先拿暫時憑證、使用者授權後再換正式憑證。
- 🔹 WP-CLI:WordPress 的命令列工具,用來在主機上管理網站。
- 🔹 mu-plugins:會被 WordPress 自動載入、後台外掛頁看不到的特殊外掛資料夾,常被拿來藏後門。
- 🔹 slug(外掛代號):外掛在 WordPress 內部使用的英文識別名,這次是 jupiterx-core。
- 🔹 鹽值(Salts):wp-config.php 裡八組長字串,用來簽章登入 Cookie。
📚 參考資料與延伸資源
- 📌 WordPress.org|Jupiter X Core 外掛頁 [1]
- 📌 CVE Project|CVE-2026-106013 官方紀錄 [52]
- 📌 Wordfence|Jupiter X Core 漏洞條目 [53]
- 📌 WordPress.org SVN|changeset 3759312(twitter.php 修補差異) [54]
- 📌 CIRCL Vulnerability-Lookup|CVE-2026-106013 [55]
- 📌 OpenCVE|CVE-2026-106013 [56]
- 📌 NVD|CVE-2024-7781(同外掛 2024 年的社群登入紀錄) [57]
- 📌 WordPress.org 論壇|Broke my website(2022-04-07) [58]
- 📌 WordPress.org 論壇|Version 2.0.9 is broken(2022-06-01) [59]
- 📌 WordPress.org 論壇|Update is overdue(2026-08-13) [60]
- 📌 WP-CLI 官方文件|wp plugin update [61]
- 📌 WP-CLI 官方文件|wp plugin verify-checksums [62]
- 📌 WP-CLI 官方文件|wp plugin auto-updates [63]
- 📌 WP-CLI 官方文件|wp config shuffle-salts [64]
- 📌 WP-CLI 官方文件|wp user reset-password [65]
- 📌 WP-CLI 官方文件|wp user session [66]
- 📌 WP-CLI 官方文件|wp option list [67]
- 📌 HestiaCP 官方文件|CLI 參考 [68]
- 📌 HestiaCP 論壇|Custom nginx settings per domain [69]













