> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# PHP2Shell 來了：用 systemd drop-in 換掉 CloudPanel 後台的 PHP
- URL: https://taiwanding.com/php2shell-lai-liao-yong-systemd-drop-in-huan-diao-cloudpanel-hou-tai-de-php/
- Published: 2026-10-22T04:52:21.000Z
- Updated: 2026-10-22T04:52:21.000Z
- Author: Kevin Chen
- Tags: #php, php2shell, #RCE

上篇最後，我的第二台主機停在這個狀態：網站用的 PHP 已經升到 8.4.27，九個沒在用的版本都停用了，只剩 CloudPanel 後台自己的 PHP 還卡在 8.1.34。

PHP 8.1 已經在 2025 年底結束上游官方支援，PHP 專案不會再發布 8.1 的正式修補版本，雖然部分發行版或平台可能會自行回移植修補，但不能假設自己裝的套件已經被修過，我確認過這台的狀況：CloudPanel 套件庫裡的 `php8.1-fpm` 沒有新版本可以更新（`apt policy` 顯示的已安裝版本和候選版本相同），執行檔的編譯日期也還是 2025 年 12 月，早於這次修補，所以可以確定後台的 PHP 還沒有修。

於是我先把後台的埠從防火牆關掉，改走 SSH 通道進去，到這裡，後台已經不再直接暴露在公網上，遠端攻擊的風險大幅降低，但舊版 PHP 本身的漏洞仍然存在，例如主機上其他服務如果有 SSRF 之類的漏洞，或攻擊者已經從別的地方取得本機存取權限，還是有機會從主機內部連到這個後台，所以我決定進一步處理：把後台換到 8.4.27。

這篇的內容，大部分是我在過程中一直追問「為什麼可以這樣做」問出來的，指令本身沒幾行，比較值得帶走的是背後的兩個機制，以及動手前怎麼判斷會不會壞、動手後怎麼確認真的換好了。

## 先說清楚：這不是官方做法

CloudPanel 沒有提供切換後台 PHP 版本的設定，GitHub 上也有人開了 feature request，請官方把後台搬到有支援的版本，到我寫這篇時還沒有官方回應。

所以接下來的內容，是**在我這一台主機上成功的非官方修改紀錄**，不是適用於所有 CloudPanel 版本的升級程序，有幾個代價要先知道：

- 後台程式是官方搭配 8.1 釋出的，在 8.4 上沒有官方驗證，換版本後某些功能可能會壞。
- 以後 CloudPanel 更新時，可能會跟你的改動打架。
- 出問題時，官方不會幫你處理。

我最後還是決定試，是因為三個條件都成立：動手前拍了 VPS 快照；改動集中在一個獨立的檔案裡，隨時可以還原；而且 8443 已經不對外，就算後台換壞了，主要影響是我自己暫時進不了後台，不會因此多開一個對外的入口。

如果你只是想先降低風險，上篇的「關埠加 SSH 通道」可以先做，這篇是給想多走一步、或想搞懂背後原理的人。

## 後台的 PHP 長什麼樣子

上篇用來找出所有 PHP 的那一行，在這台主機上列出了十一個 master process，其中一個長這樣：

```text
root     php-fpm: master process (/home/clp/services/php-fpm/fpm/php-fpm.conf)

```

先看它是由哪個 systemd 服務啟動的：

```bash
systemctl cat clp-php-fpm

```

```ini
# /usr/lib/systemd/system/clp-php-fpm.service
[Unit]
Description=The CLP PHP 8.1 FastCGI Process Manager
Documentation=man:php-fpm8.1(8)
After=network.target

[Service]
Type=notify
PIDFile=/run/php/clp-php-fpm.pid
ExecStart=/usr/sbin/php-fpm8.1 -R -F -y /home/clp/services/php-fpm/fpm/php-fpm.conf -c /home/clp/services/php-fpm/fpm/php.ini
ExecReload=/bin/kill -USR2 $MAINPID
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

```

`ExecStart` 那行的四個參數：

- `-R`：允許 FPM 的 pool 以 root 身分執行（預設不允許）。這影響的是實際處理請求的 worker process，不是 master process，master 本來就以 root 啟動，再依照每個 pool 設定的 `user`、`group` 降低 worker 的權限。而且 `-R` 只是「允許」，加了它不代表 worker 就會用 root 跑，實際身分還是看 pool 的設定
- `-F`：在前景執行，交給 systemd 管理
- `-y`：指定 FPM 的設定檔
- `-c`：指定 php.ini

