適用對象:經營 PHP 網站、正在套用緊急 PHP 更新,或剛完成更新並擔心有遺漏的人。
2026 年 10 月,針對兩個嚴重 PHP 漏洞(CVE-2026-109618 與 CVE-2026-109619)發布了修復版本。本站營運者所管理的一個網站在當晚完成更新。本頁將該次經過、兩個最初的錯誤假設,以及之後對每台伺服器套用的入侵再檢查,整理成通用步驟。漏洞與修復版本的說明,請見 兩個嚴重 PHP 漏洞與 2026 年 10 月的緊急修復版本。
發生了什麼(本站營運者管理的網站)
該網站在多個環境中使用 PHP。由於 PHP 團隊事先公布了修復版本的發布時間,營運者在發布前切換到維護頁面,並在修復版本推出前保持離線。推出後,他們換上修復版本並解除維護頁面。
公告之後
切換到維護頁面,等待修復版本發布之後
換上修復版本並解除維護頁面更新後檢查
網頁端已更新,但 cron 使用的命令列 PHP 仍指向舊版本,因此重新指定同一晚
用來解除維護模式的臨時腳本在寫入任何內容前就中止。原因:set -e 與 diff 的組合。未造成損害,腳本已重新建置事後
對每台伺服器套用相同的入侵檢查清單,並以比對檔案的方式檢查更新前到達的一筆可疑請求
美國 CISA 後來將這兩個 CVE 加入其「已知遭利用漏洞目錄」(KEV)(CISA 公告)。這支持了趕緊修補的決定,但修補只是關上門,確認是否已有人進入則是另一項工作。
假設一:「網頁 PHP 已更新,所以完成了」
第一個假設是:只要網站跑在新版本上,該伺服器上的所有 PHP 就都已更新。事實上,cron 呼叫的命令列 PHP 仍指向舊版本。
PHP 會依執行方式而成為不同的程式(SAPI,即伺服器 API)。php.net 指出,CLI SAPI 在許多方面與其他 SAPI 不同(PHP:從命令列使用 PHP)。若同一台機器安裝了多個版本,網頁可能執行新版本,而命令列卻執行舊版本。
網頁請求 → PHP-FPM / Apache 模組
執行已更新的版本(可在 Site Health 等畫面看到)
cron 與 shell 腳本 → 命令列 PHP
仍指向舊版本,網頁端的檢查無法顯示
分別檢查網頁與 CLI
檢查命令列 PHP 的版本
php -v 會顯示此 shell 所呼叫的 PHP 版本。php.net 指出,php -v 也會告訴你 PHP 是 CGI 還是 CLI 版本。which php 會顯示實際被呼叫的執行檔位置。
在有多個版本的 Debian 系統上,檢查指令指向何處
update-alternatives --display php 會顯示 php 指令指向哪個版本(update-alternatives 手冊)。--config 可以更改指向,但這屬於伺服器設定變更,請先確認操作步驟與如何復原。
在 cron 工作中寫入 PHP 的完整路徑
cron 在與登入 shell 分開的環境中執行工作(crontab(5))。若只寫 php,實際被選用的 PHP 取決於該環境。本站的建議是:寫入你已修補的那個版本的完整路徑,讓更新後的檢查只有一個答案。
從網頁端檢查網頁 PHP 版本
在 WordPress 中,後台的 Site Health 會顯示版本。在程式碼中,可以使用常數 PHP_VERSION(版本)與 PHP_SAPI(執行方式:CLI 為 cli,PHP-FPM 為 fpm-fcgi,Apache 模組為 apache2handler)(php_sapi_name)。
檢查完立即刪除 phpinfo() 頁面
公開的 phpinfo() 檔案會顯示版本,但也會暴露設定與伺服器內部細節。用完請立即刪除。不留下該檔案,是使用此方法的前提。
假設二:「diff 不是成功就是失敗」
用來解除維護模式的臨時腳本,會在寫入任何內容前,先以 diff 比較目前內容與新內容的差異。該腳本已開啟 set -e。
diff 發現差異時會回傳結束狀態 1。在 GNU 手冊中,0 表示無差異,1 表示發現差異,2 表示發生問題(GNU diffutils:diff)。發現差異正是腳本預期的情況。但 set -e(errexit)會在非零狀態時中止 shell,條件判斷等少數位置除外。該腳本因為有差異而中止,從未執行到寫入步驟。
因此,解除維護模式的步驟在未寫入任何內容的情況下中止,沒有造成損害。它是在安全的一側失敗,但反過來的組合(該停止時卻沒有停止)就會有危險。腳本已重新建置。
結束狀態 1 不是「錯誤」的指令
| 指令 | 1 的意義 | 2 的意義 | 來源 |
|---|---|---|---|
diff | 發現差異 | 發生問題 | GNU diffutils:diff |
cmp | 發現差異 | 發生問題 | GNU diffutils:cmp |
grep | 沒有選取任何行(沒有符合) | 發生錯誤 | GNU grep |
安全的寫法
GNU Bash 手冊說明,在 set -e 下,若指令作為 if 測試的一部分、位於 && 或 || 清單中(最後一個指令除外),或其狀態被 ! 反轉,shell 就不會因該指令返回非零而結束(GNU Bash:The Set Builtin)。請善加利用。
會中止,或隱藏真正的失敗
- 在
set -e下直接寫diff a b(只因有差異就中止) diff a b || true(同時吞掉「有差異」與真正的錯誤 2)- 在
grep上使用|| true,使「沒有符合」與「無法讀取檔案」看起來相同
分別處理結束代碼
- 將結束狀態存入變數,並依 0、1、2 分支處理(建議的寫法)
- 若只需知道是否有差異,可使用
if diff -q a b; then …,但要注意 2(錯誤)會與「有差異」落在同一側 - 若使用
|| true,請先確認是否能接受遺漏 2(真正的錯誤)
以下範例分別處理 0、1 與 2。|| 左側的失敗會豁免 errexit,因此腳本不會中止,且 rc 會保存結束狀態。
#!/usr/bin/env bash
set -euo pipefail
rc=0
diff -rq deployed/ live/ || rc=$?
case "$rc" in
0) echo "no differences" ;;
1) echo "differences: review before continuing" ;;
*) echo "diff failed" >&2; exit 2 ;;
esacset -e 並非完整的安全網。它有許多例外,可能掩蓋錯誤,因此在重要的分支中,自行讀取結束狀態並做出判斷會更容易(本站的建議)。
修補後:對每台伺服器執行相同的檢查
修補關閉的是入口,但無法告訴你在更新前是否已有人進入。對於 KEV 漏洞,CISA 要求在更新前檢查是否遭入侵(請見PHP 文章中的「確認遭利用與加入 CISA KEV」)。為避免各伺服器的檢查內容不一致,營運者對每台伺服器套用了相同的檢查清單。
| 項目 | 應查看的內容 | 範例指令 |
|---|---|---|
| 登入金鑰 | 你不記得登錄過的公開金鑰 | 開啟 authorized_keys |
| 使用者 | 不明使用者、管理群組的變動 | getent passwd、getent group sudo |
| 遭修改的套件檔案 | 套件安裝的檔案,自發布後有所變更 | debsums -s(Debian 系)、rpm -Va(RHEL 系) |
| 不屬於任何套件的執行檔 | 位於 /usr/local、/opt 或 PATH 上、且沒有任何套件擁有的新執行檔 | dpkg -S <檔案>(Debian 系)或 rpm -qf <檔案>(RHEL 系);尋找沒有擁有套件的檔案 |
| SUID | 以擁有者權限執行的新檔案 | find / -xdev -perm -4000 -type f |
| cron 與 systemd | 不明的排程工作與計時器 | crontab -l、/etc/crontab、/etc/cron.d、systemctl list-timers |
| /tmp | 放在其中的執行檔 | find /tmp /var/tmp -type f -perm /111 |
| 對外連線 | 連往你不認識之目的地的連線 | ss -tunp |
| 容器內部 | 執行中的程序,以及啟動後寫入的檔案 | docker top、docker diff |
crontab -l 只顯示目前使用者的工作,因此請另外檢查 /etc/crontab、/etc/cron.d 以及各使用者的 cron 暫存目錄。ss -tunp 需要 root 權限才能顯示每個連線背後的程序。dpkg -S 與 rpm -qf 會回報哪個套件擁有某個檔案;「沒有擁有套件」仍可能是你自己放置的檔案,請對照你記得安裝過的內容來確認。
find -perm -4000 會比對設定了所有指定位元的檔案(find(1))。docker diff 會列出容器建立後有變動的檔案(A:新增,C:變更,D:刪除),docker top 則顯示執行中的程序(docker container diff、docker container top)。容器通常是重新建置而非修改,因此請將 docker diff 顯示的執行檔視為非預期並加以調查(本站的建議)。
如何解讀各項目,以及共享主機、WordPress 與 VPS 的步驟,請見如何檢查你的網站或伺服器是否遭入侵。
以比對檔案釐清可疑的日誌紀錄
在這次檢查中,團隊發現更新前有一筆 POST 從 Tor 出口節點(隱藏發送者的網路出口點)送到聯絡表單。日誌只顯示請求已抵達,並未顯示是否因此寫入任何內容。
因此,他們將線上公開檔案與部署時保存的副本進行比對。兩者完全一致,也沒有多出的 PHP 檔案。此次檢查未發現任何檔案遭竄改的跡象。
在伺服器外保存一份部署內容的副本
若用來比對的唯一副本位於伺服器上,同時發生的變更就不會被發現。每次部署時,請將已發布的狀態保存在伺服器之外。
與線上檔案比較
使用 diff -rq 找出不同的檔案,或比對每個檔案的雜湊值清單(sha256sum)。若有任何差異,請確認是否為你預期的變更。
也要查找新增的檔案
即使內容一致,新放置的 PHP 檔案也不會存在於原始副本中,因此必須另外查找。diff -rq 也會回報只存在於一側的檔案。
一致並不代表安全
此方法只能說明所比對的檔案未變更。它看不到資料庫、上傳資料夾,或伺服器以外的變動。請與其他檢查合併判斷(本站的建議)。
本站的看法:在緊急之夜前先固定檢查清單
在倉促的夜晚,各伺服器上實際檢查的項目往往會有所不同。若事先決定好項目清單,每台伺服器都會接受相同的檢查,缺口也會清楚呈現。這次之所以發現舊的命令列 PHP,是因為更新後的檢查並未止於網頁端。
今天該做什麼
- 若能登入伺服器,執行
php -v與which php,並在 cron 工作中寫入 PHP 的完整路徑 - 從網頁端檢查網頁 PHP 版本,例如使用 Site Health(用完後立即刪除任何
phpinfo()頁面) - 找出
set -e腳本中呼叫diff、grep或cmp的位置,並決定如何處理結束狀態 1 - 在伺服器外保存一份已發布內容的副本,並在更新後進行比對
- 讓每台伺服器的入侵檢查清單完全相同,並逐項執行
來源
- PHP:從命令列使用 PHP
- PHP:php_sapi_name
- GNU Bash Reference Manual:The Set Builtin
- GNU diffutils:diff、cmp、GNU grep
- update-alternatives(1)、crontab(5)、find(1)
- docker container diff、docker container top
- CISA:新增兩個漏洞至已知遭利用漏洞目錄
延伸閱讀
- 漏洞與修復版本:兩個嚴重 PHP 漏洞與 2026 年 10 月的緊急修復版本
- 入侵檢查:如何檢查你的網站或伺服器是否遭入侵
- 事前準備:備份與還原基礎 / 安全盤點清單
- 名詞解釋:什麼是 RCE(遠端程式碼執行)? / 什麼是 IOC(入侵指標)?