PHP 網站攻擊情境清單

PHP 資安 · 2026-10 php2shell 修補實錄

分享

這份清單整理自一次實際的 PHP 升級與修補。PHP 官方在 2026-10-21 公布 php2shell 漏洞(CVE-2026-109618/109619,CVSS 9.2),我們順著修補一路檢查了幾個正式站,把真的遇到的狀況用白話寫下來。如果你的系統也有類似的用法,請特別注意。

怎麼用這份清單

每個情境都分成五段:白話說明、攻擊者會怎麼做、怎麼自己檢查、怎麼修,以及替代做法:一時沒辦法照標準修法做的時候,副作用最小的做法。檢查指令大多是唯讀的,可以直接在自己的主機或電腦上跑。網址、主機名稱都已經拿掉,改用 你的網站 代替。

總覽

#情境嚴重度這次有沒有遇到
01上傳進度被寫進 session高所有 PHP 主機預設都開著
02PHP 版本已停止支援高5.6、7.0、8.1 都還在正式跑
03頁面要登入,背後的 API 不用高會員資料可直接下載、修改
04除錯、測試頁留在正式站高3 支除錯頁對外可開
05上傳目錄可以寫入也可以執行高部署腳本把目錄設成 0777
06正式站開著開發模式中環境變數設成 development
07錯誤頁洩漏伺服器路徑中某支 API 每次都 Fatal error
08回應標頭直接寫出 PHP 版本中每個站都有
09帳號密碼寫死在程式裡中admin/admin123
10測試站對外公開、沒有密碼中測試分支站任何人可開
11記憶體與上傳大小沒有上限中官方緩解建議之一
12以為改了設定,其實沒生效中修補時最容易踩的坑
13升級本身把功能弄壞中測試抓到 1 個、比對抓到 4 個差異

一、PHP 本身的設定與版本

這一類跟程式寫得好不好無關,裝好 PHP 就有。

01

上傳進度被寫進 session(php2shell)

高

上傳大檔案時,PHP 可以一邊收檔、一邊把「已經收到多少 bytes」寫進 session,讓網頁另外發請求來查進度、畫進度條。php2shell 攻擊的就是這個「寫進 session」的步驟。

攻擊者

送出一個特製的上傳請求,讓 PHP 在寫 session 時寫進攻擊者控制的內容,最後可以在主機上執行指令。網站根本不需要有上傳功能,只要 PHP 有開這個設定就會中。Apache mod_php 的環境已經有公開的攻擊示範。

檢查
php -i | grep session.upload_progress.enabled
# 看到 On 或 1 就是有開(PHP 預設是開的)

php-fpm、Apache、命令列各自讀不同的 php.ini,每一份都要看。

修法
  • 最好:升級到 8.5.12、8.4.27、8.3.36 或 8.2.35 以上。
  • 暫時不能升級:在 php.ini 或 FPM pool 設定 session.upload_progress.enabled = 0,然後重啟所有 PHP worker。
  • 需要進度條的話,改用瀏覽器端計算(xhr.upload.onprogress),不需要伺服器幫忙。
替代做法

需要上傳進度條時,改由瀏覽器自己計算,伺服器完全不用設定,也不碰 session:

const xhr = new XMLHttpRequest();
xhr.upload.onprogress = e => bar.value = e.loaded / e.total * 100;
xhr.open('POST', '/upload'); xhr.send(formData);

注意:fetch() 目前拿不到上傳進度,要用 XMLHttpRequest;用 jQuery 或 axios 的話,它們也有對應的 progress 參數。副作用:只動前端,後端上傳程式不用改。

這次遇到:所有主機都是預設開啟。查過程式碼,沒有任何功能用到它,所以關掉後使用者不會察覺任何差別。檔案上傳本身(file_uploads)是另一個設定,不受影響。

02

PHP 版本已停止支援

高