再向 systemd 問這個服務的主程序，看它實際跑的是哪個執行檔：

```bash
sudo readlink -f /proc/$(systemctl show -p MainPID --value clp-php-fpm)/exe
# /usr/sbin/php-fpm8.1

```

直接向 systemd 查 `MainPID`，會比用 `pgrep` 依名稱去找更準，不會意外抓到其他剛好含有相同字串的程序。

看到這裡，整件事的關鍵就出來了：**後台用的就是系統裝的 `php-fpm8.1` 執行檔，只是配上自己的一套設定檔。** 執行檔是一回事，設定是另一回事，兩者是分開的。

那如果把執行檔換成同一台主機上已經修好的 `php-fpm8.4`，設定照舊，會怎樣？要回答這個問題，得先搞懂兩件事。

## 為什麼換執行檔可行：nginx 和 PHP 之間隔著一個 socket

網頁請求進來之後，大致是這樣流動的：

```text
瀏覽器 → 後台的 nginx (8443) → /run/clp-php-fpm.sock → php-fpm → 後台的 .php 程式碼
                                                        ↑
                                              這裡從 8.1 換成 8.4

```

nginx 本身不會執行 PHP，它是透過一個 socket 用 FastCGI 協定，把請求交給 php-fpm 處理，這個設計帶來三個結果：

**nginx 不知道也不在乎後面是哪個版本**，它只認得 socket 的路徑。

**socket 的路徑寫在設定檔裡，不在執行檔裡**，後台的 FPM 設定檔裡寫著要在哪裡開 socket，換執行檔之後，設定檔沒變，socket 也就還是同一個。

**後台程式碼只是一堆 `.php` 檔**，誰來執行都可以，只要語法和用到的函式在新版本還存在。

另外，PHP-FPM 8.1 和 8.4 都支援這裡用到的啟動參數，所以可以嘗試保留原本的設定檔、只替換執行檔。但這只是「可以嘗試」，設定指令、擴充套件和實際服務的相容性，還是要一項一項確認，後面會講怎麼確認。

順帶確認一下，這個 socket 有沒有被網站用到：

```bash
# 後台的 FPM 開了哪些 socket
sock=$(sudo grep -rhoP '^\s*listen\s*=\s*\K\S+' /home/clp/services/php-fpm/)
echo "$sock"

# 哪些設定檔有用到這些 socket
sudo grep -rl "$sock" /etc/nginx /home/clp/services 2>/dev/null

```

我的結果是兩個 socket（後台本體和檔案管理器各一個），只出現在 `/home/clp/services` 底下，網站用的 `/etc/nginx` 裡完全沒有。根據這台主機的 nginx 與 FPM 設定，沒有發現網站前台的請求使用 CloudPanel 的 FPM socket，所以切換後台的 PHP，理論上不會改變網站前台使用的 PHP 版本。要注意這個檢查的範圍只有 nginx 設定，不能完全排除其他服務或管理腳本依賴 CloudPanel 原本的 PHP 環境。

你可能會想：網站的 `php8.4-fpm` 也在跑，後台再用同一個執行檔跑一份，不會衝突嗎？它們是兩個獨立的 master process，各自讀不同的設定檔，開不同的 socket、寫不同的 PID 檔，只是剛好用同一個程式來跑。只要兩邊的 socket、PID 檔、log 路徑沒有重複，就不會互相干擾，不過兩邊各有自己的 worker，記憶體用量是分開算的，小台主機要留意一下。

## 為什麼可以「疊」一層設定：systemd 的 drop-in

知道換執行檔可行之後，下一個問題是：怎麼改？

最直覺的做法是直接編輯 `/usr/lib/systemd/system/clp-php-fpm.service`，但這個檔案是 CloudPanel 套件裝的，下次套件更新時就會被覆寫回去。

systemd 的設定其實是分層的：

- **`/usr/lib/systemd/system/`**：套件裝的原始檔，屬於軟體廠商，套件更新時會被覆寫。
- **`/etc/systemd/system/服務名稱.service.d/*.conf`**：管理員自己加的 drop-in，套件更新時**通常不會碰** `/etc` 底下的這些檔案。

systemd 啟動服務時，會先讀原始檔，再把 drop-in 裡寫到的項目疊上去，沒寫到的就沿用原本的，所以 drop-in 裡只需要寫你要改的那幾行，其他像 `Type=notify`、`PIDFile`、`Restart=always`，全部沿用 CloudPanel 原本的設定。

