
2026 年 10 月 21 日 21 點 55 分,高市首相在 X 上發了這則貼文。
若難以迅速套用修補程式,例如也可暫時停掉網站等,作為降低風險的有效措施之一。
(高市早苗 on X)
這是兩個可任意執行程式碼的 PHP 漏洞,通稱 php2shell(CVE-2026-109618 / CVE-2026-109619,CVSS v4.0 9.2)的那一夜。
那天晚上,我負責維運的某家線上服務業者 A 公司旗下多個網站,都是跑在 PHP 7.4 上。這是沒有官方修正版釋出的版本。我們正是那種「難以迅速套用修補程式」的一方。
即便如此,我們沒有停站,而是靠 WAF 與 PHP 設定把漏洞補起來。寫成這樣很像很厲害,但實際上做的只有兩件事:「在入口擋掉 multipart」以及「在 ini 裡加一行」。補完之後,真實攻擊就真的打到日誌裡來了。這篇文章按時間順序記錄那一夜的判斷、處置方式,以及途中踩到的坑。
我會寫這篇文章,是因為那一夜其實有點好玩。也許這聽起來不太莊重,但 X 和聊天群裡,不斷有人分享:目前已經確認到哪裡、該在哪裡停手、哪些暫時性對策有效。邊看邊動手,會有一種和不認識的人一起撐過同一個夜晚的感覺。既然當時收到了很多資訊,我也想把我們那一夜發生的事情分享出來。
受保密義務限制,文中的公司名、服務名、網域、IP 位址、規模等都已隱去。文中的網域已替換為
example.com。架構與設定皆以當晚當下為準,之後的處置狀況不在本文範圍內。
在講那一夜之前,先說明這個漏洞到底允許了什麼。不了解這點,後面的應對看起來可能會覺得太大費周章。以下是根據公開分析整理的內容。
攻擊者直接能拿到的有兩項:
| 可得內容 | 內容 | 出處 |
|---|---|---|
| 任意程式碼執行 | 不需登入,就能以執行 PHP 的使用者權限執行命令 | php.net、Sansec |
| 記憶體內容 | 類似 Heartbleed,其他使用者請求中的密碼、Session 權杖、API 金鑰有可能外洩 | Sansec |
第二項尤其讓人不寒而慄。伺服器上可能什麼都沒被寫入,入侵痕跡也不明顯,但使用者的登入資訊卻可能已經被偷走了。
再往下,就是一般在 Web 伺服器被執行程式碼時會發生的事。以下並不是這次實際觀測到的攻擊行為,而是通常會出現的風險。
一個 POST 就能打開這些入口。實際上,當晚我的負責站點也真的收到了那個 POST(後半段會提)。
做出停站決定的大型服務也不少。以下是報導中可確認的例子(時間以報導當下為準,之後可能有變更)。
| 服務 | 停止服務時間帶(報導時點) | 出處 | |
|---|---|---|---|
| BASE | 10/21 15:50 ~ 10/22 7:00 | GIGAZINE | |
| Melonbooks | 10/21 17:30 ~ 10/22 10:00 | 同上 | |
| NicoNico | KADOKAWA | 10/21 19:00 ~ 未定 | 同上 |
| BANDAI CHANNEL | BANDAI | 10/21 20:00 ~ 未定 | 同上 |
| pixiv | pixiv | ~ 10/22 早晨 | 同上 |
| BOOK☆WALKER | BOOK☆WALKER | ~ 10/22 早晨 | 同上 |
| Uma Musume Pretty Derby | Cygames | 10/21 22:00 左右 ~(提前自 23 點) | 同上 |
| Granblue Fantasy | Cygames | 10/21 22:00 左右 ~(提前自 23 點) | 同上 |
| Shadowverse: Worlds Beyond | Cygames | 10/21 23:00 左右 ~ 未定 | Fami通 |
| Pokémon Sleep | 任天堂 | 10/21 23:00 ~ 未定 | GIGAZINE |
| Heaven Burns Red | WFS | 10/21 22:30 之後 | Game Spark(Yahoo!新聞) |
| makeshop / カラーミーショップ | 各公司 | 10/21 晚間 ~ 10/22 | 日本網路經濟新聞(Yahoo!新聞) |
遊戲、電商、影音、電子書,完全不分業種,全都停了。從上面那麼寬的攻擊入口來看,為了避免損失營收而決定停站,可說是相當沉重的判斷。像 BASE 這樣,甚至沒有等到 23 點修正版釋出,就在傍晚先停下來了。
停站當然是一種正確選擇。不過一旦停站,營收與使用者信任都會受損。A 公司的網站選擇的是「不關站也要守住」。
| 項目 | 內容 |
|---|---|
| 網站 | 多個服務(使用相同樣板建立的架構) |
| 架構 | AWS ALB → EC2(nginx + php-fpm),各網站各自以 ASG 管理多台主機 |
| PHP | 7.4 系列。透過第三方 repository 安裝,官方支援已於 2022 年結束 |
| 管理畫面 | 以同一台伺服器上的另一個主機名稱運作。已在 nginx 以來源 IP 限制 |
| WAF | 沒有 |
最麻煩的是 PHP 7.4。根據 php.net 的公告,可直接取得修正版的只有 8.2~8.5。7.4 / 8.0 / 8.1 只有最小限度的原始碼修補(GitHub 分支)。也就是說,透過套件安裝的 PHP,無法直接靠「更新套件就結束」。
一開始,我們先把影響判定軸定成 3 個:
調查時使用 AWS Systems Manager(SSM)的 Run Command,並且只在全機台執行唯讀指令。由於有些非 Web 用途的伺服器也可能裝了 PHP,所以我們不以伺服器名稱排除,而是全部檢查。
# 在所有主機上執行的部分確認命令(唯讀)
php -v | head -1
php -i | grep -E '^session\.upload_progress\.enabled'
ss -ltnp | grep php-fpm # php-fpm 實際在哪裡聽候連線
結果如下。
| 對象 | 判定 | 理由 |
|---|---|---|
| Web 伺服器上的 PHP | 有影響・高風險 | 全部都是 7.4。且與攻擊有關的 session.upload_progress.enabled 預設為 On |
| 同一天被報導的 nginx 漏洞(PoolSlip) | 低風險 | 沒有會觸發的 rewrite 寫法 |
| 非 Web 伺服器上的 php-fpm | 外部不可達 | 只在 127.0.0.1 上聽候連線,也沒有 nginx 轉送的路徑 |
根據報導,攻擊使用的是 multipart/form-data(檔案上傳格式)的請求。因此我們考慮做一個「在入口擋掉 multipart」的 WAF。
不過如果擋得太粗暴,正常功能也會壞掉。在提出方案前,我們從兩個方向確認影響範圍。
FormData、原生 App 的表單送出等)與接收端($_FILES 等)
結論是:「只要封鎖送到非管理畫面主機的 multipart,對使用者沒有影響」。
「Host 是管理畫面就排除」這種條件,只要偽造 Host 標頭就能繞過。因此我們進一步用兩層檢查,確認如果真的偽造,會發生什麼事。
allow + deny all)內側。我們也確認了巢狀 location另外,我們把 Web ACL 依各個 ALB 分開。如果用同一份 ACL 來排除所有站點的管理畫面,只要對別的網站 ALB 送出「其他站點管理畫面的 Host」,就能繞過排除。繞過後打到的,就是該 ALB 的 default server(PHP)。
把規則 1 的概念寫成 JSON,大致如下(網域為示意):
{
"Name": "block-multipart-except-admin",
"Priority": 0,
"Action": { "Block": {} },
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "multipart",
"FieldToMatch": { "SingleHeader": { "Name": "content-type" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS"
}
},
{
"NotStatement": {
"Statement": {
"ByteMatchStatement": {
"SearchString": "admin.site-a.example.com",
"FieldToMatch": { "SingleHeader": { "Name": "host" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "EXACTLY"
}
}
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "block-multipart-except-admin"
}
}
此外,我們也先把 AWS 托管規則(CommonRuleSet / KnownBadInputs)以 Count(僅記錄) 模式加入。日誌送到 CloudWatch Logs,只保留 Count 或 Block 的項目 30 天。
一開始先把所有規則都設為 Count,確認沒有誤判後,再切換成 Block。
23 點時,PHP 官方公告發布。確定可用的暫時對策是 session.upload_progress.enabled=0。不過公告也明確寫著:「沒有完整的暫時解法,更新是唯一安全的選擇。」對於不在修正版範圍內的 7.4 來說,只能疊加暫時對策。
我們沒有直接動 php.ini 本體,而是額外加了一個檔案。如此一來,回復也很簡單:刪掉這個檔案後 reload 即可。
; /etc/php.d/99-php2shell-mitigation.ini
; CVE-2026-109618 / CVE-2026-109619 的暫時對策
session.upload_progress.enabled = 0
套用設定需要 reload php-fpm。但這個環境的 process_control_timeout 仍是預設值 0。這代表 reload 的瞬間,正在處理中的請求會變成 502。
因此我們採用一次從 ALB 上移除一台、套用、再加回去的方式。
單台的操作流程如下:
TG_ARN="arn:aws:elasticloadbalancing:...:targetgroup/site-a/..."
ID="i-0123456789abcdef0"
aws elbv2 deregister-targets --target-group-arn "$TG_ARN" --targets Id="$ID"
aws elbv2 wait target-deregistered --target-group-arn "$TG_ARN" --targets Id="$ID"
# 在該主機上(透過 SSM Run Command)
# cp 99-php2shell-mitigation.ini /etc/php.d/
# systemctl reload php-fpm
# php-fpm -i | grep session.upload_progress.enabled # => Off
aws elbv2 register-targets --target-group-arn "$TG_ARN" --targets Id="$ID"
aws elbv2 wait target-in-service --target-group-arn "$TG_ARN" --targets Id="$ID"
處理期間,我們每秒對網站送出一次請求,結果全部都維持 200。本番與測試環境加起來十幾台,約 10 分鐘內完成。
WAF 以 Count 模式啟用後,日誌立刻開始出現以下紀錄。
| 時間 | 來源 | 內容 | 結果 |
|---|---|---|---|
| 23:37 | 英國的 VPS | 對 https://example.com/(apex 網域)送出 multipart 的 POST。大小分別為 8,828 B 與 25,212 B |
WAF 尚在 Count 中。被 ALB 的「apex → www」301 重新導向擋住 |
| 23:37 | 同上 | 跟隨 301 後對 https://www.example.com/ 發出 GET(沒有本文) |
因為是 GET,所以攻擊資料不會被帶過去 |
第一波攻擊被擋住的,其實是偶然啟用的 ALB 重新導向規則。攻擊工具盯的是 apex 網域,而且在跟隨 301 時把 POST 改成了 GET,所以攻擊資料沒有到 PHP。

原本不是為了防禦而加上的那條 301,反而成了當晚的第一道防線。當然,這只是運氣好;如果對方直接打 www,Count 模式的 WAF 就會被直接穿過去。所以我們立刻把它改成 Block。
從 Count 切到 Block 時,我們只把 get-web-acl 取出的 Rules 裡的 Action 改掉,再原封不動地送回 update-web-acl。雖然沒有報錯,但測試請求卻沒有變成 403。
測試請求是送一個只有 Content-Type 是 multipart、但本文為空的 GET 到靜態檔案。PHP 不會把 GET 的本文當成 multipart 解析,因此即使真的打到正式環境也無害。
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Content-Type: multipart/form-data; boundary=x' \
https://www.example.com/robots.txt
# 期望值: 403 實際: 200
原因在於字串處理方式:
get-web-acl 回傳的 SearchString 是 base64--cli-binary-format raw-in-base64-out 再送回去,實際上會把base64 字串本身當成搜尋字串註冊結果規則不是在找 multipart,而是在找 bXVsdGlwYXJ0
aws wafv2 get-web-acl --scope REGIONAL --name site-a-acl --id "$ACL_ID" \
| jq -r '.WebACL.Rules[0].Statement.AndStatement.Statements[0]
.ByteMatchStatement.SearchString' \
| base64 -d
那段時間,WAF 還很認真地等著 bXVsdGlwYXJ0 這個 Content-Type 出現,但這種請求當然不會來。
後來我直接用手上原始的明文定義重建,並且同時確認解碼結果與 curl 的 403。那一刻真的體會到:沒有出錯卻沒有生效,才是最可怕的。
切到 Block 之後,同樣兩種大小的 POST 接連出現,而且全部都被 Block 了。
| 時間 | 來源 | 內容 | 結果 |
|---|---|---|---|
| 23:51~ | 法國、美國、德國、波蘭的 VPS(5 個 IP) | 8,828 B 與 25,212 B 的 multipart POST | WAF Block |
Python aiohttp。攻擊端看起來也在用非同步 I/O 提升效率GET /。應該是先探路WAF 日誌不會留下送出的資料內容,所以這裡只能寫成「疑似是針對本次漏洞的流量」。
接著,我們用唯讀方式檢查伺服器端有沒有受害。
| 確認項目 | 為什麼要看 | 結果 |
|---|---|---|
| php-fpm worker 異常結束(例如 signal 11) | 記憶體破壞型攻擊常見 | 10 月只有月初 1 筆 |
| 記憶體上限、執行逾時錯誤 | 另一個 CVE 的發動路徑 | 10 月為 0 筆 |
| php-fpm 底下的子程序是否有 shell 被啟動 | 是否有落地 | 沒有 |
| 最近變更的 PHP 檔案與執行檔 | 是否有 Web Shell | 只有我們自己的發版變更 |
| cron 變更、對外連線 | 是否有駐留與外連 | 沒有 |
在檢查範圍內,沒有看到入侵或竄改痕跡。
前面提到的「一個 POST 就能打開的入口」,第一個入口就是被 WAF 擋住了。如果沒有 WAF,這 5 個 IP 的 POST 就會直接送進 php-fpm。
這裡又踩到一個坑。要按日統計 php-fpm log 時,如果把 21-Oct-2026 這種日期字串直接排序,月份順序會亂掉。用 tail 截取時,甚至可能把 10 月的內容切掉。
# NG:日期會以字串順序排序,tail 之後可能把 10 月的列丟掉
grep 'signal 11' /var/log/php-fpm/error.log | awk '{print $1}' | tr -d '[' \
| sort | uniq -c | tail
# OK:先限定想看的月份,再統計
grep 'signal 11' /var/log/php-fpm/error.log | grep 'Oct-2026' \
| awk '{print $1}' | tr -d '[' | sort | uniq -c
在提交報告前,我們有即時發現並重新統計。
許多用 RPM 安裝 PHP 7.4 的環境,都是使用 Remi 的 repository(rpms.remirepo.net)。php.net 的公告曾這樣寫:
If you rely on the volunteer-maintained
rpms.remirepo.net, we urge you to switch to distribution packages or build from source, so you can immediately apply updates. The repo will not have updates today.
(PHP 2026 News Archive)
原本應該是「今天不會有更新」。但在日子轉換後直接查看 repository 時,發現 7.4.33-31 已於 10/21 上架。看來志工維護者那邊的「今天」,比公告寫得還長了一些。RPM 的變更紀錄中也明確寫到了這次 CVE。
2026-10-21 Remi Collet <[email protected]> - 7.4.33-31
- Mitigation fix Avoid state corruption via bailout
CVE-2026-109618, CVE-2026-109619
| 位置 | 套件 | 版本 |
|---|---|---|
| EL8 / EL9 / EL10 的 php74 模組 | php-* |
7.4.33-31 |
| EL8 / EL9 的 safe(SCL 形式) | php74-php-* |
7.4.33-31 |
當時 Remi 的部落格還沒發公告文。若只等公告,很可能就沒注意到。若你也是用 Remi,比起看部落格,直接看 repository 和變更紀錄會更快。
# 更新(模組形式;若是 SCL 則用 'php74-php*')
sudo dnf update 'php*'
# 用變更紀錄確認是否已套用
rpm -q --changelog php-common | head -5
# 如果出現 CVE-2026-109618, CVE-2026-109619,就代表已更新
要注意兩點:
CommonRuleSet 的 NoUserAgent_Header,和原生 App 以及 WebSocket 的正常通訊大量重疊。因為它們都不會帶 User-Agent。SizeRestrictions_Body 則與管理畫面的 CSV 匯入相符。
如果當初是「直接把托管規則設成 Block」,整個應用程式很可能都會停掉。幸好我們先用 Count 觀察日誌,才避開這個坑。
在用 WAF 的 BlockedRequests 搭配 Rule 維度查詢時,指定的不是規則名稱,而是 VisibilityConfig.MetricName。如果傳規則名稱,查出來會是 0 件。這會讓人誤以為「根本沒有 Block」。
為了避免「今夜做完就算了」,我把 WAF 日誌的定期檢查做成 Claude Code 的 Skill。採唯讀方式檢查以下項目:
multipart(用來偵測又發生二重 base64)第一次執行,就找到了上面提到的「5 個 IP 的 Block」。
rpm -q --changelog如果你暫時無法立刻升版,kenimoto.dev 已整理出 php.net 官方提供的 7 種暫時對策,以及各自無法防住的範圍。本文使用的 ini 與 WAF 之外的方式(memory_limit、post_max_size、cgroup 上限等)也有收錄。
如首相貼文所說,停站也是有效措施之一。不過,就算不停站,也有方法可以守住。WAF 和 ini 都只是緩解措施,根本對策仍是升到 8.x(雖然這件事從 2022 年支援結束後就一直被反覆提起)。如果你也是用 7.4 撐過這次,下一個夜晚來臨前,還能推進到什麼程度呢?
我把這一夜的流程整理成 15 張投影片了,歡迎拿去和團隊分享。
php2shell 的那一夜,不停機守住 PHP 7.4 正式環境 —— WAF、1 行 ini 與真實攻擊 by 井本賢
最後讓我順便宣傳一下。要在入口擋下什麼、要如何確認沒有被入侵。當 AI agent 被放進正式環境時,同樣的問題也會出現。MCP 伺服器的驗證設計,以及基於 OWASP MCP Top 10 的攻擊與防禦,我已整理在書籍《MCP 實戰資安 AI 代理時代的 Model Context Protocol 安全營運指南》(インプレス)中。
Kindle 版 / 平裝版