Skip to content

PHP2Shell: từ lỗi sử dụng bộ nhớ đến thực thi mã từ xa trên server

PHP2Shell

PHP2Shell (CVE-2026-109618 và CVE-2026-109619) cho thấy một lỗi sử dụng bộ nhớ trong lõi PHP có thể thành thực thi mã từ xa không cần xác thực, và các PoC bắt đầu lưu hành trong vòng một ngày sau khi thông tin bị rò rỉ.

PHP engine nằm ở đâu, và vì sao điều đó quan trọng

Trước khi đi vào chi tiết, cần hình dung rõ một điểm: khi có người gửi một yêu cầu tới một website chạy bằng PHP, PHP engine luôn là nơi nhận và xử lý yêu cầu đó đầu tiên. Mã của chính website (phần hiển thị nội dung, xử lý đơn hàng, kiểm tra đăng nhập...) chỉ bắt đầu chạy sau khi PHP engine đã xử lý xong phần việc của nó. Sơ đồ dưới đây minh họa thứ tự đó.

PHP engine nằm trong tiến trình Apache, ví dụ mod_php

Hình 1. PHP engine nằm trong Apache, ví dụ mod_php.

Với mod_php, PHP engine nằm ngay bên trong tiến trình Apache (phần mềm web server, tức chương trình chạy sẵn trên máy chủ để nhận yêu cầu từ trình duyệt và trả kết quả về) và dùng chung một vùng nhớ với Apache.

Như đã nói ở trên, PHP engine luôn xử lý request trước khi mã của ứng dụng kịp chạy. Vì vậy, nếu chính PHP engine gặp lỗi bộ nhớ, cuộc tấn công đã xảy ra xong trước khi ứng dụng có cơ hội kiểm tra đăng nhập: bước xác thực bị bỏ qua hoàn toàn, không phải vì ứng dụng có lỗi. Đây là lý do PHP đánh giá mod_php là cấu hình rủi ro cao nhất cho PHP2Shell.

Hậu quả cụ thể: kẻ tấn công có thể chạy lệnh tùy ý trên server với quyền của chính tiến trình Apache. Quyền đó đủ để cài backdoor, đánh cắp dữ liệu, hoặc dùng server làm bàn đạp tấn công sang hệ thống khác, mà không cần biết bất kỳ mật khẩu hay tài khoản nào.

Phần dưới đây đi sâu hơn vào mốc thời gian và đánh giá chính thức từ PHP cùng các nhà nghiên cứu.

Bối cảnh: một lỗi ở lõi PHP, không phải ở ứng dụng

Ngày 21/10/2026, dự án PHP công bố hai lỗ hổng RCE không cần xác thực, cùng mang điểm CVSS v4.0 là 9.2 (Critical), kèm các bản vá 8.5.12, 8.4.27, 8.3.36 và 8.2.35 (Rescana). Tên PHP2Shell đến từ Wiz Research, đơn vị đã tìm ra lỗi cùng Calif và OpenAI rồi báo cáo cho PHP từ tháng 8 (Recorded Future).

Điểm khiến PHP2Shell đáng làm ví dụ là lỗi nằm ở chính PHP engine, không phải ở một website cụ thể nào. Theo Recorded Future, một website vẫn có thể bị ảnh hưởng dù các phần code khác của nó, do chính người viết website đó viết ra, hoàn toàn an toàn, vì cuộc tấn công không đi qua những phần code ấy, mà đi thẳng vào PHP engine bên dưới, nơi phụ thuộc vào runtime PHP và cấu hình server chứ không phải mã của website (Recorded Future).

CVE-2026-109618 CVE-2026-109619
Tên gọi trong advisory RCE via OOM corruption RCE via stale global state
Điểm CVSS v4.0 9.2 9.2
Nhóm lỗi Hỏng bộ nhớ khi xử lý hết bộ nhớ Trạng thái global trỏ vào vùng nhớ đã giải phóng
Bản vá 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35

Cơ chế ở mức khái niệm: con trỏ còn sống, vùng nhớ đã chết

