17 min read

PHP2Shell 來了,先看清楚你跑的是哪個 PHP

PHP2Shell 來了,先看清楚你跑的是哪個 PHP

10 月 21 日,PHP 官方釋出緊急安全更新,修補兩個被稱為 PHP2Shell 的重大漏洞。隔天一早我打開終端機,做的第一件事是開一個對話視窗,跟 Claude Opus 說:「昨天有很嚴重的 PHP 漏洞,一起檢查一下我的主機。」

所以這篇的解法第一步就是:找 AI 一起討論 XD。

開玩笑的,但也不全是玩笑,這次討論真正有價值的,不是哪一行指令,而是過程中一直被問到的幾個問題:「你確定主機上只有這一個 PHP 嗎?」「這個版本號可信嗎?」「更新完,實際在跑的是新版嗎?」這篇想把這幾個問題整理出來,讓你不管用什麼平台,都能自己檢查一遍。

先講結論

如果你只有五分鐘:

  • 把 PHP 升到修補版:8.5.12、8.4.27、8.3.36、8.2.35,8.1 和更舊的版本,PHP 官方不會再發布正式修補版本。
  • 更新完一定要重啟 PHP-FPM 或 Apache,套件換了,舊的程序還在記憶體裡跑舊程式碼。
  • 確認「實際在跑的」是新版,而不是「看起來」是新版。
  • 修不了的那個,至少不要讓它直接暴露在網路上。

剩下的篇幅,是在講怎麼做到這四件事,以及我在兩台主機上踩到的坑。

PHP2Shell 是什麼

PHP2Shell 是 CVE-2026-109618 和 CVE-2026-109619 兩個漏洞的合稱,CVSS 都是 9.2,攻擊者不用登入就能在伺服器上遠端執行程式碼,只要是對外提供網頁的 PHP 都在範圍內,包含 PHP-FPM、Apache 的 mod_php、php-cgi、FrankenPHP 和 LiteSpeed。

有兩件事值得特別記住:

問題在 PHP 執行環境本身,不在你的程式碼,所以只更新 WordPress 或外掛修不掉它;反過來說,如果只是在同一個版本分支內更新(例如 8.4.26 升到 8.4.27),通常不需要修改網站程式碼,可以想成汽車召回:要換的是引擎裡的零件,車子本身不用動,但如果你原本的版本已經沒有修補,要跨大版本升級(例如從 7.4 升到 8.4),就可能遇到相容性問題,要另外測試。

細節在修補釋出前一天就外洩了,也就是說,修補出來的時候,已經有人拿著細節在掃,這種時候要假設「我的主機可能已經被看過」,修完之後也要回頭檢查。

為什麼不能只給你一行指令

官方的建議很簡單:升級到修補版,但「怎麼升級」每台主機都不一樣,因為 PHP 可能從好幾個地方來:

  • 作業系統的套件(Ubuntu、Debian 官方)
  • 控制面板自己的套件庫(CloudPanel、cPanel、Plesk 等)
  • Docker image
  • 自己編譯

我的兩台主機就剛好是兩種完全不同的狀況,所以與其給你指令,不如給你五個要回答的問題,指令只是範例,換到你的平台再對應調整。

問題一:我的主機上到底有哪些 PHP 在跑?

這是最重要的一步。

# 系統套件裝了哪些 PHP
dpkg -l | grep -E '^ii\s+(php|libapache2-mod-php)'

# 有哪些 PHP-FPM 或 Apache 服務在跑
systemctl list-units --type=service --no-pager | grep -iE 'php|apache'

# 每一個 PHP-FPM 主程序,連控制面板自己藏的那個也會現形
ps -eo pid,user,cmd | grep 'php-fpm: master' | grep -v grep

# Docker 裡的服務
docker ps --format '{{.Names}}\t{{.Image}}'

第三行特別推薦,服務名稱可能取得千奇百怪,但一般 PHP-FPM 的主程序,通常會顯示為 php-fpm: master process,後面括號還會告訴你它用哪個設定檔,我就是靠這一行,才發現控制面板的後台自己跑著一個舊版 PHP。

如果你用 Docker,還要多想一步:docker ps 只會列出「正在跑」的 container,如果你有隨需求才開的環境(例如 CTF 題目、預覽環境),它們現在沒在跑,就不會出現在清單裡,所以先把停止的 container 和所有 image 都列出來:

# 包含已停止的 container
docker ps -a --format '{{.Names}}\t{{.Image}}\t{{.Status}}'

# 主機上所有的 image
docker image ls

從名稱看起來可能有 PHP 的(例如 php、wordpress、apache 開頭的,或自己 build 的題目 image),再個別檢查:

docker run --rm --entrypoint sh [image 名稱] -c 'command -v php php-fpm'

要注意這個指令會用那個 image 實際啟動一個暫時的 container,跑完就刪掉,而且如果 image 裡沒有 sh,或 PHP 不在預設的 PATH 裡,它可能什麼都沒印出來,但不代表裡面沒有 PHP,所以這只是輔助,不是百分之百可靠的判斷。

最後,如果同一台主機有好幾個 PHP 版本,要確認每個網站實際用的是哪一個,每個網站可能使用獨立的 FPM pool,也可能好幾個網站共用預設的 pool,所以不能只憑 pool 設定檔的數量判斷某個版本有沒有在用,要從網站伺服器的設定,確認請求實際轉發到哪個 PHP-FPM socket:

# nginx 把 PHP 請求轉發到哪裡
sudo nginx -T 2>/dev/null | grep -E 'fastcgi_pass|proxy_pass'

# 每個 PHP 版本的 pool 在哪裡接收請求
grep -rn '^\s*listen\s*=' /etc/php/*/fpm/pool.d/

把兩邊對起來,就知道每個網站實際交給哪個版本處理,用 Apache 的話,要看的是網站設定裡 SetHandler 或 proxy:unix: 指向的 socket。

問題二:版本號可信嗎?

直覺上會用 php -v 看版本,但這裡有兩個陷阱。

dpkg -l 'php8.3*' | grep ^ii
zgrep -lE 'CVE-2026-10961[89]' /usr/share/doc/php8.3-*/changelog.Debian.gz

陷阱一:發行版的版本號不會跟著上游走,Ubuntu 修漏洞的方式是把修補「移植」進原本的版本,所以 Ubuntu 24.04 修好之後,php -v 還是顯示 8.3.6,要看的是套件版本(Ubuntu 24.04 的修補版是 8.3.6-0ubuntu0.24.04.13),或是直接在套件的變更紀錄裡找 CVE 編號:

反過來,控制面板自己打包的 PHP 通常會跟著上游版本號(例如 8.4.27-1+...),這時 php -v 就是準的。所以要先知道你的 PHP 是誰打包的,才知道版本號能不能信。

陷阱二:php -v 看的是命令列版本,不是網站在用的那個,網站是由 PHP-FPM 在跑,要另外看 FPM 的執行檔:

php-fpm8.4 -v | head -1

不過這看的還只是磁碟上那個檔案的版本,正在跑的程序是不是已經換成它,問題四會再確認。

如果是控制面板自己的 PHP,連執行檔是哪一個都不一定看得出來,可以用問題一拿到的 PID 反查:

sudo readlink /proc/[PID]/exe

問題三:修補從哪裡來?

在 Debian 系的主機上,apt policy 可以告訴你這個套件會從哪個來源安裝:

apt policy php8.4-fpm

輸出裡每個版本後面都有一個優先權數字,也會列出來源網址,這次我遇到的狀況是:控制面板把自己的套件庫優先權設成 1000,比 Ubuntu 官方高,所以 Ubuntu 的修補版根本裝不進來,只能等控制面板推出新版。

這件事平常沒有感覺,但在緊急修補的時候就很關鍵:你的修補速度,取決於幫你打包 PHP 的那一方。

另外,PHP 8.1 和更舊的版本已經結束上游官方支援,不會再發布正式的新版本,不過 PHP 官方這次特別提供了 7.4、8.0、8.1 的原始碼修補,部分發行版也有回移植安全更新,例如 Ubuntu 22.04 的 php8.1 套件就在 8.1.2-1ubuntu2.28 修補了這次的漏洞,還在用這些版本的話,要看你的發行版或面板廠商有沒有提供,不然就是要換到 8.2 以上,反過來說,就算 php -v 顯示的是 8.1 的版本號,也不能直接斷定有沒有被修過,一樣要看套件版本和維護商的公告。

問題四:真的換成新版了嗎?

套件更新完不等於修好了,PHP-FPM 是常駐程式,套件換了,記憶體裡的舊程序還在跑舊的程式碼,一定要重啟:

sudo apt update
sudo apt install --only-upgrade 'php8.4*'
sudo systemctl restart php8.4-fpm

然後確認四件事:

# 1. 磁碟上的執行檔是修補版
php-fpm8.4 -v | head -1

# 2. 服務是在更新之後才重新啟動的(看啟動時間)
systemctl status php8.4-fpm --no-pager | grep Active

# 3. 正在跑的主程序用的是哪個執行檔
sudo readlink /proc/$(systemctl show -p MainPID --value php8.4-fpm)/exe