PHP 每個版本只維護幾年。過了期限,之後再發現漏洞,官方也不會出修補版。

攻擊者

等新漏洞公布,直接拿去打還在用舊版的網站。舊版永遠補不起來,等於每個新漏洞都有效。

檢查
php -v
curl -sI https://你的網站/ | grep -i x-powered-by

對照 php.net 的「Supported Versions」頁。這次 8.1 以下只能自己移植修補。

修法
  • 排程升級。先比對兩個版本的語言差異、掃描需要改的程式碼位置,再升級。
  • 短期升不上去:套用 01 的設定緩解,並把主機放到防火牆或 WAF 後面,只開必要的入口。
替代做法

不要直接在原主機上升級。先用容器把新版 PHP 開在旁邊,接資料庫副本,新舊兩版打同一批網址比對;確認一樣了再把流量切過去,舊版保留著,隨時可以切回來。跨好幾個大版本時,一次只升一個大版本(例如 8.1 → 8.2 → 8.4),每一步都比對。副作用:多花一點測試時間,正式站停機時間接近零。

這次遇到:5.6、7.0、8.1 都還有正式站在跑。容器部署的站已升到 8.4.27;舊主機只能先用設定緩解。

11

記憶體與上傳大小沒有上限

中

memory_limit = -1 代表單一請求可以用掉主機全部記憶體;post_max_size 設很大,代表一個請求可以塞很多資料進來。

攻擊者

送出超大或特製的請求,一個就能把主機記憶體吃光,整台機器上的服務一起掛掉。這次官方的緩解建議裡,也有提到要把這兩個值收緊。

檢查
php -i | grep -E 'memory_limit|post_max_size|upload_max_filesize'
修法
  • memory_limit 設成實際需要的值,例如 256M。
  • post_max_size 依實際上傳需求設定。先記下原值;如果原本就設得比較大,是因為有功能真的需要,就不要隨便調小。
  • 前面有 nginx 的話,也要用 client_max_body_size 限制大小。
替代做法

不要全站一起調小。整體設一個合理的上限,只有真的需要大檔案的那一兩個上傳網址,在 nginx 針對該路徑放大;或開一個獨立的 FPM pool 給它,用不同的設定:

client_max_body_size 8m;                     # 全站
location /api/upload/video { client_max_body_size 200m; }   # 只放大這一條

副作用:要先盤點哪些網址真的需要大檔案;調小之後,觀察 log 一週,看有沒有 413 或記憶體不足的錯誤。

08

回應標頭直接寫出 PHP 版本

中

網站回應裡帶著 X-Powered-By: PHP/8.1.34,等於公開告訴所有人「我用的是哪一版」。

攻擊者

用掃描器整批掃網路,挑出版本號落在漏洞範圍內的網站優先攻擊。漏洞公布後幾小時內就會開始掃。

檢查
curl -sI https://你的網站/ | grep -i -E 'x-powered-by|server'
修法

php.ini 設 expose_php = Off;nginx 設 server_tokens off。這不是真正的修補,只是不讓自己被優先挑中。

替代做法

不想動 PHP 設定的話,在前面的 nginx 拿掉標頭就好:

fastcgi_hide_header X-Powered-By;   # php-fpm
proxy_hide_header X-Powered-By;     # 反向代理

副作用:沒有。只是讓回應少一行標頭。

這次遇到:每個站都有。升級後已經隱藏。

12

以為改了設定,其實沒生效

中

設定改完,網站還是用舊的設定在跑,自己卻以為已經修好了。

常見原因
  • 用 ini_set() 或 .user.ini 關 upload_progress:攻擊在程式執行之前就發生了,這兩種方式來不及擋,官方明確說不能用。
  • 改了 php.ini 但沒重啟 php-fpm/Apache:舊的 worker 還在用舊設定。
  • 改到命令列那份 php.ini,網站讀的是 fpm 那份。
  • 設定檔改錯字,重啟失敗,服務直接停掉。