Cả hai lỗi cùng một họ: PHP giữ một con trỏ vào vùng nhớ của request, vùng nhớ đó bị giải phóng, nhưng con trỏ vẫn được dùng tiếp. Đây là lớp lỗi use-after-free (UAF). Khi vùng nhớ cũ được cấp phát lại cho dữ liệu khác, chương trình đọc hoặc ghi vào đó mà không biết nó đã đổi chủ. Nói cách khác: con trỏ không được giải phóng (dọn sạch) hoàn toàn sau khi vùng nhớ nó trỏ tới đã bị thu hồi. Đó là gốc rễ của toàn bộ lỗi này.

Con trỏ sau khi vùng nhớ bị thu hồi, ba bước

Hình 2. Con trỏ sau khi vùng nhớ bị thu hồi, ba bước. Minh họa khái niệm UAF, không phải chuỗi khai thác cụ thể.

Cả CVE-2026-109618 và CVE-2026-109619 đều là biến thể của đúng lỗi này. Một xảy ra khi PHP thoát khẩn cấp lúc hết bộ nhớ, để lại một con trỏ trỏ vào vùng nhớ chưa được dọn đúng cách. Một xảy ra khi trạng thái nội bộ của một request không được dọn sạch trước khi worker xử lý request tiếp theo, khiến request sau vô tình dùng lại con trỏ cũ của request trước (Rescana).

Tác động PHP xác nhận cho CVE-2026-109619 là heap use-after-free không cần xác thực, lộ bộ nhớ và RCE với quyền của web worker (Rescana). Bài viết này không đi vào chuỗi khai thác chi tiết. Một lưu ý khi tham khảo PoC trên GitHub: tác giả của hai PoC công khai sau đó đã bác bỏ lý thuyết cached-pointer mà chúng dựa vào, nên không nên dùng các PoC này để mô tả cơ chế (Recorded Future).

Vì sao lớp lỗi này khó bắt khi review mã

Bình thường, PHP dọn dẹp vùng nhớ theo đúng trình tự ở bước gọi là request shutdown, như dọn văn phòng cuối ngày, mọi thứ cất đúng chỗ. Nhưng khi gặp sự cố giữa chừng (hết bộ nhớ, lỗi nghiêm trọng), PHP thoát khẩn cấp bằng zend_bailout()/longjmp(), như báo cháy, bỏ qua lối đi thường để thoát ngay, bất kể đang xử lý dở gì (Offensive Security). Lối thoát đó không dọn dẹp đúng trình tự, nên có thể để lại một vùng nhớ bị coi là "trống" trong khi vẫn còn thứ trỏ vào nó.

Đây là lý do loại lỗi này khó lộ ra khi review hoặc test thông thường. Unit test và fuzzer phần lớn khám phá đường thành công; để chạm đường lỗi, phải chủ động tạo điều kiện hiếm gặp như cạn bộ nhớ hoặc một exception đúng lúc, những thứ ít khi nằm trong kịch bản test định kỳ.

PHP từng gặp đúng kiểu lỗi này nhiều lần trước đây:

  • 2010: lỗi xảy ra khi PHP xử lý các hàm đặc biệt kiểm soát việc đọc và ghi thuộc tính của một object.
  • 2015: nhiều lỗi dùng lại vùng nhớ đã giải phóng xảy ra khi PHP giải mã (unserialize) một số kiểu dữ liệu đặc biệt.
  • 2024: một lỗi xảy ra khi một đoạn mã kết hợp một hàm đặc biệt với một toán tử gán có điều kiện, đúng lúc có exception.

Điểm chung của cả ba lần: vòng đời của một object trong bộ nhớ và đường xử lý lỗi hoặc ngoại lệ giao nhau ở một chỗ mà PHP không lường trước.

PHP2Shell đi theo đúng mẫu hình đó, chỉ khác ở chỗ lối thoát khẩn cấp lần này là do hết bộ nhớ và do bản ghi lỗi khi có lỗi nghiêm trọng, thay vì do hàm đặc biệt hay exception. Đây không phải vấn đề riêng của PHP: trong bất kỳ phần mềm nào viết bằng C và tự quản lý bộ nhớ, kiểu lỗi "dùng lại vùng nhớ đã đóng" này có thể xảy ra ở bất cứ đâu đường xử lý lỗi không khớp với đường xử lý bình thường.