這個設計本來就是留給管理員用的：不動廠商的檔案，把自己的調整放在另一個地方，套件升級也不會洗掉，要還原就把自己建立的 drop-in 刪掉。`systemctl edit` 就是 systemd 提供來建立 drop-in 的指令。（`systemctl revert` 也能還原，但它影響的範圍比較大，後面「出事怎麼還原」會說明。）

有一個小細節要注意，大部分設定是「後寫的蓋掉先寫的」，但 `ExecStart` 是「累加」的，寫兩次會變成兩個指令，而一般服務只允許一個，所以 drop-in 裡要先寫一行空的 `ExecStart=` 清掉原本的值，再寫新的：

```ini
[Service]
ExecStart=
ExecStart=/usr/sbin/php-fpm8.4 ...

```

## 動手前：檢查會壞的地方

機制搞懂了，接下來是判斷「換過去會不會壞」，我拆成三個可以事先檢查的地方，再加上一個只能實際測試的。

### 一、參數相容

前面 `systemctl cat` 已經看到原本的啟動參數，新的 `ExecStart` 照抄，只換執行檔路徑。也要順便確認原本的服務裡，有沒有其他地方也寫死了 8.1，例如 `ExecStartPre`，我這台只有 `ExecStart` 一處。

另外，原本的服務是 `Type=notify`，代表 php-fpm 啟動完成時要主動通知 systemd，新版本的執行檔也要支援這個機制，服務才會被判定為啟動成功，這點在後面實際啟動時就會知道。

### 二、設定檔在新版本讀得過

php-fpm 有一個 `-t` 參數，只檢查設定檔、不實際啟動，正好拿來測試後台的設定在 8.4 底下有沒有已經被淘汰的參數：

```bash
sudo php-fpm8.4 -t \
  -y /home/clp/services/php-fpm/fpm/php-fpm.conf \
  -c /home/clp/services/php-fpm/fpm/php.ini

```

看到 `test is successful` 就過了。要注意 `-t` 只代表設定檔通過檢查，不代表 socket 一定能成功建立、服務一定能正常運作，更不代表後台程式相容，它只是排除了其中一種會壞的原因。

### 三、擴充套件都在

換了執行檔，載入的擴充套件也會改用 8.4 那一套，如果後台用到某個 8.1 有、8.4 沒有的擴充，那個功能就會壞。

先看後台自己的 php.ini 裡，有沒有手動載入什麼擴充：

```bash
grep -nE '^\s*(zend_)?extension' /home/clp/services/php-fpm/fpm/php.ini

```

我這台有兩行：`extension=memcached.so` 和 `extension=redis.so`。這兩行只寫檔名、沒寫完整路徑，PHP 會到「當下這個版本」自己的擴充目錄去找，所以換成 8.4 之後，會自動載入 8.4 版的這兩個檔案，前提是 8.4 也有裝。

這裡還有一個前提：這份 php.ini 沒有另外用 `extension_dir` 指定擴充目錄，如果有指定、而且指到 8.1 的目錄，8.4 就會去載入 8.1 編譯的擴充而失敗。如果你看到的是寫了完整路徑、或路徑裡帶著版本號的擴充（例如某些加密載入器），也要特別小心，換版本之後它幾乎一定會載入失敗。

另外，PHP 除了讀 `-c` 指定的 php.ini，還會掃描各版本自己的額外設定目錄（例如 `/etc/php/8.4/fpm/conf.d/`），很多擴充其實是從那裡載入的，所以光看 php.ini 不夠，要看 PHP 實際載入了哪些模組。

先用 `diff` 比對兩個版本的差異：

```bash
diff \
  <(php-fpm8.1 -c /home/clp/services/php-fpm/fpm/php.ini -m 2>&1) \
  <(php-fpm8.4 -c /home/clp/services/php-fpm/fpm/php.ini -m 2>&1)

```

我的結果只有兩行差異：

```text
< imap
> random

```

- `imap`：PHP 8.4 把它從核心移到 PECL，所以 8.4 少了它，後台是管主機和網站的，用到收信功能的機會很低，這個差異可以接受。
- `random`：8.2 之後新增的內建模組，多了不影響。