檢查

改完一定要從「網站實際在跑的那個 PHP」讀回來:

php-fpm -t                 # 先檢查設定檔語法
# 在容器內查:
docker exec <容器> php -r 'var_dump(ini_get("session.upload_progress.enabled"));'
修法

修改前先備份,改完先跑 php-fpm -t/apache2ctl -t,通過了才 reload;失敗就自動還原,而且不重啟。

替代做法

把整個流程寫成一支腳本,每次都照同樣的順序做:備份 → 修改 → 檢查語法(php-fpm -t、apache2ctl -t)→ 平滑重啟(reload、graceful,不會中斷正在處理的連線)→ 從實際在跑的 PHP 讀回設定 → 失敗就自動還原備份。副作用:沒有停機;最壞的情況是還原回原狀。

二、程式與部署留下的破口

這一類跟 PHP 版本無關,升級不會自動修好,要靠人去找。這次修補時順便測出來的問題,比漏洞本身還多。

03

頁面要登入,背後的 API 不用

高

後台的會員列表頁面有擋登入,但頁面載入資料時呼叫的 API 網址沒有擋。不經過頁面、直接打 API 網址,資料一樣拿得到。

攻擊者

打開瀏覽器開發者工具,看後台頁面呼叫了哪些網址,再登出後直接打。這次實測:不用登入就能下載整份會員 CSV,還能新增、修改、刪除會員、停權、套用選單。

檢查
# 登出狀態,直接打後台用的 API
curl -s -o /dev/null -w "%{http_code}\n" https://你的網站/後台API/匯出
# 應該是 401 或 302 導到登入頁;回 200 就是有洞

GET 和 POST 都要試,「讀」有擋不代表「寫」有擋。

修法
  • 登入檢查掛在整組路由上(框架的 filter/middleware),不要靠每支程式自己檢查。
  • 沒登入時,API 回 401,頁面導到登入頁。
  • 修之前先查清楚有哪些頁面在呼叫這些 API,避免把正常使用者也擋掉。
替代做法

先「只記錄、不擋」:在整組路由加上檢查,但第一週只把沒登入的呼叫寫進 log,不回 401。看完 log,確定沒有正常功能是在沒登入的狀態下呼叫的,再切成真的擋。檢查掛在路由群組,不改每支 controller,要拿掉時只要改一行。副作用:觀察期那一週破口還在,資料很敏感的話就直接擋,並準備好一鍵回滾。

這次遇到:會員資料 API 讀寫都不用登入。修好後,未登入一律回 401;用資料庫副本比對,登入後的 22 個網址結果跟修改前完全一樣。

04

除錯、測試頁留在正式站

高

開發時寫來看錯誤、測資料庫連線、修資料的小程式,上線時沒刪掉。

攻擊者

掃常見檔名,例如 test_db.php、phpinfo.php、error_viewer.php、fix_errors.php。這些頁面常會顯示資料庫帳密、錯誤 log,甚至可以直接改資料。有些還留著 ?allow_remote=1 這種「開放遠端看」的參數。

檢查
find . -maxdepth 2 -name '*.php' | grep -i -E 'test|debug|phpinfo|fix|error_view|backup'
curl -s -o /dev/null -w "%{http_code}\n" https://你的網站/test_db.php
修法

從部署內容中移除;一時刪不掉,就在網站設定裡對這些路徑一律回 403。

替代做法

先不刪檔,在網站設定加一條規則讓這些路徑回 403;之後要用,再拿掉規則就好。真的需要在正式站除錯時,只允許公司 IP,或透過 SSH tunnel 從本機開。

# nginx
location ~ ^/(test_db|fix_errors|error_viewer|phpinfo)\.php$ { return 403; }

副作用:不動程式,規則隨時可以拿掉。

這次遇到:3 支除錯頁對外可開,升級後已改成回 403。

05

上傳目錄可以寫入也可以執行

