iT邦幫忙

0

深入解析 PHP2Shell 漏洞原理與防護

  • 分享至 

  • xImage
  •  

重點摘要

  • 2026 年 10 月 20 日,PHP 宣布將於 10 月 21 日 14:00 UTC(台灣時間22:00)釋出修補程式,修正兩項重大漏洞。

  • 這兩項漏洞合稱 PHP2Shell,因為攻擊者可利用它們達成遠端程式碼執行(Remote Code Execution,RCE)。

  • 漏洞細節原定於 10 月 22 日公開,但包括 CVE 編號(CVE-2026-109619 與 CVE-2026-109618)在內的資訊提前外洩。

  • CVE-2026-109619 是 PHP核心上傳進度功能中的釋放後使用(Use-After-Free,UAF)漏洞。

  • CVE-2026-109618
    則是由記憶體不足(Out-of-Memory,OOM)狀態觸發的記憶體毀損漏洞。

  • 本文將詳細解析這兩項漏洞的成因與運作機制。

攻擊成立的前提條件

  • 目標使用 PHP 5 或更新版本,且 session.upload_progress.enabled = On(預設設定)。

  • 目標存在可接收檔案上傳的端點,即使應用程式本身忽略上傳檔案也一樣:PHP的工作階段層會在使用者層程式碼執行前,先處理上傳進度。

  • 攻擊者必須取得該編譯版本中 zval_ptr_dtor 與 zend_call_function之間的位址偏移量;此資訊可由執行階段洩漏取得,或根據目標實際使用的 PHP執行檔與發行版推導。

CVE-2026-109619 漏洞細節

利用這項漏洞可分為五個步驟:

  1. 啟用上傳進度功能

  2. 建立記憶體釋放的觸發條件

  3. 觸發記憶體釋放

  4. 進行堆積噴灑

  5. 觸發漏洞利用

https://ithelp.ithome.com.tw/upload/images/20261023/20164635WYz0bpiuHS.jpg

圖 1 CVE-2026-109619 漏洞利用流程

圖中流程由左至右:① 開始上傳,觸發 upload_progress,指標指向 $_SESSION 資料。②暫停連線超過 1 秒並觸發釋放,產生 UAF 與懸空指標。③ 堆積噴灑,以 t[]酬載填入含有 system() 的偽造結構。④ 觸發 RCE,執行system('cmd')。圖中將正文的「建立條件」與「觸發釋放」合併為同一階段。

步驟一 啟用上傳進度功能

PHP 的預設編譯版本提供upload_progress(上傳進度)功能,可即時回報檔案已成功上傳至伺服器的進度。

使用者透過 multipart/form-data(RFC 1867 所描述的格式)上傳資料,且包含名為PHP_SESSION_UPLOAD_PROGRESS 的 POST 參數時,PHP會在目標指令碼開始執行前攔截並處理該參數。

PHP 會立即在 $_SESSION 陣列中建立特定的索引鍵。$_SESSION是內建的超全域關聯陣列,用於在同一次網站造訪期間,跨頁面儲存與存取使用者資料。檔案逐區塊上傳時,PHP會持續更新該工作階段陣列中的資訊,例如content_length(總大小)、bytes_processed(已處理位元組數)與檔案名稱。

在底層,PHP 的 HTTP 剖析器遇到檔案欄位時,會觸發內部 FILE_START事件。為了有效率地追蹤進度,PHP 會建立直接指向記憶體中該工作階段陣列的 zval指標,也就是 PHP 核心內部使用的 C 語言記憶體指標,讓 C層級引擎快速寫入收到的位元組計數。

以下是 PHP 核心原始碼 php_session.h 中的 php_session_rfc1867_progress 結構:

typedef struct _php_session_rfc1867_progress {

zval sid;

smart_str key;

zend_long update_step;

zend_long next_update;

double next_update_time;

bool cancel_upload;

bool apply_trans_sid;

size_t content_length;

zval data; /* 匯出至工作階段資料的陣列 */

zval files; /* data["files"] 陣列 */

zval *post_bytes_processed; /* data["bytes_processed"] */

zval *current_file_bytes_processed;

zval current_file; /* 目前上傳檔案的陣列 */

} php_session_rfc1867_progress;

PHP 在處理 multipart 上傳時,會快取兩個原始 zval * 指標:

zval *post_bytes_processed;

zval *current_file_bytes_processed;

因此,當攻擊者送出包含下列內容的 POST 請求時:

  • PHPSESSID Cookie/欄位:設定工作階段識別碼。

  • PHP_SESSION_UPLOAD_PROGRESS 欄位:啟用進度追蹤。

  • 檔案欄位:觸發 FILE_START,並快取上述兩個指標。

當使用者送出觸發進度追蹤的 POST 請求後,這兩個 zval 指標會指向已解碼進度資料樹中,仍有效的雜湊表儲存格(bucket)。

步驟二 建立記憶體釋放的觸發條件

攻擊者將 TCP 資料串流暫停超過 1 秒,也就是預設的session.upload_progress.min_freq 間隔。這是為了通過 PHP對進度更新頻率的限制檢查。

步驟三 觸發記憶體釋放

此時,在第二次 update() 執行前,記憶體狀態如下:

progress->post_bytes_processed → [ Tree-A 中的 bucket ]

progress->current_file_bytes_processed → [ Tree-A 中的 bucket ]

Tree-A 是目前有效且已解碼的工作階段資料,儲存在 $_SESSION 的"upload_progress_x" 索引鍵下。

資料傳輸恢復時,會觸發第二個 FILE_DATA事件。由於已經過足夠時間,php_session_rfc1867_update()
會再次執行,並依序產生下列流程:

flush() 將 Tree-A 寫入磁碟並重設狀態

flush 完成後,將 session_status 設為 none。

php_session_initialize() 重新讀取工作階段檔案

由於狀態為 none,PHP 將此視為開啟新的工作階段。它從磁碟讀取檔案,並將資料反序列化為全新的資料樹,以下稱為Tree-B。Tree-B 配置在不同的記憶體位址,內容雖然與 Tree-A相同,但實際上是一份副本。

php_session_track_init() 銷毀 Tree-A

此時,PHP 中有兩份使用相同工作階段索引鍵的資料樹:

  • Tree-A:舊的資料樹,仍位於 $_SESSION 中。

  • Tree-B:剛完成反序列化的新資料樹。

zend_hash_update() 將 Tree-B 放入 $_SESSION。雜湊表的pDestructor(zval_ptr_dtor)會處理舊值 Tree-A,釋放 Tree-A 及其所有 bucket。

此時,progress->post_bytes_processed 與 progress->current_file_bytes_processed所指向的記憶體已被釋放,因此成為懸空指標。

Tree-B 位於新的位址,但快取的指標並未更新。

也就是說,PHP 先快取指向雜湊表內部的原始 zval *指標,之後卻銷毀並替換雜湊表,沒有同步更新這些指標。這便造成 UAF,也是CVE-2026-109619 的漏洞根源。

步驟四 進行堆積噴灑

記憶體釋放後,攻擊者立即在同一次上傳的 HTTP POST 主體中加入大量名為 t[]的表單欄位。為了提高填滿目標配置區域的機率,使用的並非單一欄位:

--boundary

Content-Disposition: form-data; name="t[]"

<288 位元組的偽造結構>

--boundary

Content-Disposition: form-data; name="t[]"

<288 位元組的偽造結構>

...(共 149 個)

PHP 的 multipart 剖析器(rfc1867.c)會將這些欄位當作 FORMDATA 事件處理。每個值會轉為 zend_string,存入 PHP 陣列 $_POST['t']。t[]這個名稱表示將值附加至陣列,因此 149 個值會依序存入 $_POST['t'][0] 到$_POST['t'][148]。

每個欄位都包含偽造的 PHP 內部函式結構,其中嵌入system("<command>")。這些配置的大小經過設計,目的是佔用剛被釋放的相同記憶體區塊。若配置器重新分配該區塊,攻擊酬載便可佔據它,使懸空指標轉而指向攻擊者的酬載。

步驟五 觸發漏洞利用

攻擊者在同一個 POST 請求中送出第二個檔案。PHP透過懸空指標,向它認為仍是進度結構的記憶體寫入資料:

case MULTIPART_EVENT_FILE_DATA: {

const multipart_event_file_data *data = event_data;

if (!Z_TYPE(progress->sid) || !progress->key.s) {

break;

}

Z_LVAL_P(progress->current_file_bytes_processed) = data->offset +
data->length;

Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed;

php_session_rfc1867_update(progress, false);

}

但該位置此時已是攻擊者偽造的函式結構。當 PHP引擎存取這些遭毀損的資料時,便會沿著偽造指標執行,呼叫 system("攻擊者的命令")。

CVE-2026-109618 漏洞細節

這項漏洞的問題在於:OOM 迫使 PHP以非預期方式中止執行,可能略過正常的清理程序,導致未初始化的記憶體遭惡意覆寫,進而被用於執行程式碼。

攻擊者送出特製請求,快速消耗記憶體,直到達到 PHP 設定的memory_limit。大量檔案上傳便可能造成這種情況。

PHP 核心偵測到 OOM狀態後,必須停止執行。它不會循正常函式返回流程逐步清理變數,而是使用底層 C 語言機制 longjmp(),直接跳離目前的呼叫流程,進入請求結束處理機制。

由於 longjmp()突然跳過區域清理與初始化程式碼,部分內部結構會處於不穩定、未初始化或懸空的狀態。

攻擊者可以利用此情況,在 OOM 發生前先操控堆積配置,例如使用前述大量送出 t[]表單欄位的技巧。

這會將含有惡意執行指標的偽造 C 語言結構填入記憶體區域。

最後,在 php_request_shutdown 階段,PHP會依序處理並釋放仍有效的資源。當引擎遇到因 longjmp()中斷而留下的未初始化記憶體時,便可能直接讀取攻擊者偽造的結構。若引擎透過該毀損記憶體中的函式指標(例如解構函式)進行跳轉,執行流程就會轉向攻擊者的酬載,造成
RCE。

使用 Akamai Guardicore Segmentation 緩解風險

AkamaiExAR客戶已收到事件通知,內容列出受影響資產,包括有漏洞的 PHP版本、使用中的網頁伺服器整合方式,以及建議的緩解與網路分段措施。

以下 Akamai Guardicore Segmentation Insight 查詢可用於偵測有漏洞的應用程式,並協助進行分段:

Linux 資產

編註:原稿下列 Linux 查詢有多處截斷,包含REGEX_MATCH、版本字串與型別轉換運算式,無法直接執行。此處保留原始內容供對照;執行前須取得完整查詢。

WITH LINUX AS (

SELECT DISTINCT name, version, source

FROM deb_packages

WHERE REGEX_MATCH(name, '^php[0-9][0-9.]*-(common|cli)$', 0) IS NOT NULL

UNION ALL

SELECT DISTINCT name, version, source

FROM rpm_packages

WHERE REGEX_MATCH(name, '^(rh-)?php[0-9.]*[uw]?(-php)?-(common|cli)$', 0) IS
NOT NULL

),

PARSED AS (

SELECT name, source, version,

CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 1) AS INTEGER) AS
major,

CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 2) AS INTEGER) AS
minor,

CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9) AS patch,

CASE

WHEN version LIKE '%8.5.9-0ubuntu2.%'

THEN CAST(REGEX_MATCH(version, '0ubuntu2\.([0-9]+)', 1) AS INTEGER) >= 1

WHEN version LIKE '%8.5.4-0ubuntu1.%'

THEN CAST(REGEX_MATCH(version, '0ubuntu1\.([0-9]+)', 1) AS INTEGER) >= 5

WHEN version LIKE '%8.3.6-0ubuntu0.24.04

THEN CAST(REGEX_MATCH(version, '24\.04\.([0-9]+)', 1) AS INTEGER) >= 13

WHEN version LIKE '%8.1.2-1ubuntu2.%'