Vòng đời của CVE này: khai thác xuất hiện trước bản vá

Dòng thời gian PHP2Shell, 9 mốc từ 19/8 đến 26/10/2026

Hình 3. Dòng thời gian PHP2Shell, 9 mốc từ 19/8 đến 26/10/2026.

Từ lúc Wiz báo cáo (24/8) đến ngày có bản vá (21/10) là gần hai tháng. Cuối khoảng đó thông tin rò rỉ chiều 20/10, nên PHP đẩy ngày phát hành từ 22/10 lên 21/10 (Simon Willison). Các mốc 17/10, 21/10, 24/10 và 26/10 lấy từ Rescana, các mốc tháng 8 và tháng 9 từ Simon Willison.

Mức độ ảnh hưởng: lỗi ở runtime kéo theo mọi thứ chạy trên nó

Vì lỗi nằm ở PHP, cập nhật CMS hay plugin không đủ để vá: nhóm nghiên cứu nhấn mạnh rằng cập nhật một ứng dụng không cập nhật runtime PHP bên dưới nó. Họ cũng cho biết PHP chạy khoảng 70% website (Rescana). Bảng dưới tóm tắt các chiều ảnh hưởng đã được nguồn xác nhận. Để dễ hình dung quy mô: PHP hiện chạy trên khoảng 70% website toàn cầu, nên mỗi con số dưới đây chỉ là một lát cắt trong bức tranh rất lớn đó.

Khía cạnh Quan sát Nguồn
Quy mô phơi nhiễm Modat đếm 4.384.302 dịch vụ PHP trên 1.886.612 host. VirusTotal thấy khoảng 5,1 triệu IP chạy PHP, nhưng chỉ khoảng 1,1 triệu IP công bố phiên bản. Recorded Future
Nhánh hết hỗ trợ Trong số IP công bố phiên bản, khoảng 758.000 chạy nhánh đã hết vòng đời (5.4 đến 8.1), so với khoảng 272.000 trên nhánh còn hỗ trợ. Recorded Future
Phần mềm bị chứng minh khai thác Wiz chứng minh RCE không cần đăng nhập trên WordPress, Drupal và Joomla (CVE-2026-109618), WordPress và Drupal (CVE-2026-109619), dùng container image mới nhất lúc thử. Rescana
Lộ bí mật không cần RCE Nhóm nghiên cứu cho biết CVE-2026-109619 có thể làm lộ bộ nhớ worker, gồm mật khẩu, token, API key và chứng chỉ của request khác. Rescana
Khai thác thực tế Amazon ghi nhận một nỗ lực khai thác ngày 17/10, bốn ngày trước khi công bố. watchTowr quan sát khai thác từ 21/10 qua mạng honeypot của họ. Rescana
Lan sang dịch vụ khác Khoảng 394.000 host PHP cũng mở SSH, 233.000 mở MySQL và 68.000 mở RDP. Recorded Future
Danh mục KEV của CISA CISA thêm cả hai CVE vào KEV ngày 26/10/2026, hạn xử lý 29/10/2026 cho các cơ quan dân sự liên bang Mỹ. Rescana

Cần đọc các con số khai thác thận trọng. Amazon nói chưa rõ các nỗ lực đó có thành công trên hệ thống thật hay không. Patchstack, ngay sau khi PHP phát hành bản vá, cho biết telemetry của họ mới thấy hoạt động dò quét, chưa có bằng chứng rõ ràng về xâm nhập. Ngày 24/10, một nhà nghiên cứu ghi nhận trên honeypot cảnh kẻ tấn công chạy lệnh shell, nhưng chính họ nói bằng chứng này không phản ánh độ tin cậy của exploit trên cấu hình khác (Rescana).

Với người vận hành, có hai điểm đáng chú ý. Khai thác diễn ra bên dưới tầng ứng dụng nên log ứng dụng có thể ghi lại rất ít ngữ cảnh (Recorded Future). CISA khuyến nghị theo dõi các tiến trình PHP worker khởi chạy shell hoặc tải công cụ về (Rescana).

Phòng thủ: vá PHP, rồi restart mọi worker