高

為了讓上傳「不會出權限錯誤」,部署時把目錄設成 chmod 777。結果任何人都能寫,而且放進去的 .php 檔還能被執行。

攻擊者

只要找到一個上傳點,或任何能寫檔的漏洞,就上傳一支 .php 後門(web shell),之後從瀏覽器就能控制主機。

檢查
find . -type d -perm -0002 -not -path './vendor/*'      # 所有人都能寫的目錄
find uploads writable -name '*.php' 2>/dev/null          # 上傳目錄裡不該有 .php
修法
  • 目錄只給網站執行身分(例如 www-data)寫入權限,不要開 777。
  • 上傳目錄設成禁止執行 PHP,或乾脆放在網站根目錄之外。
  • 上傳檔名改用系統產生,不要沿用使用者給的檔名和副檔名。
替代做法

上傳程式不用改,在網站設定讓上傳目錄「不執行 PHP」就好;權限從 777 改成「擁有者是網站身分、權限 755/644」,網站照樣可以寫檔。

# nginx
location ~ ^/(uploads|writable)/.*\.php$ { deny all; }
# Apache(放在上傳目錄的 .htaccess 或 vhost)
php_admin_flag engine off

副作用:如果真的有功能把 .php 放在上傳目錄裡執行,那個功能會壞掉,但那本來就是後門的做法。

這次遇到:舊的部署設定每次都把可寫目錄設成 0777。改成用容器啟動時把目錄擁有者改成網站身分,拿掉 777。

06

正式站開著開發模式

中

框架的環境設定(例如 CI_ENVIRONMENT、APP_ENV、APP_DEBUG)在正式站還是 development。

攻擊者

故意弄出錯誤,例如打一個不存在的網址、送奇怪的參數,錯誤頁就會顯示完整程式路徑、程式碼片段、SQL,有時還有環境變數。這些都是下一步攻擊的地圖。

檢查
curl -s https://你的網站/不存在的頁面-xyz | grep -i -E 'stack|trace|\.php|exception'
修法

正式站設成 production,並在 php.ini 設 display_errors = Off、log_errors = On:錯誤寫進 log,不要顯示給使用者看。

替代做法

只改環境變數,不改程式。需要看錯誤時去看 log 檔(writable/logs/、docker logs),不要開在畫面上。要讓開發者在正式站看細節,可以在 log 裡記一個錯誤編號,畫面上只顯示這個編號。副作用:開發者要改成看 log 找錯誤。

這次遇到:有一個站的正式環境設成 development,404 頁會回傳 stack trace。已改成 production。

07

錯誤頁洩漏伺服器路徑

中

程式本身有 bug,每次呼叫都噴 Fatal error,錯誤訊息裡帶著伺服器上的完整檔案路徑。

攻擊者

從路徑知道主機的目錄結構、框架和版本,配合其他漏洞時,就知道該讀哪個檔、往哪裡寫。

檢查

用無效的參數打每一支公開 API,看回應裡有沒有 Fatal error、/var/www/、/app/ 這類字樣。

修法

修 bug,並套用 06 的設定。這次的原因是程式和框架新版的方法簽名不合,升級之後就修好了:無效的 token 現在回 404 和一句中文說明。

替代做法

在框架加一個全域的錯誤處理:記完整的錯誤到 log,畫面只回固定格式,例如 {"success":false,"message":"系統錯誤","id":"錯誤編號"}。原本的 bug 還沒修好之前,這樣做也能先擋住洩漏。副作用:前端看到的錯誤訊息會變簡單,要靠編號回頭查 log。

09

帳號密碼寫死在程式裡

中

程式裡直接寫著 admin/admin123 這類帳密。即使現在沒有頁面在用,它還是存在 git 紀錄裡。

攻擊者

只要原始碼外洩(例如 .git 資料夾被公開,或 repo 權限設錯),就能直接拿來登入。或者以後有人把這個驗證又掛回某個路由,就等於開了一扇預設密碼的門。

