🖼️ 一張「圖片」就能在你家網站開後門?Bricksforge 檔案上傳漏洞全解析(CVE-2026-85097/CVSS 9.8)
🔥 懶人包:
Bricks Builder 的熱門擴充外掛 Bricksforge 爆出一個「不用登入、不用密碼」就能在你的主機上放進並執行 PHP 程式的漏洞(CVE-2026-85097,CVSS 9.8 嚴重等級;Patchstack 自家頁面標 10.0)。更要命的是,資安廠商 Patchstack 說,第一筆實際攻擊最早在 2026-10-07 21:47 UTC(台灣時間 10 月 8 日清晨 5 點多)就被偵測到,也就是消息一公開,掃描機器人已經在路上了。受影響版本是 3.1.8.9 以下,官方確認的修補版是 3.1.8.10。還有一件很多人會忽略的事:社群裡已經有人回報 4.0.0 也被打到,而且他的網站連一個上傳欄位都沒有 🚨
🌐 前往 Bricksforge 官方網站更新日誌確認版本 [1]
內容目錄
🧳 第一章:只是寄放一張圖片,怎麼會被搬進「可以執行程式」的房間?
先問你一個問題:如果你去車站寄行李,櫃台人員檢查完箱子「確實是行李箱」就放進置物櫃,等你來領的時候,卻只看你手上那張領取單寫的「請送到 XX 房間」,完全不再核對箱子跟地址對不對得上,你覺得會發生什麼事?這次 Bricksforge 的漏洞,玩的就是這套把戲 🧋
Bricksforge 的 Pro Forms(進階表單)有檔案上傳功能。訪客選好檔案之後,外掛會先把它放進一個叫 wp-content/uploads/bricksforge/tmp/ 的「暫存置物櫃」,這時候它真的有檢查——確認檔案長得像一張圖片(技術上叫 MIME 檢查)。問題出在第二步:訪客按下「送出表單」時,會附上一張「領取單」(temporaryFileUploads 這個參數),裡面寫著檔案放在哪、最後要叫什麼名字、網址是什麼。Patchstack 的分析指出,外掛對領取單上的 url 欄位完全沒有再驗證一次,攻擊者就能先放一個「圖片」進去,再把領取單改成「請把內容放到 .php 結尾的位置」,伺服器就乖乖照辦了 🧧
那個「長得像圖片、其實夾帶 PHP 程式碼」的檔案,資安圈叫它 GIF/PHP polyglot。你可以把它想成一張「雙面身分證」:給保全看的那一面是合法的圖片,給伺服器執行時看的那一面卻是一段程式 🪪
🧊 冷知識:polyglot 這個字的本意是「通曉多國語言的人」
放到檔案上,就是「同時符合兩種格式」的檔案:開頭幾個位元組是合法的 GIF 檔頭,後面卻接著 PHP 程式碼。檢查程式只看開頭就點頭放行,PHP 直譯器卻不管前面是什麼,只會執行它看得懂的那一段。這招不是新發明,已經是檔案上傳類漏洞裡的「經典劇本」了,每隔幾年就會換個外掛重演一次 🎭
整個攻擊流程,Patchstack 拆成四步,用大白話對照一下,順便標出誰是「已經被證實」的事:
- ㊀ 先跟網站要一張「號碼牌」(nonce)。外掛有一個不需要登入的 AJAX 入口 bricksforge_regenerate_nonce,任何人都能呼叫它拿到有效的 nonce。nonce 這個字是「number used once」的縮寫,原本的設計意圖就是當一次性的通行憑證 ✅
- ㊁ 上傳那張「雙面身分證」。GIF/PHP polyglot 通過 MIME 檢查,被放進暫存目錄 ✅
- ㊂ 送出表單時,把領取單改掉。檔案路徑寫圖片、url 欄位卻以 .php 結尾 ✅
- ㊃ 外掛信了領取單,把內容放到 .php 的位置。只要有人去請求那個檔案,伺服器就會執行裡面的 PHP ✅
⏱️ 這件事跑得多快?時間軸一次看
把幾個來源交叉比對後,整理出下面這張表,你可以發現「公開」跟「被攻擊」之間幾乎沒有空窗期:
| 時間 | 發生了什麼事 | 可信度 |
|---|---|---|
| 2026-09-03 | CVE 編號被保留(VulDB 記錄的 reserved 日期) | ✅ 已確認 |
| 2026-10-07 21:47:17 UTC | Patchstack 遙測到第一筆野外利用,換算台灣時間是 10 月 8 日 05:47 | ✅ 已確認 |
| 2026-10-08 | CVE 與 NVD 條目公開、Patchstack 發布分析文章、官方修補版 3.1.8.10 可用 | ✅ 已確認 |
| 2026-10-08(UTC 08:17~11:39) | 一位站長在 Bricksforge 官方論壇回報:他的客戶站在 4.0.0 版被丟進多個 polyglot GIF,分兩波,相隔約一小時 | ✅ 論壇貼文原文 |
| 發現者 | 兩份 AI 報告提到研究員代號 d.v4n_s3c,但沒查到原始頁面佐證 | 【❓待確認】 |
😦 碎碎念:CVSS 為什麼有人寫 9.8、有人寫 10.0?
CVE 官方紀錄與 NVD 收錄的是 CVSS 3.1 的 9.8,Patchstack 資料庫頁面卻標 10.0。哪一邊算錯?都不是,不同機構的評分算法與預設假設本來就可能不同。至於為什麼差在這 0.2 分,沒有查到官方說明,所以不亂猜【❓待確認】。重點是兩個數字都落在「嚴重」等級,不影響你「今天就要處理」的結論 🚦
那它被成功利用之後,最壞會走到哪一步?照事實確定的程度,一層一層講給你聽:
- ✅ 已確認事實:不用登入、不用使用者點任何東西,就能上傳並執行任意 PHP(CVSS 向量 AV:N/AC:L/PR:N/UI:N)。
- ✅ 已確認事實:Patchstack 明說「正被實際攻擊中」,並記錄到 63 個不同來源 IP;其中一群有 44 個 IP 在幾秒內送出 56 筆內容一模一樣的請求,另一群 3 個 IP 針對同一個暫存檔測了 25 種副檔名與編碼變形。
- 🟡 合理推論:成功執行 PHP 之後,攻擊者通常會放網頁後門(web shell)、讀 wp-config.php 拿到資料庫帳密、新增管理員帳號。Patchstack 也提到這幾種後果,但「某個特定網站已經發生」要靠你自己查證。
- 🔴 假設情境:如果同一台主機上的多個網站共用權限、沒有隔離,一個網站被打穿,其他網站也可能受牽連。這要看你的 Linux 使用者與 PHP-FPM 設定,不能直接等同「整台主機淪陷」。
🎭 第二章:先別慌,看看你屬於哪一種情境
🩵 荷包試算:更新本身要花錢嗎?
Bricksforge 是付費外掛,官方文件說購買後從客戶後台(Customer Dashboard)取得最新版本。授權過期之後還能不能下載安全更新,沒有查到條款【❓待確認】,建議先登入你的帳號確認。至於 Patchstack 的 RapidMitigate 虛擬修補規則,屬於他們的付費服務,方案與價格請看官網【❓待確認】。相較之下,真正會燒錢的是「已經中招之後」的鑑識、清後門與商譽修復,通常比現在花半小時更新貴很多 💸
漏洞這種事,同樣一份新聞稿,小編、自架玩家、公司 IT 該做的事完全不一樣。把四種常見的人拆開來聊,你直接對號入座就好 👇
🙋 我只是網站小編,平常在網站後台只會發文
你要做的事其實只有三件:確認版本、更新、不會就找人。後面的技術章節你可以放心跳過,直接看第三章的「五分鐘無痛自救指南」。這裡有個特別要先知道的差別:Bricksforge 不是從 WordPress 官方外掛庫下載的,後台沒跳出「有可用更新」不代表你已經安全,原因第三章會講 🧋
👨💻 我是自架玩家,手上有幾個站
你的重點是「速度」與「不要漏掉任何一站」。第五章、第六章有針對 HestiaCP 寫好的盤點、備份、更新、驗證流程,單一網站、獨立多站、Multisite 三種架構都各有一套,貼上就能用。要提醒的是,你會是最容易踩到「更新完就以為沒事」這個坑的人,下面第四種情境我們會細講 📜
🏢 我的網站有會員資料或線上收款
如果你的 Bricksforge 是 3.1.8.9 或更舊,這件事已經不只是「該更新」,而是「該確認有沒有已經被進來過」。因為 Patchstack 的資料顯示攻擊在公開當天就開始,只要你的網站曾經以舊版暴露在網路上,更新只能擋住「下一次」,擋不了「已經發生的事」。涉及個資的話,建議同步啟動公司內部的資安事件流程,保留記錄,之後不管是對保險公司、客戶還是主管機關都用得上 🩺
🚨 高風險情境:沒有上傳欄位,也沒開檔案上傳功能
先講一個真實案例,這是 Bricksforge 官方論壇 2026 年 10 月 8 日的一則貼文。一位站長說他的客戶站裝的是 4.0.0,當時「已是最新版」,整站外掛也都有更新,表單只有一個沒附件的聯絡表單。結果主機商的惡意程式掃描器在 wp-content/uploads/bricksforge/tmp/ 抓到六個檔案(polyglot.gif、polyglot-1.gif 到 polyglot-4.gif,還有一個 brf_4161.gif),分兩波被丟進去 🪤
他很幸運:主機本來就封鎖 uploads 目錄執行 PHP,所以檔案沒被執行。他的存取紀錄裡還有幾筆別的 IP 在抓 README.txt——那是攻擊者在「確認你裝的是哪個版本」,其中一筆還回了 200。
🚨 畫重點!千萬別踩雷:
有人以為「我的表單根本沒放上傳欄位,應該不會中」。這個想法在這起事件裡已經被打臉了:上面那位站長的網站沒有任何上傳欄位,照樣被寫入檔案。Patchstack 也寫明攻擊打的是 /wp-json/bricksforge/v1/form_submit 這條 REST 路徑,跟你有沒有放上傳欄位沒有直接關係。只要外掛啟用著、版本落在受影響範圍,就要視為暴露 🙅♂️
🧑🔧 DevOps 小知識:更新後還要多看兩件事
第一,3.1.8.10 是 Patchstack 與 CVE 紀錄確認的修補版;但 4.x 這邊,Patchstack 與 NVD 都只寫「≤ 3.1.8.9 受影響」,並沒有列出 4.x 的修補版。三份 AI 報告提到官方 4.0.1 的更新日誌有「Pro Forms 檔案上傳安全修補」,但沒能把那一條查出來【❓待確認】,而上面的論壇案例又證明 4.0.0 被打過。所以 4.x 的務實做法是:直接升到你客戶後台目前提供的最新版,不要停在 4.0.0。第二,如果舊版曾經暴露過,更新完還要做後門排查,這部分第五章會一步步帶你做 🛡️
🛟 第三章:五分鐘無痛自救指南(電腦小白也能跟著做)
不管你屬於哪一種情境,這一段都建議先走一遍,整個過程不需要碰任何指令,跟著畫面點就好 🤏
- ㊀ 先去哪裡看版本?
登入 WordPress 後台,左邊選單點「外掛」→「已安裝的外掛」,往下找名字是 Bricksforge 的那一列,名稱下方會寫「版本 3.x.x.x」。找不到這個外掛的話,代表你沒裝,這個漏洞跟你無關 🔢 - ㊁ 看到版本號,怎麼判斷安不安全?
最好辨識的規則是:版本號最前面是 3,而且後面比 3.1.8.10 小(例如 3.1.8.9、3.1.8.6)就是受影響;剛好 3.1.8.10 或更大的 3.x 是已修補。版本號開頭是 4 的話,看起來像「很新」,但請當作「還沒確認安全」,直接去拿最新版(原因在上一章的 DevOps 小知識)🚥 - ㊂ 要更新,但「更新」按鈕沒出現怎麼辦?
Bricksforge 不是 WordPress 官方外掛庫的外掛,更新來源是廠商自己的系統,所以你的後台不一定會顯示「立即更新」。這時候請登入 Bricksforge 官方的客戶後台(Customer Dashboard),下載最新版的 ZIP 檔,再回到 WordPress 後台點「外掛」→「安裝外掛」→「上傳外掛」,選擇那個 ZIP。WordPress 發現同名外掛時,會問你要不要「取代目前版本」,選取代就可以了 📦 - ㊃ 更新前,要備份嗎?
要,而且動手前最好先做。最簡單的方式是請主機商幫你做快照,或是在主機控制台按「備份」。如果你的主機是 HestiaCP,登入控制台後在「備份」頁面點新增備份即可。不想動手的話,把這篇文章網址丟給幫你維護的人也可以 🛟 - ㊄ 更新完,怎麼確認有沒有「已經被進來過」?
請做兩個檢查。第一,到「使用者」→「所有使用者」,點上面的「管理員」篩選,看名單裡有沒有你不認識的名字,特別是像亂碼的帳號名(例如一串英文字母加數字)、或是用你沒看過的信箱註冊的。第二,如果你的主機控制台有「檔案管理員」,進到 public_html/wp-content/uploads,用搜尋欄輸入 .php。這個資料夾正常只會有圖片、PDF 這類檔案,只要搜到任何 PHP 檔,尤其是檔名長得像 login_admin_xxxx.php 的,就是強烈警訊 🔍 - ㊅ 判斷不出來,或發現可疑東西,怎麼辦?
先不要自己刪,也先別慌。把畫面截圖,連同這篇文章網址一起,直接傳給幫你維護網站的工程師,或是主機商客服,跟他們說「我的外掛程式 Bricksforge 有 CVE-2026-85097 漏洞,請幫我更新並檢查有沒有後門」。看不懂後台、判斷不出來都是很正常的事,不是你的問題,找人幫忙是標準做法 🙇
🫂 新手求助:官方的求助管道在哪?
Bricksforge 有自己的社群論壇(forum.bricksforge.io),本篇提到的 4.0.0 受害案例就是發在「Bug Reports」版。你也可以在論壇搜尋最新的安全公告。要快速查漏洞資訊,Patchstack 與 NVD 都有 CVE-2026-85097 的專頁,文末參考資料都附了連結 🆘
✅ 如果你現在真的更新不了,最乾淨的暫時辦法是:後台「外掛」→「已安裝的外掛」,把 Bricksforge 按「停用」。停用之後,攻擊用的那些入口就不再被載入,等你拿到新版再重新啟用。代價是網站上用 Bricksforge 做的功能(例如 Pro Forms 表單)會暫時失效,這個取捨你得自己拿捏 🧯
🤿 第四章:技術深潛篇——這條攻擊鏈在伺服器上到底留下了什麼痕跡?
接下來是給有 SSH 權限、想知道「為什麼會這樣、怎麼查」的人看的內容。只想補洞的話,前三章已經夠用了;想知道細節的,我們繼續往下拆 ⚔️
| 評估指標 | 具體資訊 |
|---|---|
| 外掛名稱 | Bricksforge(Bricks Builder 擴充外掛,非 WordPress.org 外掛庫) |
| CVE 編號 | CVE-2026-85097 |
| CVSS | 9.8(CVE/NVD);Patchstack 頁面標 10.0 |
| CVSS 向量 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 弱點類型 | CWE-434:不受限制地上傳危險類型檔案 |
| 受影響版本 | ≤ 3.1.8.9 |
| 官方確認的修補版 | 3.1.8.10(Patchstack 明列) |
| 4.x 狀況 | 【❓待確認】4.0.0 有受害回報;4.0.1 的修補說明僅見於 AI 報告引述,未能自行查到 |
| 漏洞程式位置 | includes/elements/pro-forms/actions/init.php、base.php(Patchstack 標示) |
| 是否已遭利用 | ✅ 是(Patchstack 遙測);CISA KEV 目前仍標「否」 |
Patchstack 整理出的攻擊特徵,其實就是我們排查時要找的「指紋」。先它們歸類成下面幾種,後面每個排查指令都會用到這張清單 🔎
- 入口路徑:絕大多數打向 POST /wp-json/bricksforge/v1/form_submit,只有一筆走 /wp-admin/admin-ajax.php(action 為 bricksforge_form_submit)。論壇受害者另外看到 /wp-json/bricksforge/v1/upload。
- 落地位置:多數 payload 指向 /wp-content/uploads/bricksforge/tmp/;少數把檔案放到一般的 /wp-content/uploads/2026/10/,檔名符合 login_admin_*.php。
- 副檔名花招:.php、.php5~.php8、.phtml、.pht、.phtm、.phps,加上大小寫混用(.PHP、.pHp)以及編碼過的 %2ephp、.%70hp、.%2570%2568%2570。
- 最關鍵的特徵:「伺服器端路徑是 .gif/.png,目的網址卻是 PHP」,在 Patchstack 看到的請求裡,95% 的伺服器端路徑以 .gif 或 .png 結尾。
🧑🔧 DevOps 小知識:為什麼「grep temporaryFileUploads」在 access log 裡多半找不到?
好幾份分析報告都建議用 grep temporaryFileUploads 去搜 access log。但這個參數是放在 POST 的 JSON 內容(request body)裡,Nginx 與 Apache 預設的 combined 日誌格式不會記錄 body,所以你幾乎一定搜不到,而且「搜不到」會讓人誤以為沒被打(這是依日誌格式做的推論,除非你有 WAF 或 ModSecurity 稽核日誌在記 body 🟡)。比較可靠的做法是從「路徑」下手:找對 /wp-json/bricksforge/v1/ 的 POST、對 uploads 底下 .php 檔的請求、以及 README.txt 的探測。這也是第五章 Log 排查的邏輯 🧠
🧊 冷知識:WordPress 的 uploads 資料夾,本來就不該執行任何程式
這個資料夾的設計初衷是放媒體檔案。很多自架玩家都會在伺服器層直接禁止它執行 PHP。論壇那位 4.0.0 受害的站長就是靠這個設定,讓六個 polyglot 檔「躺在那裡但永遠不會被執行」。這也是為什麼會在後面的加固步驟,把「禁止 uploads 執行 PHP」排在很前面——它不是修補漏洞,而是讓「下一個類似漏洞」的殺傷力直接歸零 🧯
🕵️ 第五章:資安排查 Checklist——怎麼確認自己「有沒有已經被進來過」?
🤖 重要提醒:以下指令與排查流程由 ChatGPT、Gemini、Meta AI、Grok、DeepSeek 的分析報告整合,並由 Claude 逐項複查、依 HestiaCP 官方文件與 WP-CLI 官方文件改寫修正
在整合時抓到不少原始報告的問題:有人把 Nginx 的 location 寫進全域設定(那裡不能放)、有人把 curl_exec 加進 disable_functions(WordPress 自己就靠它對外連線)、有人拿 Bricksforge 去跑只支援 WordPress.org 外掛的 Checksum 指令、還有人用不存在的 REST 路徑當驗證。這些都已經改掉,但你的主機環境不一定一樣。正式環境(尤其是正在營業中的網站)的高風險操作——包含更新、停用、刪除檔案、變更權限或執行批次腳本——請務必由熟悉該站架構的專業人員進行最後確認與執行,切勿直接複製貼上後就按下 Enter 🧙
先說這一章的地圖:環境判斷 👉 盤點(版本比對)👉 Log 日誌排查 👉 後門檢查 👉 加固檢查 五關,每一關都分成「為什麼這樣做、指令、預期輸出、看到什麼該怎麼辦」,而且單一網站、獨立多站、Multisite 三種架構都各有對應。本文預設環境是 Debian 12 + HestiaCP,網站放在 Hestia 的標準位置 /home/使用者/web/網域/public_html,並假設跟 Hestia 預設一樣是 Apache + Nginx 反向代理的組合 🧭
🧰 ㊀ 先貼這一段:整章都會用到的小工具
這段只是定義兩個小函式,不會動到任何網站。請在同一個 SSH 連線裡先貼一次,連線中斷後要重貼。wpu 的作用,是讓 WP-CLI 一律用「網站擁有者」的身分執行,而不是 root,避免把檔案權限搞亂;如果你的 WP-CLI 是用 Hestia 的 v-add-user-wp-cli 裝在使用者家目錄,它也找得到 🪛
# 以網站擁有者的身分執行 WP-CLI:wpu 擁有者 網站路徑 wp指令...
wpu() {
local owner="$1" wp_path="$2"
shift 2
sudo -u "$owner" -H -- env PATH="/home/$owner/.wp-cli:$PATH" \
wp --path="$wp_path" "$@"
}
# 由網站路徑反推「擁有者」與「網域」(Hestia 標準路徑才適用)
site_owner() { stat -c '%U' -- "$1/wp-config.php"; }
site_domain() { basename -- "$(dirname -- "$1")"; }
# 版本分級:3.x 以 3.1.8.10 為界;4.x 先視為「待確認」
FIXED_3X="3.1.8.10"
classify_version() {
local v="$1"
if [ -z "$v" ]; then
echo "未安裝"
elif dpkg --compare-versions "$v" lt "$FIXED_3X"; then
echo "🔴 受影響(低於 $FIXED_3X)"
elif dpkg --compare-versions "$v" ge "4.0.0"; then
echo "🟠 4.x:請升到廠商最新版(修補版號待確認)"
else
echo "🟢 已達 3.x 修補版"
fi
}
🧑🔧 DevOps 小知識:為什麼後面的迴圈都長成 3< <(find …) 這個樣子?
常見寫法是 find | while read,但迴圈裡只要有任何指令會讀標準輸入(sudo、mysql 都有可能),就會把 find 輸出的清單吃掉一半,結果只處理到第一個網站。把 find 的輸出接到檔案描述符 3,再用 read … <&3 讀,迴圈內就不會互相搶輸入了;加上 -print0 與 read -d ”,路徑含空格也不會斷掉 🔧
🧭 ㊁ 環境判斷:你在管一個 WordPress,還是一整台 WordPress 主機?
為什麼這樣做:「一台主機上有 10 個 WordPress」跟「一個 Multisite 裡有 10 個子站」長得很像,其實完全不同。前者要逐一找每個 wp-config.php,後者外掛檔案全網只有一份。判斷錯了,後面的備份、更新、驗證範圍都會跟著跑偏 🤔
# 💠 單一網站/✴️ Multisite:先確認這個路徑真的是 WordPress,再判斷是不是 Multisite
WP_PATH="/home/admin/web/example.com/public_html"
OWNER="$(site_owner "$WP_PATH")"
if wpu "$OWNER" "$WP_PATH" core is-installed 2>/dev/null; then
echo "OK:偵測到 WordPress(擁有者:$OWNER)"
else
echo "ERROR:這個路徑不是 WordPress,先停手"
fi
if wpu "$OWNER" "$WP_PATH" core is-installed --network 2>/dev/null; then
echo "這是 Multisite 網路"
else
echo "不是 Multisite"
fi
grep -nE "MULTISITE|SUBDOMAIN_INSTALL" "$WP_PATH/wp-config.php" || echo "wp-config.php 沒有 MULTISITE 相關設定"
# ❇️ 獨立多站:列出主機上每一個 WordPress(每個 wp-config.php 就是一個獨立安裝)
while IFS= read -r -d '' CONFIG <&3; do
WP_PATH="$(dirname -- "$CONFIG")"
OWNER="$(stat -c '%U' -- "$CONFIG")"
if wpu "$OWNER" "$WP_PATH" core is-installed 2>/dev/null; then
if wpu "$OWNER" "$WP_PATH" core is-installed --network 2>/dev/null; then
KIND="Multisite"
else
KIND="一般安裝"
fi
printf '%s | 擁有者:%s | %s\n' "$WP_PATH" "$OWNER" "$KIND"
else
printf '【略過】%s 無法載入 WordPress,請人工確認\n' "$WP_PATH"
fi
done 3< <(sudo find /home -maxdepth 6 -type f -name 'wp-config.php' -print0 2>/dev/null)
預期輸出範例:
OK:偵測到 WordPress(擁有者:admin) 不是 Multisite wp-config.php 沒有 MULTISITE 相關設定 /home/admin/web/site-a.com/public_html | 擁有者:admin | 一般安裝 /home/bob/web/shop.example/public_html | 擁有者:bob | 一般安裝 /home/bob/web/network.example/public_html | 擁有者:bob | Multisite
看到什麼,該怎麼辦:
- 顯示「不是 Multisite」,而且 ❇️ 的清單只有一行 → 你是 💠 單一網站,後面照單站流程走。
- 清單有很多行,每行都是「一般安裝」→ ❇️ 獨立多站,後面每一站都要各自處理,不能拿 A 站結果代表 B 站。
- 出現「Multisite」→ ✴️ Multisite,外掛檔案屬於整個 Network,更新一次即可,但要逐 Site 驗證。
- 出現「【略過】」→ 先查 PHP、檔案權限或路徑,不要直接對它下更新指令。同一站如果被找到兩個 wp-config.php,多半是備份或測試副本,請人工確認哪個才是正式站。
🔎 ㊂ 盤點(版本比對):誰裝了它?現在是哪一版?
為什麼這樣做:版本號不寫死在腳本裡,改用上面的 classify_version 即時判斷。這一步只讀不寫,放心跑 🔍
# 💠 單一網站
WP_PATH="/home/admin/web/example.com/public_html"
OWNER="$(site_owner "$WP_PATH")"
VER="$(wpu "$OWNER" "$WP_PATH" plugin get bricksforge --field=version 2>/dev/null || true)"
STATUS="$(wpu "$OWNER" "$WP_PATH" plugin get bricksforge --field=status 2>/dev/null || true)"
printf '版本:%s | 狀態:%s | 判定:%s\n' "${VER:-無}" "${STATUS:-無}" "$(classify_version "$VER")"
# ❇️ 獨立多站:逐站盤點,結果會帶上網站路徑
while IFS= read -r -d '' CONFIG <&3; do
WP_PATH="$(dirname -- "$CONFIG")"
OWNER="$(stat -c '%U' -- "$CONFIG")"
wpu "$OWNER" "$WP_PATH" core is-installed 2>/dev/null || continue
VER="$(wpu "$OWNER" "$WP_PATH" plugin get bricksforge --field=version 2>/dev/null || true)"
[ -n "$VER" ] || continue
STATUS="$(wpu "$OWNER" "$WP_PATH" plugin get bricksforge --field=status 2>/dev/null || true)"
printf '%s | 版本:%s | 狀態:%s | %s\n' "$WP_PATH" "$VER" "$STATUS" "$(classify_version "$VER")"
done 3< <(sudo find /home -maxdepth 6 -type f -name 'wp-config.php' -print0 2>/dev/null)
# ✴️ Multisite:外掛檔案全網只有一份,查一次就代表整個 Network
WP_PATH="/home/bob/web/network.example/public_html"
OWNER="$(site_owner "$WP_PATH")"
VER="$(wpu "$OWNER" "$WP_PATH" plugin get bricksforge --field=version 2>/dev/null || true)"
printf '版本:%s | 判定:%s\n' "${VER:-無}" "$(classify_version "$VER")"
if wpu "$OWNER" "$WP_PATH" plugin is-active bricksforge --network; then
echo "Network 啟用:整個網路都在暴露範圍"
else
echo "未 Network 啟用:可能只有部分 Site 啟用,改用 --url 逐站確認"
fi
預期輸出範例:
/home/admin/web/site-a.com/public_html | 版本:3.1.8.9 | 狀態:active | 🔴 受影響(低於 3.1.8.10) /home/bob/web/shop.example/public_html | 版本:4.0.0 | 狀態:active | 🟠 4.x:請升到廠商最新版(修補版號待確認) /home/bob/web/blog.example/public_html | 版本:3.1.8.10 | 狀態:inactive | 🟢 已達 3.x 修補版
看到什麼,該怎麼辦:🔴 的站直接排進今天的修補清單;🟠 的站一樣要升級,同時視同「可能已暴露」;🟢 的站版本沒問題,但如果它舊版時期曾經對外開放,後面三關仍要做。狀態是 inactive 也不能完全放心:外掛檔案還在磁碟上,只是目前沒載入,有沒有可被直接請求的檔案,要靠後面的檔案檢查才知道 🟡
📊 ㊃ Log 日誌排查:找「路徑+特徵+來源」的組合,不要找 body
為什麼這樣做:前面說過,標準日誌不會記 POST 內容,所以改找三種痕跡:對 Bricksforge REST 路徑的 POST、對 uploads 底下 PHP 檔的請求、以及對 README.txt 的版本探測。HestiaCP 預設的網站日誌在 /var/log/nginx/domains/網域.log,輪替後的舊檔是 .log.1、.log.2.gz,所以用 zgrep(純文字與壓縮檔都能讀)。反向代理模式下,Nginx 的日誌才有真實訪客 IP,Apache 那份多半只會看到 127.0.0.1。如果你的網站前面還擋了一層 CDN,看到的可能是 CDN 的 IP 🔎
DOMAIN="example.com"
LOG_BASE="/var/log/nginx/domains/${DOMAIN}.log"
# ❶ 對 Bricksforge 路徑的 POST(admin-ajax 的 action 在 body 裡看不到,只能靠 rest_route 或路徑)
sudo zgrep -hiE '"POST [^ ]*(bricksforge|rest_route=[^ "]*bricksforge)' "$LOG_BASE"* 2>/dev/null | tail -n 50
# ❷ 對 uploads 底下 PHP 類檔案的任何請求(最強的入侵指標)
sudo zgrep -hiE '"(GET|POST|HEAD) [^ ?]*/wp-content/uploads/[^ ?]*\.(php[0-9]*|phtml|pht|phtm|phps|phar)([ ?/.]|$)' "$LOG_BASE"* 2>/dev/null | tail -n 50
# ❸ 版本探測:有人在讀 readme.txt(資訊性,代表你被掃過)
sudo zgrep -hiE '"GET [^ ]*/wp-content/plugins/bricksforge/readme\.txt' "$LOG_BASE"* 2>/dev/null | tail -n 20
# ❹ 打 Bricksforge 路徑最頻繁的來源 IP
sudo zgrep -hiE '"POST [^ ]*bricksforge' "$LOG_BASE"* 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 20
# ❇️ 獨立多站/✴️ Multisite:掃過所有網域的日誌,只印出「有命中」的檔案
while IFS= read -r -d '' LOG <&3; do
HITS="$(sudo zgrep -hiE '"POST [^ ]*bricksforge|"(GET|POST|HEAD) [^ ?]*/wp-content/uploads/[^ ?]*\.(php[0-9]*|phtml|pht|phtm|phps|phar)([ ?/.]|$)' "$LOG" 2>/dev/null || true)"
if [ -n "$HITS" ]; then
printf '\n===== %s =====\n' "$LOG"
printf '%s\n' "$HITS" | tail -n 20
fi
done 3< <(sudo find /var/log/nginx/domains -maxdepth 1 -type f \( -name '*.log' -o -name '*.log.*' \) ! -name '*.error.log*' -print0 2>/dev/null)
預期輸出範例(可疑的樣子):
203.0.113.42 - - [08/Oct/2026:05:51:03 +0800] "POST /wp-json/bricksforge/v1/form_submit HTTP/1.1" 200 312 "-" "python-requests/2.31.0" 203.0.113.42 - - [08/Oct/2026:05:51:09 +0800] "GET /wp-content/uploads/2026/10/login_admin_a1b2c3.php HTTP/1.1" 200 41 "-" "python-requests/2.31.0"
看到什麼,該怎麼辦:
- ❶ 有命中但都是正常訪客、頻率低 → 先確認是不是你自己網站的表單在運作(Pro Forms 正常送出本來就會打 form_submit);同一個 IP 短時間大量重複、使用 python-requests 或 curl 這類 User-Agent → 提高警覺。
- ❷ 有命中,而且狀態碼是 200 → 高度疑似已經被成功利用,直接跳到第六章「A. 已中招」,先別更新。
- ❸ 有命中 → 代表被掃描過,不等於被打穿,但請把後面的檔案檢查做完。
- 完全沒有命中 → 只能說「目前沒有直接證據」,不能證明沒被打過,因為日誌可能已經輪替,或前面有 CDN 擋掉了真實紀錄。繼續往下做檔案檢查。
🕳️ ㊄ 後門檢查:光是版本正確,不代表整站乾淨
為什麼這樣做:Patchstack 看到的 payload 多半落在 uploads/bricksforge/tmp/,少數會把 PHP 放進一般 uploads 資料夾。除此之外,攻擊者拿到執行權限後,常見的持久化位置還有 mu-plugins(不用在後台啟用就會自動載入)、新增的管理員帳號、排程任務。以下每一項都只讀不寫,也不會刪任何東西 🕵️♀️
# 💠 單一網站:檔案層檢查
WP_PATH="/home/admin/web/example.com/public_html"
UPLOADS="$WP_PATH/wp-content/uploads"
SINCE="2026-10-01" # 比首次野外利用(10/7)早幾天,留一點緩衝
echo "=== ❶ uploads 底下不該存在的可執行副檔名(正常網站應該是空的)==="
sudo find "$UPLOADS" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.pht' -o -iname '*.phar' \) \
-printf '%TY-%Tm-%Td %TH:%TM %u:%g %m %p\n' 2>/dev/null | sort
echo "=== ❷ Bricksforge 暫存目錄裡所有檔案 ==="
if [ -d "$UPLOADS/bricksforge/tmp" ]; then
sudo find "$UPLOADS/bricksforge/tmp" -type f \
-printf '%TY-%Tm-%Td %TH:%TM %u:%g %m %p\n' | sort
else
echo "暫存目錄不存在(不代表安全)"
fi
echo "=== ❸ 圖片檔裡夾帶 PHP 標籤(polyglot 特徵,僅作線索)==="
sudo find "$UPLOADS" -type f \
\( -iname '*.gif' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \) \
-newermt "$SINCE" -exec grep -laE '<\?php|<\?=' {} + 2>/dev/null
echo "=== ❹ mu-plugins 目錄 ==="
if [ -d "$WP_PATH/wp-content/mu-plugins" ]; then
sudo find "$WP_PATH/wp-content/mu-plugins" -maxdepth 2 -type f -print
else
echo "mu-plugins 目錄不存在"
fi
echo "=== ❺ wp-content 底下 $SINCE 之後被修改過的 PHP(外掛正常更新也會出現,需人工比對)==="
sudo 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
# 💠 單一網站:資料庫與帳號層檢查
WP_PATH="/home/admin/web/example.com/public_html"
OWNER="$(site_owner "$WP_PATH")"
echo "=== ❻ 管理員帳號(留意不認識的、註冊時間落在 10/7 前後的)==="
wpu "$OWNER" "$WP_PATH" user list --role=administrator \
--fields=ID,user_login,user_email,user_registered --format=table
echo "=== ❼ 排程任務(留意陌生的 hook 名稱)==="
wpu "$OWNER" "$WP_PATH" cron event list --fields=hook,next_run_relative,recurrence --format=table
echo "=== ❽ wp_options 裡含 base64_decode 或 eval 的選項(只是線索,很多正常外掛也會有)==="
PREFIX="$(wpu "$OWNER" "$WP_PATH" db prefix)"
QUERY_FILE="$(mktemp)"
cat > "$QUERY_FILE" <<'EOF'
SELECT option_name, LENGTH(option_value) AS value_length, autoload
FROM __P__options
WHERE option_value LIKE '%base64_decode(%'
OR option_value LIKE '%eval(%'
ORDER BY option_id DESC
LIMIT 30;
EOF
sed -i "s/__P__/${PREFIX}/g" "$QUERY_FILE"
wpu "$OWNER" "$WP_PATH" db query < "$QUERY_FILE"
rm -f -- "$QUERY_FILE"
# ❇️ 獨立多站:每站各自檢查 uploads 與 mu-plugins,結果帶網站路徑(不要拿 A 站結果代表 B 站)
while IFS= read -r -d '' CONFIG <&3; do
WP_PATH="$(dirname -- "$CONFIG")"
OWNER="$(stat -c '%U' -- "$CONFIG")"
HITS="$(sudo find "$WP_PATH/wp-content/uploads" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.pht' -o -iname '*.phar' \) -print 2>/dev/null || true)"
MU="$(sudo find "$WP_PATH/wp-content/mu-plugins" -maxdepth 2 -type f -print 2>/dev/null || true)"
if [ -n "$HITS$MU" ]; then
printf '\n===== 可疑:%s(擁有者:%s)=====\n' "$WP_PATH" "$OWNER"
[ -n "$HITS" ] && printf '[uploads 內的 PHP]\n%s\n' "$HITS"
[ -n "$MU" ] && printf '[mu-plugins]\n%s\n' "$MU"
fi
done 3< <(sudo find /home -maxdepth 6 -type f -name 'wp-config.php' -print0 2>/dev/null)
echo "掃描完成:上面沒有列出的網站,代表這兩項檢查沒有發現異常"
# ✴️ Multisite:檔案檢查跟單一網站一樣只做一次(uploads 在 sites/ 子目錄下,find 會一併掃到)
# 帳號則要分兩層看:先看 Network 超級管理員,再逐 Site 看管理員
WP_PATH="/home/bob/web/network.example/public_html"
OWNER="$(site_owner "$WP_PATH")"
wpu "$OWNER" "$WP_PATH" super-admin list
while IFS= read -r SITE_URL; do
printf '\n===== %s =====\n' "$SITE_URL"
wpu "$OWNER" "$WP_PATH" user list --url="$SITE_URL" --role=administrator \
--fields=ID,user_login,user_email,user_registered --format=table
done < <(wpu "$OWNER" "$WP_PATH" site list --field=url)
預期輸出範例:
=== ❶ uploads 底下不該存在的可執行副檔名(正常網站應該是空的)=== 2026-10-08 05:52 admin:admin 644 /home/admin/web/example.com/public_html/wp-content/uploads/2026/10/login_admin_a1b2c3.php === ❻ 管理員帳號 === +----+------------+-------------------+---------------------+ | ID | user_login | user_email | user_registered | +----+------------+-------------------+---------------------+ | 1 | admin | admin@example.com | 2024-01-15 10:00:00 | | 87 | x9k2m1 | tmp@mailinator.example | 2026-10-08 05:53:11 | +----+------------+-------------------+---------------------+
看到什麼,該怎麼辦:
- ❶ 找到任何 PHP 類檔案 → 視為已中招。先別刪,也別更新,直接進第六章 A,那裡有取證與隔離流程。
- ❷ 暫存目錄裡只有近期的正常圖片、沒有其他 → 低風險;出現 polyglot 之類可疑檔名(像論壇案例的 polyglot.gif)→ 一樣先保留證據。
- ❸ 命中的圖片是線索不是定論:有些正常圖片的 EXIF 欄位也可能含有 <?php 字樣,需要人工打開確認。
- ❹ mu-plugins 出現不是你們部署的 PHP 檔,檔名像亂碼 → 高度可疑。
- ❺ 出現不認識的管理員,特別是註冊時間落在 2026-10-07 之後 → 高度可疑。但「陌生」不等於「惡意」,可能是外包或維運商留下的,先問清楚再處理。
- 全部乾淨 → 這一輪沒有發現明確跡象,仍建議做下一關的加固,並持續觀察一週的日誌。
🫂 新手求助:我只有 Hestia 控制台,不會 SSH 怎麼辦?
HestiaCP 的網頁控制台在「網站」→ 你的網域 → 「檔案管理員」(若有啟用)可以用滑鼠看 wp-content/uploads 裡有沒有 .php。看不懂或不確定的話,把整章指令連同這篇文章丟給你的主機商或維運廠商,請他們幫你跑一次,這不是你的責任,找人是正常做法 📚
🔧 ㊅ 加固檢查:就算修好了,也把第二道門鎖起來
為什麼這樣做:更新外掛才是根本,但這起漏洞的教訓是「檔案被寫進 uploads,然後被當程式執行」這兩步各自都有機會被擋下。論壇那位 4.0.0 的站長,就是因為 uploads 禁止執行 PHP,才讓攻擊停在第一步。以下三件事,每一件都是「通用加固」,不是 CVE 官方要求 🛡️
❶ 在 Nginx 層禁止 uploads 底下的 PHP 類檔案被請求
HestiaCP 的網站設定檔會在你「重建網域」或續約 SSL 時被樣板覆蓋,所以千萬不要直接改 nginx.conf。官方提供的做法是在 /home/使用者/conf/web/網域/ 底下放 nginx.conf_任意名稱(HTTP)與 nginx.ssl.conf_任意名稱(HTTPS),樣板會自動載入,而且不會被覆蓋。這兩個檔案是放在 server 區塊裡面,所以可以直接寫 location,這點跟放在 /etc/nginx/conf.d/ 不一樣——那裡是全域(http 區塊),放 location 會讓 Nginx 直接報錯 🧱
# 定義一個函式:deny_uploads_php 使用者 網域
deny_uploads_php() {
local owner="$1" domain="$2"
local conf_dir="/home/$owner/conf/web/$domain"
local http_inc="$conf_dir/nginx.conf_deny_uploads_php"
local ssl_inc="$conf_dir/nginx.ssl.conf_deny_uploads_php"
if [ ! -d "$conf_dir" ]; then
printf '找不到 %s,略過\n' "$conf_dir"
return 1
fi
sudo tee "$http_inc" > /dev/null <<'EOF'
# 加固:uploads 底下一律不准請求 PHP 類副檔名(含 x.php.gif 這種雙副檔名)
location ~* ^/wp-content/uploads/.*\.(php[0-9]*|phtml|pht|phtm|phps|phar)(\.|/|$) {
return 403;
}
EOF
sudo ln -sf "$http_inc" "$ssl_inc"
printf '已建立:%s(以及 HTTPS 用的連結)\n' "$http_inc"
}
# 💠 單一網站(以 admin 使用者的 example.com 為例)
deny_uploads_php "admin" "example.com"
# 測試設定語法,通過才重新載入;失敗就不要 reload
sudo nginx -t && sudo systemctl reload nginx
# ❇️ 獨立多站/✴️ Multisite:每個 Hestia 網域各做一次(Multisite 通常只有一個網域,子站共用)
while IFS= read -r -d '' CONFIG <&3; do
WP_PATH="$(dirname -- "$CONFIG")"
deny_uploads_php "$(site_owner "$WP_PATH")" "$(site_domain "$WP_PATH")"
done 3< <(sudo find /home -maxdepth 6 -type f -name 'wp-config.php' -path '*/web/*/public_html/wp-config.php' -print0 2>/dev/null)
sudo nginx -t && sudo systemctl reload nginx
預期輸出範例:
已建立:/home/admin/conf/web/example.com/nginx.conf_deny_uploads_php(以及 HTTPS 用的連結) nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
分支判斷邏輯:測試通過 → 已生效;nginx -t 報錯 → 不會 reload,網站不受影響,把剛建的兩個檔案刪掉再查原因;你的網站是純 Nginx(沒有 Apache 在後面)→ 樣板裡的 PHP 處理規則順序可能比這條早,需要改用 Hestia 的自訂網站樣板,這部分沒有實機驗證【❓待確認】。要復原也很簡單:刪掉那兩個檔案再 reload 就好 ♻️
❷ 在 Apache 層再補一道(縱深防禦)
# 在 uploads 放 .htaccess(已經有就不覆蓋):htaccess_deny_php 擁有者 網站路徑
htaccess_deny_php() {
local owner="$1" up="$2/wp-content/uploads"
if [ ! -d "$up" ]; then
printf '找不到 %s,略過\n' "$up"
return 1
fi
if [ -e "$up/.htaccess" ]; then
printf '%s/.htaccess 已存在,請人工合併,不覆蓋\n' "$up"
return 1
fi
sudo -u "$owner" -- tee "$up/.htaccess" > /dev/null <<'EOF'
# 加固:uploads 底下禁止 PHP 類檔案
<FilesMatch "(?i)\.(php[0-9]*|phtml|pht|phtm|phps|phar)(\.|$)">
Require all denied
</FilesMatch>
EOF
printf '已建立:%s/.htaccess\n' "$up"
}
# 💠 單一網站
htaccess_deny_php "admin" "/home/admin/web/example.com/public_html"
🧑🔧 DevOps 小知識:為什麼 .htaccess 只能當第二道?
.htaccess 放在網站目錄裡,攻擊者一旦能寫檔,就有機會在更深的子目錄丟一個自己的 .htaccess 把規則改掉;Nginx 那層在網站目錄之外,攻擊者的 PHP 碰不到。所以主力放 Nginx,.htaccess 只是補強。另外它只有在 Apache 允許 .htaccess 覆寫時才生效,這點同樣沒有逐台驗證【❓待確認】 🧩
❸ PHP:停用高風險函式(不要加 curl_exec)
# 對主機上所有已安裝的 PHP-FPM 版本做冪等替換(重複執行不會重複疊加)
while IFS= read -r -d '' INI <&3; do
VER="$(basename -- "$(dirname -- "$(dirname -- "$INI")")")"
sudo cp -- "$INI" "$INI.BAK.$(date +%F_%H%M%S)"
sudo sed -i -E 's/^[;#]*\s*disable_functions\s*=.*/disable_functions = exec,passthru,shell_exec,system,proc_open,popen/' "$INI"
sudo systemctl restart "php${VER}-fpm"
printf 'PHP %s 已更新並重啟:%s\n' "$VER" "$INI"
done 3< <(sudo find /etc/php -maxdepth 3 -type f -path '*/fpm/php.ini' -print0)
# 確認生效(php-fpm 的設定要用網頁請求看,CLI 的 php -i 看的是另一份 ini,所以改用 grep 讀檔)
sudo grep -H '^disable_functions' /etc/php/*/fpm/php.ini
說明:⚠️ 這份清單刻意不含 curl_exec、parse_ini_file:WordPress 的對外連線(外掛更新、wp-cron)要靠 curl,停用了網站會出問題;也不要把 eval 放進去,它是 PHP 的語言結構,不是函式,放進去沒有效果。部分備份或圖片處理外掛會用到 proc_open 或 exec,改完請實際測一次 🛡️
🧑🔧 DevOps 小知識:open_basedir 不用你再全域設定
有些報告建議在 php.ini 全域寫死 open_basedir,但你一寫死某個網站的路徑,其他網站全部壞掉。HestiaCP 的 PHP-FPM 樣板本來就會為每個網域設好 open_basedir(我看到的是樣板內容),你可以用下面這行確認自家主機有沒有:
sudo grep -H open_basedir /etc/php/*/fpm/pool.d/*.conf 🔒
❹ 檔案權限與鹽值(Salts):依嚴重程度決定要不要刷
刷鹽值就像把全公司的鎖芯換掉:所有人,包括可能已經偷到登入 Cookie 的攻擊者,都會被強制登出。因為這是「無需登入的遠端執行」、而且已經有實際攻擊,我的判斷標準是:舊版曾經對外暴露過(版本 ≤ 3.1.8.9 或 4.0.0),就刷;只是剛好裝了新版、從沒暴露過,就不用。刷完所有使用者(包含你自己)都要重新登入。此指令會連線 WordPress.org 取得新的隨機字串,主機要能對外連線 🔑
# 💠 單一網站:收緊 wp-config.php 權限 + 刷新鹽值(兩者缺一不可,順序也不要反) WP_PATH="/home/admin/web/example.com/public_html" OWNER="$(site_owner "$WP_PATH")" sudo cp -- "$WP_PATH/wp-config.php" "$WP_PATH/wp-config.php.BAK.$(date +%F_%H%M%S)" sudo chmod 640 -- "$WP_PATH/wp-config.php" stat -c '%a %U:%G %n' -- "$WP_PATH/wp-config.php" wpu "$OWNER" "$WP_PATH" config shuffle-salts
# ❇️ 獨立多站:只對「確定舊版暴露過」的站刷鹽值,請先把站名手動列進陣列,不要無差別全刷
EXPOSED_SITES=(
"/home/admin/web/site-a.com/public_html"
"/home/bob/web/shop.example/public_html"
)
for WP_PATH in "${EXPOSED_SITES[@]}"; do
OWNER="$(site_owner "$WP_PATH")"
printf '=== %s ===\n' "$WP_PATH"
sudo cp -- "$WP_PATH/wp-config.php" "$WP_PATH/wp-config.php.BAK.$(date +%F_%H%M%S)"
sudo chmod 640 -- "$WP_PATH/wp-config.php"
wpu "$OWNER" "$WP_PATH" config shuffle-salts
sleep 2
done
# ✴️ Multisite:Salt 屬於同一份 wp-config.php,對 Network 路徑執行一次即可,不要每個 Site 重刷
🚨 備份出來的 wp-config.php.BAK.時間 裡有資料庫密碼,而且會跟網站放在同一個資料夾裡。如果你的網站目錄可以被網頁直接下載,這份備份檔就是洩漏來源,請確認它在事後被移到網站目錄以外,或直接刪除(刪除前請再三確認)🪤
📋 ㊆ 三環境速查表:把五關濃縮成一張
| 階段 | 💠 單一網站 | ❇️ 獨立多站 | ✴️ Multisite |
|---|---|---|---|
| ① 環境判斷 | core is-installed 與 –network | 找出每一個 wp-config.php | –network 成功即是 |
| ② 盤點版本 | plugin get | 逐站迴圈,動態取擁有者 | 查一次代表全網,再確認 Network 啟用 |
| ③ Log 排查 | 單一網域日誌 | 掃所有網域日誌,只印有命中的 | 共用網域日誌,再依 URL 回推子站 |
| ④ 後門檢查 | 檔案+帳號+排程+options | 逐站各自檢查 | 檔案一次;帳號分 Network/Site 兩層 |
| ⑤ 加固 | Nginx 規則+php.ini+權限+Salt | 逐網域 Nginx 規則;Salt 只刷暴露過的站 | 規則一次;Salt 一次 |
🤔 讀到有點喘?幾個常見焦慮先幫你解掉
- 🌠 Q:我的 Bricksforge 已經是 4.0.0 最新版,是不是就安全了?
A:先別這樣想。官方論壇已經有 4.0.0 被丟進 polyglot 檔的回報,而 Patchstack 與 CVE 紀錄只確認 3.1.8.10 是修補版。4.x 請直接升到你客戶後台目前提供的最新版,再做一次第五章的檔案檢查 📦 - 🌟 Q:我的表單沒有上傳欄位,還需要處理嗎?
A:需要。論壇那個案例的網站就沒有任何上傳欄位,照樣被寫入檔案。只要外掛啟用著、版本在受影響範圍,就視為暴露 🙅 - ✨ Q:更新之後,網站表單會不會壞掉?
A:這種安全修補通常不會改你的版面,但表單類功能最怕細節變動。更新前先備份,更新後實際去前台送一份表單測試,是最保險的做法 💾 - 🌠 Q:我看不懂那些指令,是不是沒救了?
A:不會。第三章全程用滑鼠就能完成最基本的處理,第四到六章是給有 SSH 權限、想深入排查的人看的補充 🤗
🛠️ 第六章:已經中招、還沒中招、準備修補——三種狀況各走各的路
前面解決的是「怎麼判斷」,這一章才是真正的維運 Runbook。主線流程固定是 判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證,只是如果你在第五章發現了跡象,要先走 A(止血取證)再回到主線 🧹
🚨 ㊀ A. 已中招/疑似中招:先止血、留證據,再修補
為什麼這樣做:一旦第五章出現明確跡象(uploads 裡有 PHP、日誌有對 PHP 檔的成功請求、冒出陌生管理員),第一目標是「切斷入口、保留現場、隔離後門」,而不是急著更新。在沒留證據的情況下直接清理,會把最有價值的線索一起刪掉。還有一個現實要先講:如果確認攻擊者真的執行過程式,單純「刪一個可疑檔、更新外掛」常常不夠,因為他可能已經留下別的持久化管道。這種情況,用已知乾淨的備份還原,再更新、再驗證通常比逐個清理可靠 🛟
# 💠 單一網站:❶ 隔離 + ❷ 取證(取證完成前,什麼都不要刪)
WP_PATH="/home/admin/web/example.com/public_html"
OWNER="$(site_owner "$WP_PATH")"
DOMAIN="$(site_domain "$WP_PATH")"
UPLOADS="$WP_PATH/wp-content/uploads"
STAMP="$(date +%Y%m%d-%H%M%S)"
EVID="$HOME/bf-incident-${STAMP}"
mkdir -p -- "$EVID"
chmod 700 -- "$EVID"
echo "=== ❶ 隔離:停用 Bricksforge(只改設定,不動檔案)==="
if ! wpu "$OWNER" "$WP_PATH" plugin deactivate bricksforge; then
echo "停用失敗(外掛檔案可能已被竄改):改用搬移目錄的方式阻斷載入"
sudo mv -- "$WP_PATH/wp-content/plugins/bricksforge" "$EVID/bricksforge-plugin-dir"
fi
echo "=== ❷ 取證:日誌、外掛目錄、uploads、mu-plugins、資料庫、可疑檔雜湊 ==="
sudo find /var/log/nginx/domains -maxdepth 1 -type f -name "${DOMAIN}.*" -printf '%f\0' \
| sudo tar -czf "$EVID/logs-${DOMAIN}.tar.gz" -C /var/log/nginx/domains --null -T - \
|| echo "日誌打包失敗或找不到,請確認路徑"
sudo tar -czf "$EVID/uploads-snapshot.tar.gz" -C "$WP_PATH/wp-content" uploads
sudo tar -czf "$EVID/mu-plugins.tar.gz" -C "$WP_PATH/wp-content" mu-plugins 2>/dev/null \
|| echo "沒有 mu-plugins 目錄"
sudo tar -czf "$EVID/plugin-bricksforge.tar.gz" -C "$WP_PATH/wp-content/plugins" bricksforge 2>/dev/null \
|| echo "外掛目錄已被搬走或不存在"
( umask 077; wpu "$OWNER" "$WP_PATH" db export - > "$EVID/db-snapshot.sql" )
sudo find "$UPLOADS" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.pht' -o -iname '*.phar' \) -print0 \
| sudo xargs -0 -r sha256sum > "$EVID/suspect-sha256.txt"
echo "取證資料已放在:$EVID"
ls -lh -- "$EVID"
# ❸ 把 uploads 裡的可疑檔搬到隔離區(保留相對路徑與證據,不直接刪除)
QUAR="$EVID/quarantine"
mkdir -p -- "$QUAR"
while IFS= read -r -d '' FILE <&3; do
REL="${FILE#"$WP_PATH"/}"
sudo mkdir -p -- "$QUAR/$(dirname -- "$REL")"
sudo mv -- "$FILE" "$QUAR/$REL"
sudo chmod 000 -- "$QUAR/$REL"
printf '已隔離:%s\n' "$REL"
done 3< <(sudo find "$UPLOADS" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.pht' -o -iname '*.phar' \) -print0)
# ❹ 暫時封鎖入口(函式定義在下一小節 B,先貼過去再回來執行這行;或先跳過,因為外掛已停用)
# block_bricksforge_endpoints "$OWNER" "$DOMAIN"
# ❺ 帳號與 Session:重設所有管理員密碼、銷毀 Session、刷新鹽值
# 重設後沒有人知道新密碼,管理員請走「忘記密碼」信件流程;若網站寄信功能也不可信,請改由主機端人工設定
while IFS= read -r ADMIN_ID <&3; do
wpu "$OWNER" "$WP_PATH" user reset-password "$ADMIN_ID" --skip-email
wpu "$OWNER" "$WP_PATH" user session destroy "$ADMIN_ID" --all
printf '已重設並登出管理員 ID:%s\n' "$ADMIN_ID"
done 3< <(wpu "$OWNER" "$WP_PATH" user list --role=administrator --field=ID)
sudo cp -- "$WP_PATH/wp-config.php" "$WP_PATH/wp-config.php.BAK.${STAMP}"
wpu "$OWNER" "$WP_PATH" config shuffle-salts
wpu "$OWNER" "$WP_PATH" cache flush
# ❻ 已確認是惡意的陌生管理員:先在 SUSPICIOUS_ID 填入「已確認」的 ID 再執行
# ⚠️ 這是破壞性操作,無法復原;執行前務必再三確認,而且確定 db-snapshot.sql 已經存在
SUSPICIOUS_ID=""
if [[ "$SUSPICIOUS_ID" =~ ^[0-9]+$ ]] && [ -s "$EVID/db-snapshot.sql" ]; then
wpu "$OWNER" "$WP_PATH" user delete "$SUSPICIOUS_ID" --reassign=1 --yes
else
echo "ID 尚未填寫,或資料庫快照不存在,已略過刪除"
fi
# ❇️ 獨立多站:只處理「有證據」的那一站,改一下路徑再套用上面 ❶~❻,不要對整台主機無差別執行 WP_PATH="/home/bob/web/shop.example/public_html" OWNER="$(site_owner "$WP_PATH")" wpu "$OWNER" "$WP_PATH" core is-installed && echo "目標確認:$WP_PATH(擁有者:$OWNER)" # ✴️ Multisite:外掛與 mu-plugins 屬於整個 Network,事件範圍也要以 Network 為單位 WP_PATH="/home/bob/web/network.example/public_html" OWNER="$(site_owner "$WP_PATH")" wpu "$OWNER" "$WP_PATH" plugin deactivate bricksforge --network wpu "$OWNER" "$WP_PATH" config shuffle-salts
預期輸出範例:
=== ❶ 隔離:停用 Bricksforge(只改設定,不動檔案)=== Plugin 'bricksforge' deactivated. Success: Deactivated 1 of 1 plugins. 取證資料已放在:/root/bf-incident-20261009-143022 已隔離:wp-content/uploads/2026/10/login_admin_a1b2c3.php 已重設並登出管理員 ID:1 Success: Shuffled the salt keys.
看到什麼,該怎麼辦:
- 取證與隔離都成功 → 繼續下面的 C「正式修補」;如果是公司網站或有會員資料,同步通知內部資安或主管,並把證據目錄放在受保護的位置。
- 停用外掛時出現 PHP Fatal Error → 代表外掛檔案可能已被改過,上面的流程會自動改用搬移目錄的方式阻斷載入,後續請交給資安專業人員做完整清理。
- 隔離完成後,uploads 又長出新的 PHP 檔 → 代表還有常駐後門或排程在運作,請檢查 crontab -l -u 擁有者 與 wp cron event list,不要急著重新啟用任何外掛。
- 網站牽涉會員個資或線上收款,且確認曾被執行 → 這已經不只是「更新外掛」,請同步啟動事件應變流程並保留稽核記錄 🔴。
🧊 冷知識:鹽值(Salts)跟登入狀態的愛恨情仇
WordPress 的登入 Cookie 是靠 wp-config.php 裡那八組隨機字串簽章的。攻擊者如果趁機偷到管理員 Cookie,你只改密碼是不夠的,因為他手上的 Cookie 依然有效;重新產生鹽值,就像把整棟樓的鎖芯換掉,所有人(包括攻擊者)都會被踢下線。代價是正常使用者也要重新登入,這就是為什麼只建議在「舊版曾暴露」時才刷 🔑
🧯 ㊁ B. 短期應急:還沒確認中招,但現在還不能立刻更新
🚨 短期應急 ≠ 正式修補。
停用外掛、擋路徑都只是縮短暴露時間,真正的修補仍然是更新到官方修補版。而且擋路徑有代價:Pro Forms 正常運作時,表單送出走的就是 /wp-json/bricksforge/v1/form_submit,封鎖之後你網站上的 Bricksforge 表單會全部送不出去 🧱
# 選項一(最乾淨):直接停用外掛 WP_PATH="/home/admin/web/example.com/public_html" OWNER="$(site_owner "$WP_PATH")" wpu "$OWNER" "$WP_PATH" plugin deactivate bricksforge
# 選項二:表單功能不能中斷時,在 Nginx 層擋掉入口(用 HestiaCP 官方的 nginx.conf_ 自訂檔,不會被樣板覆蓋)
block_bricksforge_endpoints() {
local owner="$1" domain="$2"
local conf_dir="/home/$owner/conf/web/$domain"
local http_inc="$conf_dir/nginx.conf_bricksforge_block"
local ssl_inc="$conf_dir/nginx.ssl.conf_bricksforge_block"
if [ ! -d "$conf_dir" ]; then
printf '找不到 %s,略過\n' "$conf_dir"
return 1
fi
sudo tee "$http_inc" > /dev/null <<'EOF'
# 暫時封鎖:CVE-2026-85097(更新完成後請移除)
location ~* ^/(index\.php/)?wp-json/bricksforge/v1/(upload|form_submit) {
return 403;
}
if ($arg_rest_route ~* "^(/|%2F)?bricksforge(/|%2F)v1(/|%2F)(upload|form_submit)") {
return 403;
}
if ($arg_action ~* "^bricksforge_(regenerate_nonce|form_submit)$") {
return 403;
}
EOF
sudo ln -sf "$http_inc" "$ssl_inc"
printf '已封鎖:%s\n' "$domain"
}
unblock_bricksforge_endpoints() {
local owner="$1" domain="$2"
local conf_dir="/home/$owner/conf/web/$domain"
sudo rm -f -- "$conf_dir/nginx.conf_bricksforge_block" "$conf_dir/nginx.ssl.conf_bricksforge_block"
printf '已解除封鎖:%s\n' "$domain"
}
# 💠 單一網站(❇️ 獨立多站請對有風險的網域各執行一次;✴️ Multisite 通常一個網域即可)
block_bricksforge_endpoints "admin" "example.com"
sudo nginx -t && sudo systemctl reload nginx
# 更新完成後再解除:
# unblock_bricksforge_endpoints "admin" "example.com" && sudo nginx -t && sudo systemctl reload nginx
🧑🔧 DevOps 小知識:這條規則擋不到什麼?
Nginx 的 $arg_action 只看網址上的查詢字串,POST body 裡的 action 它看不到,所以 admin-ajax 那一條只能擋到「action 寫在網址上」的請求(Patchstack 觀察到的利用請求幾乎都走 REST 路徑,所以路徑規則是主力)。要在 body 層過濾,需要 WAF,例如 Patchstack 的 RapidMitigate、或自己的 ModSecurity 規則,我沒有逐一驗證它們在 HestiaCP 上的整合方式【❓待確認】 🧩
看到什麼,該怎麼辦:可以停用外掛 → 直接停用,排進維護窗口再更新;表單不能停 → 套用選項二,並在 24 小時內完成正式修補;套用後 nginx -t 報錯 → 不要 reload,把剛建的兩個檔案刪掉再查原因 ♻️
㊂ C. 正式修補:備份 👉 Dry-run 👉 更新 👉 驗證
先看一個前提:Bricksforge 不是 WordPress.org 的外掛,WP-CLI 的 plugin update 只有在這個網站的更新機制拿得到廠商更新資訊時才有用。所以下面的函式支援兩條路:路 A 是 plugin update;路 B 是從官方客戶後台下載的 ZIP,用 plugin install –force 覆蓋。覆蓋安裝時,WordPress 會先移除舊外掛目錄再放新的,舊目錄裡被塞進去的多餘檔案也會一起消失(但這不等於整站乾淨,後門檢查還是要做)📦
# 把「備份 → Dry-run → 更新 → 版本確認」做成一個函式,單站與多站共用,邏輯一致
# 用法:update_bricksforge 網站路徑 [官方ZIP完整路徑]
update_bricksforge() {
local wp_path="$1" zip="${2:-}"
local owner name stamp backup_dir ver
owner="$(site_owner "$wp_path")"
name="$(site_domain "$wp_path")"
stamp="$(date +%Y%m%d-%H%M%S)"
backup_dir="$HOME/wp-security-backup"
mkdir -p -- "$backup_dir"
echo "❶ 備份:$name"
( umask 077; wpu "$owner" "$wp_path" db export - > "$backup_dir/${name}_db_${stamp}.sql" )
if [ ! -s "$backup_dir/${name}_db_${stamp}.sql" ]; then
echo "資料庫備份失敗或是空檔,停止,不進行更新"
return 1
fi
sudo tar -czf "$backup_dir/${name}_wp-content_${stamp}.tar.gz" -C "$wp_path" wp-content \
|| { echo "檔案備份失敗,停止,不進行更新"; return 1; }
ls -lh -- "$backup_dir/${name}_"*"_${stamp}".*
if [ -n "$zip" ]; then
if [ ! -f "$zip" ] || ! unzip -tq "$zip" > /dev/null; then
echo "ZIP 不存在或已損毀:$zip,停止"
return 1
fi
if [ "$(unzip -Z1 "$zip" | head -n 1 | cut -d/ -f1)" != "bricksforge" ]; then
echo "ZIP 最上層資料夾不是 bricksforge,覆蓋安裝會變成第二份外掛,停止,請人工確認"
return 1
fi
else
echo "❷ Dry-run(沒有列出任何更新,不等於已是最新版,以最後的版本判定為準)"
wpu "$owner" "$wp_path" plugin update bricksforge --dry-run
fi
echo "❸ 更新:停用 → 更新 → 啟用"
wpu "$owner" "$wp_path" plugin deactivate bricksforge
if [ -n "$zip" ]; then
wpu "$owner" "$wp_path" plugin install "$zip" --force \
|| { echo "ZIP 安裝失敗,外掛維持停用(這是安全狀態),請查原因"; return 1; }
else
wpu "$owner" "$wp_path" plugin update bricksforge \
|| { echo "更新失敗,外掛維持停用(這是安全狀態),改走路 B(官方 ZIP)"; return 1; }
fi
wpu "$owner" "$wp_path" plugin activate bricksforge
ver="$(wpu "$owner" "$wp_path" plugin get bricksforge --field=version 2>/dev/null || true)"
printf '❹ 更新後版本:%s → %s\n' "${ver:-讀取失敗}" "$(classify_version "$ver")"
}
# 💠 單一網站:路 A(WP-CLI 能抓到更新時)
update_bricksforge "/home/admin/web/example.com/public_html"
# 💠 單一網站:路 B(官方 ZIP 先上傳到該使用者的 tmp 目錄,並確認檔案擁有者能讀)
# update_bricksforge "/home/admin/web/example.com/public_html" "/home/admin/tmp/bricksforge-latest.zip"
# ❇️ 獨立多站:一站走完「備份 → 更新 → 驗證」才進下一站,每站之間強制停下來讓你人工確認
# 請把「確認有風險」的站手動列進陣列,不要對整台主機無差別更新
TARGET_SITES=(
"/home/admin/web/site-a.com/public_html"
"/home/bob/web/shop.example/public_html"
)
for WP_PATH in "${TARGET_SITES[@]}"; do
printf '\n==================== %s ====================\n' "$WP_PATH"
update_bricksforge "$WP_PATH" || { echo "這一站失敗,先停在這裡處理"; break; }
read -r -p "請開瀏覽器確認這一站前後台與表單正常,按 Enter 繼續下一站,Ctrl+C 中斷:"
sleep 3
done
# ✴️ Multisite:外掛檔案全網只有一份,更新一次就好,之後再逐 Site 驗證啟用狀態
# 注意:函式內的停用/啟用是「單站層級」,Network 啟用的外掛請改用 --network(見下方)
WP_PATH="/home/bob/web/network.example/public_html"
OWNER="$(site_owner "$WP_PATH")"
wpu "$OWNER" "$WP_PATH" plugin deactivate bricksforge --network
wpu "$OWNER" "$WP_PATH" plugin update bricksforge
wpu "$OWNER" "$WP_PATH" plugin activate bricksforge --network
wpu "$OWNER" "$WP_PATH" plugin get bricksforge --fields=name,status,version
預期輸出範例:
❶ 備份:example.com -rw------- 1 root root 45M Oct 9 14:30 /root/wp-security-backup/example.com_db_20261009-143022.sql -rw-r--r-- 1 root root 320M Oct 9 14:30 /root/wp-security-backup/example.com_wp-content_20261009-143022.tar.gz ❸ 更新:停用 → 更新 → 啟用 Success: Deactivated 1 of 1 plugins. Success: Updated 1 of 1 plugins. Success: Activated 1 of 1 plugins. ❹ 更新後版本:3.1.8.10 → 🟢 已達 3.x 修補版
看到什麼,該怎麼辦:備份失敗 → 函式會直接停下,先排查磁碟空間或權限;更新失敗 → 外掛會維持「停用」,這是安全狀態,改走路 B;更新完版本仍是 🟠 或 🔴 → 代表更新來源沒生效,請改用官方 ZIP;更新後網站白畫面 → 立刻 plugin deactivate,再用上面的資料庫與 wp-content 備份還原,並等官方說明 🛑
🚨 備份檔裡有資料庫內容,所以在函式裡用 umask 077 讓 SQL 檔只有擁有者能讀。另外請記得:如果這一站已經中招,更新前的備份本身就含有後門,不能當成「乾淨的還原來源」,要還原請用事發前的、已知乾淨的備份 🪤
❹ 驗證:更新成功 ≠ 工作完成,至少看四層
WP_PATH="/home/admin/web/example.com/public_html"
OWNER="$(site_owner "$WP_PATH")"
DOMAIN="$(site_domain "$WP_PATH")"
PLUGIN_DIR="$WP_PATH/wp-content/plugins/bricksforge"
ZIP="/home/admin/tmp/bricksforge-latest.zip" # 與目前安裝「同一版」的官方 ZIP
echo "=== 第一層:版本 ==="
VER="$(wpu "$OWNER" "$WP_PATH" plugin get bricksforge --field=version)"
printf '%s → %s\n' "$VER" "$(classify_version "$VER")"
echo "=== 第二層:外掛檔案與官方 ZIP 逐檔比對(Bricksforge 非 WordPress.org 外掛,不靠 verify-checksums)==="
if [ -f "$ZIP" ]; then
TMPD="$(mktemp -d)"
unzip -q "$ZIP" -d "$TMPD"
if diff -rq "$TMPD/bricksforge" "$PLUGIN_DIR"; then
echo "與官方 ZIP 完全一致"
else
echo "有差異:請逐項確認是不是多出了不該有的檔案"
fi
[ -d "$TMPD" ] && rm -rf -- "$TMPD"
else
echo "沒有官方 ZIP 可比對,略過這層"
fi
echo "=== 第二層(續):WordPress 核心檔案校驗 ==="
wpu "$OWNER" "$WP_PATH" core verify-checksums
echo "=== 第三層:前後台回應(不對 Bricksforge 端點發送任何請求)==="
curl -sS -o /dev/null -w '首頁:%{http_code}\n' "https://${DOMAIN}/"
curl -sS -o /dev/null -w '登入頁:%{http_code}\n' "https://${DOMAIN}/wp-login.php"
wpu "$OWNER" "$WP_PATH" cache flush
echo "=== 第四層:uploads 是否還有 PHP 類檔案 ==="
sudo find "$WP_PATH/wp-content/uploads" -type f \
\( -iname '*.php*' -o -iname '*.phtm*' -o -iname '*.pht' -o -iname '*.phar' \) -print
echo "(上面沒有列出任何檔案就是乾淨)"
預期輸出範例:
3.1.8.10 → 🟢 已達 3.x 修補版 與官方 ZIP 完全一致 Success: WordPress installation verifies against checksums. 首頁:200 登入頁:200 (上面沒有列出任何檔案就是乾淨)
🚨 驗證的侷限:
比對通過,只能證明「外掛目錄的檔案跟官方 ZIP 一致」,證明不了整站沒有後門:後門可能在 mu-plugins、uploads、資料庫、排程裡,這些都不在比對範圍內。另外 wp plugin verify-checksums 是拿 WordPress.org 的校驗值比對,Bricksforge 不在 WordPress.org,還沒有實機驗證它對這個外掛會回什麼,所以不把它當依據【❓待確認】。最後別忘了人工到前台送出一份 Bricksforge Pro Forms 表單,確認功能正常 🔐
🔥 一句話總結:真正安全的批次維運,是先判斷環境、盤點、備份,確認無誤後才逐站更新,最後驗證。環境判斷錯誤,比指令寫錯更致命。這套 判斷環境 👉 盤點 👉 備份 👉 Dry-run 👉 更新 👉 驗證 的閉環,換成下一個外掛、下一個 CVE,一樣可以直接套用 🦸
🧩 舉一反三:「檔案上傳」這一整類漏洞,通用的防禦心法
這次的 CVE-2026-85097 屬於典型的「檔案上傳驗證不一致」(CWE-434)。以下是這類漏洞通用的防禦招式,不是本 CVE 的官方要求,但值得記在腦子裡 🧠
- 前段檢查過了,後段不能再相信用戶給的資料。這次就是 MIME 檢查通過之後,在提交階段又直接採信客戶端送來的 url 欄位。伺服器端每一個階段,都應該以自己手上的資料為準,並重新驗證副檔名與實際內容。
- uploads 目錄永遠不該是執行區。即使沒有任何 CVE,也應該在伺服器層禁止它執行 PHP。論壇那位站長的案例已經證明,這一個設定就能讓攻擊停在第一步。
- 「沒用到這個功能」不等於「入口不存在」。官方論壇的案例是沒有上傳欄位照樣中招。判斷風險要看「漏洞程式碼有沒有被載入」,而不是你在畫面上有沒有看到這個功能。
- 漏洞修補與事件應變是兩件事。只要舊版曾經暴露在網路上,更新完還要查日誌、查檔案、查帳號,缺一不可。
🏆 第七章:事件綜合評估(10 分制,數字越高代表風險或應對品質越正向)
| 評估維度 | 分數 | 一句話評語 |
|---|---|---|
| 漏洞嚴重程度(越高代表風險越大) | 9.8 | 免登入、免互動、遠端執行,而且公開當天就有人在打 |
| 官方應變速度 | 6 | 3.1.8.10 修補版有出,但 4.x 的修補狀況說法不一、論壇已有 4.0.0 受害回報,版本資訊對站長不夠清楚【❓待確認】 |
| 一般站長自救可行性 | 6 | 非 WordPress.org 外掛,後台不一定有一鍵更新,得自己去客戶後台下載 ZIP,對新手有點門檻 |
| DevOps 技術文件完整度 | 8 | Patchstack 把攻擊鏈、入口路徑、IOC 與副檔名花招都寫得很清楚;缺的是官方對 4.x 的明確說明 |
| 🏆 加權綜合建議 | 立即行動 | 最終建議:【今天內更新,並且一定要查舊版暴露期間有沒有被進來過】 |
🔥 這起事件最大的提醒是:漏洞最危險的時候,不是它剛被發現的時候,而是它「剛公開、你還沒更新」的那幾個小時。Patchstack 說第一筆攻擊在公開前後就出現了,這中間幾乎沒有給站長喘息的空檔。與其等哪天網站被掛上奇怪的連結才發現不對勁,不如現在就花五分鐘,打開後台看一眼 Bricksforge 的版本號碼,然後做完第五章那幾個檢查 🔢
📚 參考資料與延伸資源
- 📌 Patchstack|Critical Unauthenticated Arbitrary File Upload Vulnerability Exploited in Bricksforge Plugin [42]
- 📌 NVD|CVE-2026-85097 官方詳情頁 [43]
- 📌 VulDB|Bricksforge Plugin up to 3.1.8.9 temporaryFileUploads unrestricted upload [44]
- 📌 Bricksforge 官方更新日誌 [1]
- 📌 Bricksforge 官方論壇|Security report: possible unauthenticated file upload(v4.0.0) [45]
- 📌 WP-CLI 官方文件|wp config shuffle-salts [46]
- 📌 WP-CLI 官方文件|wp plugin update [47]
- 📌 WP-CLI 官方文件|wp user reset-password [48]
- 📌 HestiaCP 官方文件|CLI Reference [49]
- 📌 HestiaCP 論壇|Custom nginx settings per domain(nginx.conf_ 自訂檔做法) [50]