但 `diff` 只會顯示「兩邊不一樣」的地方，如果兩個版本出現同樣的警告，或兩邊都沒載入某個模組，`diff` 反而看不出來，所以還要直接檢查 8.4 這一邊：

```bash
php-fpm8.4 -c /home/clp/services/php-fpm/fpm/php.ini -m 2>&1 \
  | grep -Ei '^(redis|memcached)$|Warning|Unable to load'

```

要看到 `redis` 和 `memcached` 兩行都出現，而且沒有任何 `Warning` 或 `Unable to load`，才代表後台需要的擴充在 8.4 底下都載入成功了。

### 四、只能實際測試的：程式碼相容性

前面幾項都過之後，剩下的未知數是後台的程式碼本身在 8.4 上有沒有相容問題，這只能實際點頁面測試。這也是 CloudPanel 官方遲遲沒換的原因：他們要對所有功能、所有使用者的環境負責，測試成本比我們自己改一台高得多。

## 動手：建立 drop-in

開始前兩件事：先到 VPS 業者的管理介面拍一次快照，另外多開一個 SSH 視窗留著，萬一這個視窗出狀況還有退路。

`systemctl edit` 會打開編輯器讓你寫 drop-in，如果想直接貼指令，也可以用下面這種寫法，效果一樣：

```bash
sudo mkdir -p /etc/systemd/system/clp-php-fpm.service.d

sudo tee /etc/systemd/system/clp-php-fpm.service.d/override.conf >/dev/null <<'EOF'
[Unit]
Description=The CLP PHP FastCGI Process Manager (override: PHP 8.4)

[Service]
ExecStart=
ExecStart=/usr/sbin/php-fpm8.4 -R -F -y /home/clp/services/php-fpm/fpm/php-fpm.conf -c /home/clp/services/php-fpm/fpm/php.ini
EOF

sudo systemctl daemon-reload
sudo systemctl restart clp-php-fpm

```

`Description` 那行不是必要的，但改了之後，以後用 `systemctl status` 一眼就會看到這個服務被改過，不會忘記。

確認結果：

```bash
systemctl status clp-php-fpm --no-pager | head -5
sudo readlink -f /proc/$(systemctl show -p MainPID --value clp-php-fpm)/exe

```

我看到的是：

```text
● clp-php-fpm.service - The CLP PHP FastCGI Process Manager (override: PHP 8.4)
     Loaded: loaded (/usr/lib/systemd/system/clp-php-fpm.service; enabled; preset: enabled)
    Drop-In: /etc/systemd/system/clp-php-fpm.service.d
             └─override.conf
     Active: active (running) since ... ; 10ms ago
/usr/sbin/php-fpm8.4

```

`Drop-In` 那段代表 systemd 有讀到我們的設定，最後一行代表服務的主程序換成 8.4 的執行檔了，如果路徑後面出現 `(deleted)`，代表程序還在用已經被換掉的舊執行檔，要再重啟一次。服務能進入 `active (running)`，也代表 8.4 的執行檔有正確回應 `Type=notify` 的啟動通知。現在再跑一次 `systemctl cat clp-php-fpm`，會看到原始檔和 drop-in 一前一後印出來，就是前面說的「疊加」。

## 確認它有在穩定運作

這裡有一個容易被騙的地方：`active (running)` 後面寫的是 `10ms ago`，也就是剛啟動的瞬間。

原本的服務設定了 `Restart=always`、`RestartSec=3`，意思是掛掉三秒後會自動重啟，如果新版本一啟動就崩潰，你剛好在某次重啟的瞬間看狀態，看到的一樣是 `active (running)`，所以要多等幾秒，看它有沒有在反覆重啟：

```bash
sleep 10
systemctl show clp-php-fpm -p NRestarts -p ActiveEnterTimestamp

# 用「正在跑的那個程序」的執行檔查版本
exe=$(sudo readlink -f /proc/$(systemctl show -p MainPID --value clp-php-fpm)/exe)
"$exe" -v | head -1

```

`NRestarts=0`、啟動時間沒有變，代表這段觀察期間沒有被 systemd 自動重啟，不過這只能說明「這段時間沒問題」，不能直接證明長期穩定，之後幾天偶爾看一下就好。

版本那行刻意從 `MainPID` 反查執行檔，而不是直接跑 `php-fpm8.4 -v`，因為後者只是在問磁碟上那個檔案的版本，不能證明服務真的在用它。

## 實際點一遍後台

另開一個視窗盯著錯誤訊息：