檢查
grep -rn -i -E "password\s*=\s*['\"]|admin123|123456" app/ config/ --include=*.php
修法

刪掉那段程式,帳密一律改從環境變數讀。如果這組密碼別的地方也用過,要一併更換。

替代做法

分兩步走,避免改的當下就把人鎖在外面:第一步,把寫死的值搬到環境變數,值先保持一樣,上線確認正常;第二步,再換成新密碼。功能根本沒在用的話,直接刪掉最乾淨。副作用:第一步完成前,舊密碼還在 git 紀錄裡,所以第二步一定要做。

這次遇到:一個沒在用的 Basic Auth 過濾器寫死了 admin123,已刪除;用舊帳密打 API 確認回 401。

10

測試站對外公開、沒有密碼

中

測試分支部署成一個對外網址,方便大家看,但沒有加任何保護。

攻擊者

測試站通常版本最舊、設定最鬆,有時還連著正式資料庫。攻擊者會從 DNS 紀錄或憑證紀錄找到 test.、dev.、staging. 這類網址,先從這裡進來。

檢查

列出所有對外的子網域,逐一確認:不開就看不到的站有沒有加密碼?它連的是哪一個資料庫?

修法

不用時就停掉;需要時加 Basic Auth 或限制來源 IP,而且一定要用獨立的測試資料庫。

替代做法

不用停站,在反向代理或 Cloudflare Access 這一層加上 Basic Auth 或 IP 白名單,測試站本身的程式完全不用改。副作用:看測試站的人要多輸入一次密碼。

13

升級本身把功能弄壞

中

為了修漏洞而升級,結果某個功能壞了,而且可能過幾天才被使用者發現。

常見原因
  • 新版 PHP 改了函式的預設值或棄用參數,例如 CSV 相關函式的 escape 參數。
  • 框架新版改了讀取 cookie 和請求資料的方式。
  • 程式的方法和框架新版的同名方法簽名衝突,直接 Fatal error。
  • 部署方式跟著改了,例如目錄權限、啟動指令。
做法

每一層都要和「改之前」比,不是只看有沒有通過:

  • 升級前先查官方文件,列出兩個版本的差異,用 PHPCompatibility 掃出要改的程式位置。
  • 在還沒改的程式上先跑一次單元測試,當作基準。
  • 用資料庫副本,同時跑新舊兩版,打同一批網址比對回應。
  • 上線後跑 smoke 測試和基準比對,並觀察 log。
  • 最後列出「最可能壞掉的地方」,請人登入實際操作一次。
替代做法

升級和功能修改分開提交、分開上線,出問題時才知道是哪一個造成的。上線時用新容器取代舊容器(藍綠部署),舊版本保留著,回滾只要一行指令。副作用:要多維護一個部署設定,換來的是幾分鐘內就能退回原狀。

這次遇到:測試抓到 1 個因框架改版造成的失敗,已修。新舊版比對 27 個網址,有 4 個不同,都是預期中的修正(原本會壞掉的 API 變正常、錯誤頁不再洩漏)。

五分鐘自我檢查
curl -sI https://你的網站/ | grep -i x-powered-by                    # 08 版本號有沒有露出來
php -i | grep -E 'upload_progress.enabled|memory_limit|display_errors' # 01 11 06
curl -s -o /dev/null -w "%{http_code}\n" https://你的網站/test_db.php  # 04 除錯頁
curl -s -o /dev/null -w "%{http_code}\n" https://你的網站/.git/config  # 原始碼外洩
# 03:登出狀態下,把後台頁面呼叫的 API 逐一打一次

資料來源:php.net 2026-10-21 安全公告,以及這次修補時在實際主機上的檢查與測試。「這次遇到」的內容已經匿名化,不含網址、主機與帳號。整理日期:2026-10-23