# 4. 網站有回應
curl -sI https://[你的網域] | head -3

第 3 項如果顯示 /usr/sbin/php-fpm8.4 (deleted),代表程序還在用已經被套件換掉的舊執行檔,也就是還沒真正重啟到新版,要再重啟一次,第 1 項說是新版、第 2 項說是剛重啟、第 3 項沒有 (deleted),三個對得上,才能說正在跑的是修補版,第 4 項只確認 HTTP 有回應,網站功能正不正常,還是要用瀏覽器實際測。

這裡有個容易漏掉的地方:如果控制面板的後台用的是自己的 PHP 服務,它的服務名稱通常不叫 php*-fpm,用萬用字元重啟不會碰到它,要個別處理。

最後別忘了用瀏覽器實際點一下網站,特別是表單、上傳、快取這些依賴擴充套件的功能,這次就有社群回報,某個面板的修補版第一次打包時漏了幾個擴充套件,更新完網站功能就壞了,後來才重新打包補上。

問題五:修不了的,怎麼縮小它的暴露面?

總會有暫時修不了的東西,這次我遇到的就是控制面板後台的 PHP 8.1,這時候的思路是:既然修不了,就縮小它能被碰到的範圍,但要記得,這是降低風險的暫時措施,不是修好了。

停掉沒在用的版本,我那台主機十個 PHP 版本全部都在跑,實際上只有一個有網站在用,沒在用的就停用並 mask,避免開機時又被帶起來,停用前一定要先照問題一的方法,從 nginx 或 Apache 的設定確認沒有網站把請求轉發到這些版本,不要只看 pool 設定檔,判斷錯了會直接把正在用的網站停掉(版本清單換成你確認過沒在用的):

for v in 7.1 7.2 7.3 7.4 8.0 8.1 8.2 8.3 8.5; do
  sudo systemctl disable --now php$v-fpm
  sudo systemctl mask php$v-fpm
done

注意是停用而不是移除,有些控制面板的套件會把每個 PHP 版本都列為必要依賴,用 apt 移除舊版本會連面板一起拔掉,之後要重新啟用,用 systemctl unmask 就能解開,停完之後,也記得馬上確認網站還正常。

關掉不需要對外的埠,管理後台通常不需要對全世界開放。先看防火牆規則:

sudo ufw status numbered

刪除規則時要照原本的寫法刪。我原本以為規則是 8443/tcp,刪了才發現對不到,仔細一看規則寫的是 8433:8443/tcp,當初設定時打錯一個數字,結果一口氣開了 11 個埠。

刪完之後,要確認 IPv4 和 IPv6 的規則都不見了,再從外部實際連一次,確認真的連不上:

# 在主機上:IPv4 和 IPv6 的規則都要消失
sudo ufw status | grep 8443

# 在自己的電腦上:應該要逾時或被拒絕
nc -zv -w3 [主機 IP] 8443

設定檔寫的是你以為的樣子,從外面實際連一次,才是真正的樣子。

改用 SSH 通道進後台,埠關掉之後,後台還是用得到,只是要先開 SSH 通道:

ssh -L 8443:127.0.0.1:8443 [管理帳號]@[主機 IP]
# 然後瀏覽器開 https://localhost:8443

原理是:SSH 的 22 埠一直開著,通道從這裡進到主機裡面,再從主機本機連後台,而防火牆預設不會擋主機自己連自己,這樣後台就不再直接暴露於公網,必須先通過 SSH 身分驗證才能從外部建立通道,能大幅降低遠端攻擊風險,如果你的 SSH 還允許密碼登入,建議順便改成只允許金鑰。

但 PHP 漏洞本身仍然存在,它還是在主機上跑著,主機上其他服務如果有 SSRF 之類的漏洞,或攻擊者已經從別處取得本機存取權限,還是可能從內部連到它,所以這只是暫時的隔離措施,不能取代安全更新。

如果家裡是浮動 IP,這個做法會比「防火牆只允許自己的 IP」更不容易把自己鎖在外面,萬一 SSH 也出問題,大部分 VPS 業者的管理介面都有網頁版主控台,可以從那裡進去救援。

回頭檢查空窗期

因為細節在修補前就外洩了,修完之後我也回頭看了 10 月 19 日之後的狀況:

# 網站目錄裡有沒有新增或被改過的 PHP 檔(webshell 最常見的落地方式)
sudo find /var/www /home/*/htdocs -name '*.php' -newermt '2026-10-19' -ls 2>/dev/null

# PHP 有沒有異常崩潰(記憶體破壞類漏洞被嘗試時可能留下的痕跡之一)
sudo journalctl -k --since '2026-10-19' --no-pager | grep -i segfault

# 最近的登入紀錄
last -i | head -10

find 有列出檔案也先別緊張,網站程式自動更新或快取也會產生新檔,逐一確認是不是正常的就好。崩潰紀錄也不代表一定被打穿,但如果很多,就值得配合存取紀錄往下追,反過來,沒有崩潰紀錄也不代表沒事,成功的利用不一定會讓 PHP 崩潰,也不一定會寫入 webshell。

我兩台都是跑完沒有可疑輸出,登入紀錄也都是我自己。

不過,這些只是初步排查,沒有發現異常不代表能完全排除入侵,last 看的是登入紀錄,透過 HTTP 漏洞進來的攻擊不會出現在這裡,若需要更深入確認,還要檢查 nginx 或 Apache 的存取紀錄、PHP 錯誤紀錄、異常程序與對外連線。

我的兩台主機

主機 A:以為有,其實沒有

第一台是 Ubuntu 24.04,跑部落格和幾個 Docker 服務,照問題一盤點下來:主機上沒有裝任何 PHP 套件,結論是 PHP2Shell 跟這台無關。

但盤點的過程還是有收穫:它提醒我有一個會隨需開 container 的服務,那些環境平常不會出現在 docker ps 裡,要另外檢查 image,也順手把系統其他的安全更新補完了。

如果我一開始就假設「我有 PHP,趕快升級」,會白忙一場;假設「我沒有 PHP,不用管」,又可能漏掉隨需開的那些,先盤點,才知道要做什麼。

主機 B:以為一個,其實十一個

第二台用 CloudPanel 管理,跑一個 PHP 網站,我原本的認知是「這台有一個 PHP」,盤點完的實際狀況是:

  • 十個 PHP-FPM 版本(7.1 到 8.5)全部都在跑
  • 只有 8.4 設定了網站的 pool,停用其他版本之後網站也運作正常
  • 還有第十一個:CloudPanel 後台自己的 PHP,跑在 8.1.34,PHP 官方不會再發布修補版本,CloudPanel 的套件庫也還沒有更新,而且直接對外開在 8443

處理順序是:

  1. 把 8.4 升到 8.4.27,重啟,確認網站正常
  2. 停用並 mask 其他九個沒在用的版本
  3. 發現後台卡在 8.1,修補版短期內不會出來
  4. 把後台的埠從防火牆關掉(順便抓到那條打錯的規則),改走 SSH 通道
  5. 回頭檢查空窗期,沒有發現異常

到這裡,後台已經不再直接暴露在公網上,風險大幅降低,但舊版 PHP 的漏洞本身還在,所以我後來還是想辦法把後台也換到 8.4.27,這段牽涉到 systemd 和 FastCGI 的運作方式。

跟 AI 一起處理的心得

回到開頭的玩笑,這次整個過程不到一個小時,我的做法是把終端機的輸出原封不動貼給 Claude,讓它判讀、建議下一步,它也會即時去查最新狀況,例如控制面板 GitHub issue 裡大家回報的修補進度,這部分比我自己一篇篇翻快很多。

但它也不是每次都對,它一開始用 sudo -v 判斷某個帳號有完整的 sudo 權限,後來看到 sudo -l 才修正;也曾經猜主機上的資料庫是舊網站留下來的,後來才想到部落格本身就需要它,所以我的原則是:每一步的輸出自己都要看懂,看不懂就問「為什麼」,搞懂了才往下做,尤其是在正式環境,指令跑下去之前,要知道它會改到什麼、出事要怎麼還原。

下篇的內容,就是我在過程中一直追問「為什麼可以這樣做」問出來的。

檢查清單

  • [ ] 盤點主機上所有的 PHP:套件、服務、FPM master process、Docker(包含已停止的 container 和 image)
  • [ ] 從 nginx 或 Apache 的設定,確認每個網站實際轉發到哪個 PHP-FPM
  • [ ] 確認你的 PHP 是誰打包的,版本號能不能信
  • [ ] 確認修補從哪個來源來,來了沒有
  • [ ] 更新後重啟,從正在跑的主程序確認是修補版(沒有 (deleted)),網站功能正常
  • [ ] 確認沒有網站轉發過去之後,才把沒在用的版本停用並 mask
  • [ ] 不需要對外的埠關掉(IPv4、IPv6 都要確認,並從外部實際測試),管理後台改走 SSH 通道
  • [ ] 回頭檢查 10 月 19 日之後的新增檔案、崩潰紀錄、登入紀錄

參考資料