Biện pháp dứt điểm duy nhất là nâng PHP lên 8.5.12, 8.4.27, 8.3.36 hoặc 8.2.35, hoặc cài gói đã vá của distro hay nhà cung cấp. PHP nói không có biện pháp giảm nhẹ nào đầy đủ, cập nhật là lựa chọn an toàn duy nhất (Rescana). Các điểm dễ bị bỏ sót:

  • Restart worker: restart Apache, PHP-FPM và mọi worker chạy lâu để runtime đã vá được nạp. Nhóm nghiên cứu nhấn mạnh cập nhật ứng dụng không cập nhật runtime PHP bên dưới (Rescana).
  • Gói của distro: PHP cho biết các distro và nhà cung cấp LTS đã nhận bản vá trước, nên cần kiểm tra gói cập nhật từ nhà cung cấp của bạn (Rescana).
  • PHP đi kèm sản phẩm khác: CISA lưu ý PHP thường là thành phần của phần mềm khác nên khó nhìn thấy từ bên ngoài, vì vậy cần kiểm kê cả container, image CI và appliance (Rescana).
  • PHP 8.1 trở xuống: không có bản phát hành chính thức. PHP khuyên liên hệ nhà cung cấp LTS để có bản backport hoặc nâng lên bản được hỗ trợ. CISA khuyến nghị đưa các hệ thống PHP không thể cập nhật offline cho đến khi vá được (Rescana).

Khi chưa vá được, PHP nói các biện pháp tạm thời chỉ giảm tiếp xúc với một số đường tấn công chứ không làm hệ thống an toàn (Rescana):

Cấu hình Nhắm vào Lưu ý
memory_limit = -1, đặt ở cấp quản trị CVE-2026-109618 Chuyển việc xử lý cạn bộ nhớ cho hệ điều hành, worker có thể bị kết thúc và làm gián đoạn request. PHP cảnh báo không thay bằng RLIMIT_AS hoặc RLIMIT_DATA.
session.upload_progress.enabled = 0 CVE-2026-109619 Chặn các đường session đã xác nhận nhưng không chặn các biến thể khác. Đặt trong php.ini hoặc cấu hình FPM pool, không dùng ini_set() hay .user.ini, rồi restart worker.
fatal_error_backtraces = 0 (PHP 8.5) CVE-2026-109619 Áp dụng cùng cách đặt và restart như dòng trên.

Bài học cho người vận hành hạ tầng

PHP2Shell đáng học vì cho thấy vòng đời của một CVE nghiêm trọng hiện nay ngắn đến mức nào. Wiz báo cáo lỗi cho PHP từ 24/8, nhưng thông tin vẫn bị rò rỉ trước ngày vá (Simon Willison).

  1. Bảo mật ứng dụng không che được lỗi ở runtime. Website vẫn có thể bị ảnh hưởng dù mã ứng dụng an toàn (Recorded Future).
  2. Cửa sổ giữa công bố và khai thác gần như biến mất. Thông tin rò rỉ chiều 20/10 và PoC lưu hành trong ngày 21/10 (Simon Willison). Amazon ghi nhận một nỗ lực khai thác từ 17/10 và khuyên điều tra từ ngày đó chứ không phải từ ngày công bố (Rescana).
  3. Phải biết PHP chạy ở đâu trước khi có sự cố. CISA khuyến nghị kiểm kê mọi hệ thống chạy PHP, vì PHP thường nằm trong phần mềm khác (Rescana).
  4. Kiểm tra bằng số phiên bản chưa đủ. Khoảng bốn phần năm IP chạy PHP không công bố phiên bản, nên quét theo chuỗi phiên bản bỏ sót phần lớn bề mặt phơi nhiễm. VirusTotal còn đếm khoảng 85.000 IP đang chạy bản phát hành 24/9, bản mới nhất trước khi có bản vá (Recorded Future).
  5. Nhánh hết hỗ trợ là nợ kỹ thuật có hạn. PHP 8.2 hết vòng đời vào 31/12/2026 (Rescana), tức khoảng hai tháng nữa.

Nguồn

Các trang đã mở trực tiếp để lấy số liệu và ngày tháng:

Tài liệu gốc mà các bài trên trích dẫn chưa được mở trực tiếp khi soạn bài, nên cần đối chiếu trước khi trích dẫn lại: