PHP2Shell là gì? Chi tiết 2 lỗ hổng nguy hiểm CVE-2026-109619 và CVE-2026-109618
Tổng quan về lỗ hổng PHP2Shell
Thuật ngữ PHP2Shell dùng để chỉ bộ đôi lỗ hổng bảo mật nghiêm trọng trong nhân PHP Core:
- CVE-2026-109619: Lỗ hổng giải phóng bộ nhớ nhưng vẫn sử dụng (Use-After-Free - UAF) nằm trong tính năng theo dõi tiến trình tải lên tệp tin (upload progress) của PHP core.
- CVE-2026-109618: Lỗ hổng hỏng bộ nhớ (Memory Corruption) bị kích hoạt bởi tình trạng tràn bộ nhớ (Out-Of-Memory - OOM).
Điều kiện tiên quyết để xảy ra cuộc tấn công
Để khai thác thành công, hệ thống mục tiêu cần đáp ứng các điều kiện sau:
- Máy chủ chạy phiên bản PHP từ 5 trở lên với cấu hình mặc định session.upload_progress.enabled = On.
- Áp dụng cho bất kỳ endpoint nào chấp nhận tải tệp lên (kể cả endpoint bỏ qua hoặc không xử lý tệp đó), do tầng quản lý session của PHP xử lý tiến trình tải lên trước khi mã nguồn người dùng (userland code) kịp thực thi.
- Kẻ tấn công cần có độ lệch offset đặc thù của bản dựng (build-specific offset) giữa hai hàm zval_ptr_dtor và zend_call_function (có thể bị rò rỉ tại thời điểm thực thi runtime hoặc suy ra từ chính xác tệp nhị phân PHP/bản phân phối của mục tiêu).
Đọc thêm: Kỹ sư VietSunshine phát hiện lỗ hổng CVE-2026-32475 nghiêm trọng trong Elementor Pro
Phân tích kỹ thuật CVE-2026-109619 (Use-After-Free)
Quá trình khai thác CVE-2026-109619 diễn ra tuần tự qua 5 bước kỹ thuật:

