資安指南

緊急 PHP 更新後該檢查什麼:網頁 PHP 與命令列 PHP 的差異、set -e 與 diff 結束代碼,以及入侵再檢查

緊急修補 PHP 後,網頁 PHP 與命令列(CLI)PHP 可能不同。說明如何分別檢查、set -e 與 diff 結束代碼,以及修補後的入侵檢查。

發布於 2026-11-01 更新於 2026-11-01 最後核實 2026-11-01 閱讀時間 5 分鐘
本頁內容

適用對象:經營 PHP 網站、正在套用緊急 PHP 更新,或剛完成更新並擔心有遺漏的人。

2026 年 10 月,針對兩個嚴重 PHP 漏洞(CVE-2026-109618 與 CVE-2026-109619)發布了修復版本。本站營運者所管理的一個網站在當晚完成更新。本頁將該次經過、兩個最初的錯誤假設,以及之後對每台伺服器套用的入侵再檢查,整理成通用步驟。漏洞與修復版本的說明,請見 兩個嚴重 PHP 漏洞與 2026 年 10 月的緊急修復版本。

發生了什麼(本站營運者管理的網站)

該網站在多個環境中使用 PHP。由於 PHP 團隊事先公布了修復版本的發布時間,營運者在發布前切換到維護頁面,並在修復版本推出前保持離線。推出後,他們換上修復版本並解除維護頁面。

  1. 公告之後

    切換到維護頁面,等待修復版本
  2. 發布之後

    換上修復版本並解除維護頁面
  3. 更新後檢查

    網頁端已更新,但 cron 使用的命令列 PHP 仍指向舊版本,因此重新指定
  4. 同一晚

    用來解除維護模式的臨時腳本在寫入任何內容前就中止。原因:set -e 與 diff 的組合。未造成損害,腳本已重新建置
  5. 事後

    對每台伺服器套用相同的入侵檢查清單,並以比對檔案的方式檢查更新前到達的一筆可疑請求

美國 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

仍指向舊版本,網頁端的檢查無法顯示

在同一台伺服器上,網頁使用的 PHP 與命令列使用的 PHP 可能是不同的程式、不同的版本。修補後,請兩者都檢查。

分別檢查網頁與 CLI

1

檢查命令列 PHP 的版本

php -v 會顯示此 shell 所呼叫的 PHP 版本。php.net 指出,php -v 也會告訴你 PHP 是 CGI 還是 CLI 版本。which php 會顯示實際被呼叫的執行檔位置。

2

在有多個版本的 Debian 系統上,檢查指令指向何處

update-alternatives --display php 會顯示 php 指令指向哪個版本(update-alternatives 手冊)。--config 可以更改指向,但這屬於伺服器設定變更,請先確認操作步驟與如何復原。

3

在 cron 工作中寫入 PHP 的完整路徑

cron 在與登入 shell 分開的環境中執行工作(crontab(5))。若只寫 php,實際被選用的 PHP 取決於該環境。本站的建議是:寫入你已修補的那個版本的完整路徑,讓更新後的檢查只有一個答案。

4

從網頁端檢查網頁 PHP 版本

在 WordPress 中,後台的 Site Health 會顯示版本。在程式碼中,可以使用常數 PHP_VERSION(版本)與 PHP_SAPI(執行方式:CLI 為 cli,PHP-FPM 為 fpm-fcgi,Apache 模組為 apache2handler)(php_sapi_name)。

5

檢查完立即刪除 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 ;;
esac

set -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 檔案。此次檢查未發現任何檔案遭竄改的跡象。

1

在伺服器外保存一份部署內容的副本

若用來比對的唯一副本位於伺服器上,同時發生的變更就不會被發現。每次部署時,請將已發布的狀態保存在伺服器之外。

2

與線上檔案比較

使用 diff -rq 找出不同的檔案,或比對每個檔案的雜湊值清單(sha256sum)。若有任何差異,請確認是否為你預期的變更。

3

也要查找新增的檔案

即使內容一致,新放置的 PHP 檔案也不會存在於原始副本中,因此必須另外查找。diff -rq 也會回報只存在於一側的檔案。

4

一致並不代表安全

此方法只能說明所比對的檔案未變更。它看不到資料庫、上傳資料夾,或伺服器以外的變動。請與其他檢查合併判斷(本站的建議)。

本站的看法:在緊急之夜前先固定檢查清單

在倉促的夜晚,各伺服器上實際檢查的項目往往會有所不同。若事先決定好項目清單,每台伺服器都會接受相同的檢查,缺口也會清楚呈現。這次之所以發現舊的命令列 PHP,是因為更新後的檢查並未止於網頁端。

今天該做什麼

  1. 若能登入伺服器,執行 php -v 與 which php,並在 cron 工作中寫入 PHP 的完整路徑
  2. 從網頁端檢查網頁 PHP 版本,例如使用 Site Health(用完後立即刪除任何 phpinfo() 頁面)
  3. 找出 set -e 腳本中呼叫 diff、grep 或 cmp 的位置,並決定如何處理結束狀態 1
  4. 在伺服器外保存一份已發布內容的副本,並在更新後進行比對
  5. 讓每台伺服器的入侵檢查清單完全相同,並逐項執行

來源

延伸閱讀

FAQ

Q我更新 PHP 後,它還可能是舊版本嗎?
A

可能。處理網頁請求的 PHP(PHP-FPM 或 Apache 模組)與從 shell 或 cron 呼叫的命令列 PHP(CLI)可能是不同的程式,版本也可能不同。php.net 將 PHP 的執行方式稱為 SAPI,並指出 CLI SAPI 在許多方面與其他 SAPI 不同。網頁端可能已修補,而 cron 使用的 PHP 仍是舊版。

Q我要如何確認實際執行中的 PHP 版本?
A

在命令列執行 php -v。網頁端則從網頁側檢查;若使用 WordPress,可在後台的 Site Health 查看。若使用 phpinfo() 頁面,用完請立即刪除。which php 會顯示被呼叫的執行檔位置;在有多個版本的 Debian 系統上,update-alternatives --display php 會顯示 php 指令指向哪裡。在 cron 工作中寫入 PHP 的完整路徑,可避免不確定實際執行的是哪一個版本。

Q為什麼使用 diff 時,含有 set -e 的 shell 腳本會中止?
A

diff 發現差異時會回傳結束狀態 1。GNU 手冊定義 0 為無差異、1 為有差異、2 為發生問題。set -e(errexit)會在非零狀態時中止腳本,因此只要發現差異就會中止。可將指令放在 if 條件中,或把結束狀態存入變數,並分別處理 0、1 與 2。grep(1 表示沒有符合)與 cmp(1 表示不同)的行為相同。

Q日誌出現可疑紀錄時,我要如何判斷是否遭入侵?
A

僅憑日誌無法判斷。日誌記錄的是抵達的嘗試,不一定代表成功。請比對線上的公開檔案與部署時保存的副本,確認兩者一致,且沒有多出的 PHP 檔案。一致時可以說未發現檔案遭竄改,但這並不證明安全,請與其他檢查合併判斷。