THEN CAST(REGEX_MATCH(version, '1ubunt >= 28

WHEN version LIKE '%7.4.3-4ubuntu2.%'

THEN CAST(REGEX_MATCH(version, '4ubunt * 1000

+ COALESCE(CAST(REGEX_MATCH(versiGER), 0) >= 29005

WHEN version LIKE '%7.2.24-0ubuntu0.18.0

THEN CAST(REGEX_MATCH(version, '18\.04 1000

+ COALESCE(CAST(REGEX_MATCH(version, 'esm([0-9]+)', 1) AS INTEGER), 0) >=
17014

WHEN version LIKE '%7.0.33-0ubuntu0.16.0

THEN CAST(REGEX_MATCH(version, '16\.04\.([0-9]+)', 1) AS INTEGER) * 1000

+ COALESCE(CAST(REGEX_MATCH(versiGER), 0) >= 16022

WHEN version LIKE '%5.5.9+dfsg-1ubuntu4.

THEN CAST(REGEX_MATCH(version, '1ubunt * 1000

+ COALESCE(CAST(REGEX_MATCH(version, 'esm([0-9]+)', 1) AS INTEGER), 0) >=
29018

ELSE 0

END AS ubuntu_fixed

FROM LINUX

)

SELECT name, source, version

FROM PARSED

WHERE ubuntu_fixed = 0

AND (

(major = 8 AND minor = 5 AND patch < 12)

OR

(major = 8 AND minor = 4 AND patch < 27)

OR

(major = 8 AND minor = 3 AND patch < 36)

OR

(major = 8 AND minor = 2 AND patch < 35)

OR

(major = 8 AND minor IN (0, 1))

OR

(major BETWEEN 4 AND 7)

)

Windows 資產

WITH WINDOWS AS (

SELECT DISTINCT name, version, install_location AS source

FROM programs

WHERE publisher = 'The PHP Group'

OR (name LIKE 'XAMPP%' AND publisher IN ('Bitnami', 'Apache Friends'))

UNION ALL

SELECT DISTINCT path AS name, product_version AS version, 'php.exe' AS source

FROM file

WHERE path LIKE 'C:\php%\php.exe'

OR path = 'C:\Program Files\PHP\php.exe'

OR path LIKE 'C:\Program Files\PHP\%\php.exe'

OR path = 'C:\Program Files (x86)\PHP\php.exe'

OR path LIKE 'C:\Program Files (x86)\PHP\%\php.exe'

OR path = 'C:\xampp\php\php.exe'

OR path LIKE 'C:\wamp%\bin\php\%\php.exe'

OR path LIKE 'C:\laragon\bin\php\%\php.exe'

OR path LIKE 'C:\tools\php%\php.exe'

OR path = 'C:\ProgramData\chocolatey\lib\php\tools\php.exe'

OR path LIKE 'C:\Users\%\scoop\apps\php\current\php.exe'

),

PARSED AS (

SELECT name, source, version,

CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 1) AS INTEGER) AS
major,

CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 2) AS INTEGER) AS
minor,

CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 3) AS INTEGER) AS
patch

FROM WINDOWS

)

SELECT name, source, version

FROM PARSED

WHERE

(major = 8 AND minor = 5 AND patch < 12)

OR

(major = 8 AND minor = 4 AND patch < 27)

OR

(major = 8 AND minor = 3 AND patch < 36)

OR

(major = 8 AND minor = 2 AND patch < 35)

OR

(major = 8 AND minor IN (0, 1))

OR

(major BETWEEN 4 AND 7)

修補重點

合稱 PHP2Shell的兩項重大漏洞(CVE-2026-109618與CVE-2026-109619),可讓攻擊者在執行PHP 5 或更新版本的伺服器上達成 RCE。請立即修補 PHP。

持續關注研究動態

Akamai 資安情報團隊(Security Intelligence Group)將持續監控、報告這類威脅,並為客戶及整體資安社群制定緩解措施。如欲掌握團隊的最新消息,請造訪研究首頁,並追蹤我們的社群媒體。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言