```bash
sudo journalctl -u clp-php-fpm -f

```

然後透過 SSH 通道進後台，把常用的功能點一遍：登入、儀表板、網站列表、網站設定（包含 PHP 設定頁）、檔案管理、資料庫管理、使用者管理。

有兩個地方特別值得看：

- **檔案管理**：它在後台的 FPM 裡用的是另一個獨立的 pool，雖然一起換成了 8.4，但值得實際開個資料夾、看個檔案試試。
- **資料庫管理（phpMyAdmin）**：首頁右側的伺服器資訊會顯示 PHP 版本，看到 8.4.27，就能確認後台與 phpMyAdmin 的頁面已經改用 8.4。

要注意這個確認的範圍：這次改的只有 `clp-php-fpm` 這個服務，如果 CloudPanel 還有排程工作或命令列工具是直接呼叫 `php8.1`，它們不會因為這次修改而跟著換。可以簡單查一下還有誰在用 8.1：

```bash
# 正在跑的程序
ps -eo pid,user,cmd | grep -v grep | grep 'php8\.1'

# 系統排程
sudo grep -rn 'php8\.1' /etc/cron* /var/spool/cron 2>/dev/null

```

命令列工具和排程不直接處理網頁請求，跟這次漏洞的攻擊方式關係比較小，但還是要知道它們沒有一起換。

我這台的後台各頁面都正常，後台與 phpMyAdmin 也確認改用 8.4.27 了。

## 出事怎麼還原

任何一頁壞掉、或整個後台打不開，就把這次建立的 drop-in 刪掉：

```bash
sudo rm /etc/systemd/system/clp-php-fpm.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart clp-php-fpm

```

服務就會回到 CloudPanel 原本的 8.1，這也是我敢動手的原因：改動只存在一個獨立的檔案裡，原本的東西一個字都沒碰。

你可能會看到別的地方建議用 `systemctl revert clp-php-fpm`，它也能還原，但範圍比較大：它會刪掉這個服務在 `/etc/systemd/system/` 和 `/run/systemd/system/` 底下**所有的** drop-in，如果服務被 mask 過，也會一併解除 mask。如果你用 `systemctl cat` 確認過這個服務只有你自己建立的這一個 override，用 `revert` 沒有問題；不確定的話，只刪除自己建立的那個檔案比較保險。

## 這次學到的

這套思路其實不只適用於 CloudPanel，任何「廠商的服務綁著一個舊版本」的情況都可以這樣想：

- 先找出它實際在跑什麼：`systemctl cat` 找啟動方式，`MainPID` 搭配 `readlink` 找執行檔。
- 分清楚「執行檔」和「設定」：很多時候兩者是分開的，只換其中一個是可以嘗試的。
- 改設定用 drop-in，不要動廠商的原始檔。
- 動手前把能檢查的先檢查：參數、設定檔、相依套件，而且每一項檢查都要知道它證明了什麼、沒證明什麼。
- 比對差異之外，也要直接確認需要的東西真的在，`diff` 只看得到不一樣的地方。
- 確認結果時，問「正在跑的那個程序」，不要只問磁碟上的檔案。
- 驗證不了的，就先準備好還原的方法，而且搞清楚還原指令會動到哪些東西，然後實際測試。
- 不要只看一次狀態就下結論，等幾秒、看重啟次數，之後也偶爾回頭看。

## 參考資料

- [PHP Manual: FastCGI Process Manager (FPM)](https://www.php.net/manual/en/install.fpm.php?ref=taiwanding.com)
- [systemd.unit — Unit configuration（drop-in 的說明）](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html?ref=taiwanding.com)
- [systemctl（revert 的說明）](https://www.freedesktop.org/software/systemd/man/latest/systemctl.html?ref=taiwanding.com)
- [Feature request: upgrade the PHP version used by CloudPanel's own php-fpm - CloudPanel Issue #816](https://github.com/cloudpanel-io/cloudpanel-ce/issues/816?ref=taiwanding.com)
- [Upcoming Critical PHP Security Release - CloudPanel Issue #823](https://github.com/cloudpanel-io/cloudpanel-ce/issues/823?ref=taiwanding.com)
- [PHP2Shell: Critical PHP vulnerabilities patched, update now! - Help Net Security](https://www.helpnetsecurity.com/2026/10/21/php2shell-php-vulnerabilities-patched-cve-2026-109618-cve-2026-109619/?ref=taiwanding.com)