Bước 1: Kích hoạt tính năng upload progress
Tính năng upload_progress có sẵn trong các bản dựng PHP mặc định để phản hồi thời gian thực tiến độ tải tệp lên máy chủ. Khi người dùng gửi một yêu cầu multipart/form-data (theo RFC 1867) kèm tham số POST PHP_SESSION_UPLOAD_PROGRESS, PHP sẽ chặn và xử lý trước khi script mục tiêu bắt đầu chạy.
PHP tạo một khóa cụ thể bên trong mảng siêu toàn cục $_SESSION và liên tục cập nhật các thông số như content_length, bytes_processed, tên tệp.
Trong mã nguồn PHP core (php_session.h), cấu trúc dữ liệu được định nghĩa như sau:
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; /* Mảng xuất ra dữ liệu session */
zval files; /* Mảng data["files"] */
zval *post_bytes_processed; /* data["bytes_processed"] */
zval *current_file_bytes_processed;
zval current_file; /* Mảng tệp đang tải lên */
} php_session_rfc1867_progress;
Khi gặp trường tệp tin, bộ phân tích HTTP kích hoạt sự kiện FILE_START. PHP lưu giữ (cache) hai con trỏ zval * trỏ trực tiếp vào các bucket của bảng băm (hash table) trong cấu trúc dữ liệu session:
- zval *post_bytes_processed;
- zval *current_file_bytes_processed;
Kẻ tấn công gửi yêu cầu POST chứa: Cookie/trường PHPSESSID, trường PHP_SESSION_UPLOAD_PROGRESS, và một trường file để kích hoạt lưu tạm 2 con trỏ trên.
Bước 2: Tạo điều kiện giải phóng bộ nhớ (Free condition)
Kẻ tấn công chủ động tạm dừng luồng truyền TCP (pause TCP stream) trong khoảng thời gian lớn hơn 1 giây. Đây là khoảng thời gian mặc định của cấu hình session.upload_progress.min_freq, nhằm vượt qua cơ chế giới hạn tần suất cập nhật tiến độ (rate limiting guard) của PHP.
Bước 3: Kích hoạt giải phóng bộ nhớ (Trigger the free)
Trước lần cập nhật thứ hai, bộ nhớ có trạng thái:
- progress->post_bytes_processed → trỏ tới [bucket trong Tree-A]
- progress->current_file_bytes_processed → trỏ tới [bucket trong Tree-A] (Tree-A là dữ liệu session trực tiếp nằm trong $_SESSION["upload_progress_x"])
Khi dữ liệu tiếp tục được gửi đến, sự kiện FILE_DATA thứ hai diễn ra và gọi hàm php_session_rfc1867_update():
- flush() ghi dữ liệu Tree-A xuống ổ đĩa và đặt trạng thái session_status = none.
- Do trạng thái là none, hàm php_session_initialize() hiểu đây là một phiên làm việc mới, tiến hành đọc lại tệp từ ổ đĩa và giải mã (deserialize) thành một cấu trúc mới hoàn toàn: Tree-B (nằm ở một địa chỉ bộ nhớ khác).
- Hàm php_session_track_init() giải phóng Tree-A.
- zend_hash_update() nạp Tree-B vào $_SESSION. Bộ hủy zval_ptr_dtor của bảng băm chạy trên giá trị cũ (Tree-A), giải phóng toàn bộ Tree-A và các bucket của nó.
Hậu quả: Hai con trỏ progress->post_bytes_processed và progress->current_file_bytes_processed trở thành con trỏ lơ lửng (dangling pointers) do trỏ vào vùng nhớ đã bị giải phóng, trong khi không có tiến trình nào cập nhật lại chúng trỏ sang Tree-B. Đây chính là bản chất lỗi Use-After-Free của CVE-2026-109619.
Bước 4: Kỹ thuật rải bộ nhớ (Heap spray)
Ngay sau khi vùng nhớ bị giải phóng, kẻ tấn công chèn một lượng lớn trường form có tên dạng t[] ngay trong chính thân HTTP POST đó (ví dụ 149 trường):
Content-Disposition: form-data; name="t[]"
<288 bytes cấu trúc giả mạo>
--boundary
Content-Disposition: form-data; name="t[]"
<288 bytes cấu trúc giả mạo>
... (lặp lại 149 lần)
Trình phân tích multipart của PHP (rfc1867.c) xử lý các trường này dưới dạng sự kiện FORMDATA. Từng giá trị được đưa vào zend_string trong mảng $_POST['t'] từ vị trí $_POST['t'][0] đến $_POST['t'][148].
Các cấu trúc này chứa cấu trúc hàm nội bộ giả mạo của PHP tích hợp sẵn lệnh thực thi system("
Bước 5: Kích hoạt thực thi mã
Kẻ tấn công gửi tiếp tệp thứ hai trong cùng yêu cầu POST. PHP ghi đè thông tin vào địa chỉ con trỏ lơ lửng:
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);
}
Do vùng nhớ đã bị tráo đổi thành cấu trúc hàm giả mạo, PHP Core xử lý các con trỏ giả và kích hoạt lệnh hệ thống system("
Phân tích kỹ thuật CVE-2026-109618 (Memory Corruption qua OOM)
Lỗ hổng CVE-2026-109618 xuất phát từ cách thức PHP xử lý khi gặp tình trạng cạn kiệt bộ nhớ (Out-Of-Memory - OOM):
- Kích hoạt OOM: Kẻ tấn công gửi một yêu cầu được thiết kế đặc biệt nhằm tiêu tốn tài nguyên bộ nhớ cực nhanh cho đến khi chạm mức giới hạn memory_limit cấu hình trong PHP (chẳng hạn thông qua các lượt tải tệp dung lượng cực lớn).
- Bỏ qua quy trình dọn dẹp bằng longjmp(): Khi phát hiện OOM, PHP Core phải dừng thực thi. Thay vì trả về qua từng hàm và dọn dẹp biến tuần tự, nhân PHP dùng cơ chế cấp C là longjmp() để tua ngược ngăn xếp (stack unwinding) và nhảy thẳng đến cơ chế đóng yêu cầu (request shutdown).
- Bộ nhớ chưa khởi tạo: Lệnh longjmp() bỏ qua các đoạn mã giải phóng và khởi tạo cục bộ, khiến các cấu trúc dữ liệu nội bộ rơi vào trạng thái chưa khởi tạo, biến động hoặc trở thành con trỏ lơ lửng.
- Thao túng Heap: Kẻ tấn công trước đó đã dàn xếp bộ nhớ Heap (bằng kỹ thuật gửi nhiều trường form t[] tương tự như trên) để lấp đầy các phân vùng nhớ bằng các cấu trúc C giả mạo chứa con trỏ thực thi mã độc.
- Thực thi mã khi tắt tiến trình: Trong giai đoạn php_request_shutdown, PHP quét qua các tài nguyên còn hoạt động để giải phóng. Khi chạm vào vùng nhớ chưa được khởi tạo sau cú nhảy longjmp(), hệ thống đọc cấu trúc giả mạo của kẻ tấn công. Nếu phân nhánh vào một con trỏ hàm (như destructor) trong vùng nhớ hỏng này, quyền điều khiển sẽ chuyển sang mã độc và dẫn tới RCE.
Phương án phát hiện và giảm thiểu bằng Akamai Guardicore Segmentation
Khách hàng sử dụng giải pháp Akamai ExAR đã nhận được thông tin chi tiết về các tài sản bị ảnh hưởng (bao gồm phiên bản PHP, cấu hình web server liên quan) cùng các bước phân đoạn mạng và giảm thiểu rủi ro.
Quản trị viên có thể sử dụng các truy vấn Akamai Guardicore Segmentation Insight dưới đây để rà soát và khoanh vùng ứng dụng tồn tại lỗ hổng:
Dành cho hệ thống Linux
Truy vấn kiểm tra gói cài đặt PHP trên các bản phân phối Linux và phát hiện các phiên bản chưa được vá:
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)
)
Dành cho hệ thống Windows
Truy vấn kiểm tra các bản cài đặt PHP, XAMPP, WampServer, Laragon hoặc Chocolatey/Scoop trên máy chủ Windows:
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)
Hai lỗ hổng nghiêm trọng mang tên PHP2Shell (CVE-2026-109618 và CVE-2026-109619) mở ra nguy cơ thực thi mã từ xa (RCE) trên mọi máy chủ chạy PHP từ phiên bản 5 trở lên. Các tổ chức và quản trị viên cần tiến hành rà soát hạ tầng và cập nhật bản vá cho PHP ngay lập tức.
Nguồn: Understanding PHP2Shell: Everything You Need to Know





Loading ...