PHP2Shell: Vá Lỗi Chưa Đủ Nếu Không Khởi Động Lại Worker

Phân tích PHP2Shell (CVE-2026-109618/109619): lỗi OOM và trạng thái toàn cục cũ trong worker PHP, bản vá 8.5.12/8.4.27/8.3.36/8.2.35 và cách kiểm tra.

PHP2Shell: Vá Lỗi Chưa Đủ Nếu Không Khởi Động Lại Worker
Trong bài viết này

PHP2Shell là cặp lỗ hổng thực thi mã từ xa không cần xác thực (pre-auth RCE) nằm trong Zend Engine của PHP. Bài học lớn nhất của nó mang tính kiến trúc. Lỗi nằm ở vòng đời request bên dưới ứng dụng, nên việc vá chỉ thật sự xong khi mọi worker PHP đang chạy trong bộ nhớ đã được thay bằng tiến trình mới.

Chuyện gì đã xảy ra và diễn biến nhanh ra sao

Hai mã định danh gồm:

  • CVE-2026-109618: lỗi an toàn bộ nhớ khi xử lý tình huống hết bộ nhớ (Out-of-Memory, OOM).
  • CVE-2026-109619: lỗi use-after-free (dùng lại vùng nhớ đã giải phóng) do trạng thái toàn cục cũ.

Cả hai đạt 9.2 (Critical) theo CVSS v4.0. Theo CVSS v3.1, điểm lần lượt là 9.8 và 8.1.

Nhóm nghiên cứu từ Calif.io, OpenAI và Wiz báo cáo lỗi ngày 1/8/2026, có dùng mô hình AI hỗ trợ kiểm thử tự động. Sau đó, các mốc thời gian dồn lại rất nhanh:

  • 17/10: mạng honeypot MadPot của Amazon ghi nhận các nỗ lực khai thác đi qua mạng ẩn danh. Khi đó chưa có bản vá nào.
  • 21/10, 14:00 UTC: PHP Group phát hành bản vá khẩn cấp.
  • 26/10: CISA đưa CVE-2026-109618 vào danh mục Known Exploited Vulnerabilities (KEV) sau khi ghi nhận hoạt động quét và tấn công diện rộng.

Điểm đáng lưu tâm nhất là việc khai thác diễn ra trước khi lỗ hổng được công bố. Ngay ngày bản vá ra mắt, phe phòng thủ đã đi sau một bước.

Nguyên nhân gốc: mã dọn dẹp tin vào một request còn dựng dở

Cận cảnh thanh bộ nhớ máy tính, minh họa cách tiến trình worker PHP giữ trạng thái trong RAM giữa các request

Cả hai lỗi kích hoạt trước khi code ứng dụng được nạp. Vì vậy WordPress, Laravel hay lớp kiểm tra input tự viết đều không có cơ hội can thiệp.

Thoát khỏi OOM bằng longjmp (CVE-2026-109618)

Khi phân tích dữ liệu đầu vào như multipart form data, bộ cấp phát của Zend (zend_alloc) đối chiếu các lần cấp phát với giới hạn memory_limit. Khi chạm ngưỡng, PHP không tháo stack tuần tự từng bước. Nó gọi hàm longjmp() của C runtime để nhảy ra khỏi ngữ cảnh hiện tại và hủy request.

Một cú nhảy phi cục bộ (non-local jump) bỏ qua mọi đoạn khởi tạo chưa kịp chạy. Hệ quả là một số cấu trúc nội bộ của request bị bỏ lại ở trạng thái khởi tạo dở dang. Giai đoạn kết thúc request (RSHUTDOWN) sau đó lại mặc định rằng các con trỏ và đối tượng ấy hợp lệ. Việc giải phóng hoặc duyệt qua con trỏ chưa khởi tạo sẽ làm hỏng vùng heap.

Bài học chung ở đây là mọi thủ tục dọn dẹp chạy sau một lần hủy đột ngột đều phải coi trạng thái là có thể chưa hoàn chỉnh.

Trạng thái toàn cục sống lâu hơn request (CVE-2026-109619)

Một số tính năng của PHP giữ con trỏ toàn cục tạm thời trỏ vào vùng nhớ thuộc về từng request. Ví dụ được nêu là tính năng theo dõi tiến trình upload (session.upload_progress). Khi request kết thúc bất thường, con trỏ toàn cục này có thể không được reset, dù vùng nhớ nó trỏ tới đã được trả lại bằng efree.

Kết quả là một con trỏ lơ lửng (dangling pointer). Khi chính worker đó nhận request kế tiếp, trạng thái toàn cục cũ có thể bị đọc trước khi được khởi tạo lại. Đó chính là use-after-free.

Vì sao SAPI quyết định mức độ phơi nhiễm

Server API (SAPI) quyết định một tiến trình sống bao lâu. Điều đó cũng quyết định trạng thái lỗi có thể tồn tại trong bao lâu.

  • Apache mod_php: PHP chạy ngay trong tiến trình httpd, và đường dọn dẹp lỗi chạy vô điều kiện kể cả khi khởi tạo thất bại. Hỏng bộ nhớ có thể xảy ra trước khi ứng dụng được nạp, nên đây là cấu hình rủi ro cao nhất.
  • PHP-FPM: mỗi worker phục vụ nhiều request tuần tự, tới ngưỡng pm.max_requests. Nếu OOM không làm worker dừng hẳn, con trỏ lơ lửng của CVE-2026-109619 có thể tồn lưu sang request kế tiếp trên cùng PID.
  • Worker mode của FrankenPHP và LiteSpeed: tiến trình hoặc thread được giữ lâu dài để đổi lấy tốc độ, chấp nhận giảm tính cô lập. Tái sử dụng bộ nhớ qua nhiều chu kỳ giữ trạng thái heap ổn định hơn, nên các tham chiếu rác giữa các request càng đáng lo.

Nhận định đáng rút ra là tái sử dụng worker vốn là tính năng tối ưu hiệu năng. Thế nhưng nó lặng lẽ biến khâu dọn dẹp request thành một ranh giới bảo mật.

Vá đúng cách: thứ chạy trong RAM mới là thứ quyết định

Các bản đã khắc phục là PHP 8.5.12, 8.4.27, 8.3.36 và 8.2.35. Mọi nhánh từ 5.4 đến 8.5 trước các bản này đều bị ảnh hưởng. Với các nhánh đã hết vòng đời hỗ trợ (8.1, 8.0, 7.x), nhóm PHP chỉ phát hành bản vá mã nguồn, không kèm binary đóng gói sẵn. Đội đang dùng các nhánh này phải tự build hoặc trông vào nhà phân phối.

Hãy nâng cấp qua trình quản lý gói của hệ điều hành, rồi khởi động lại dịch vụ:

sudo apt-get update && sudo apt-get --only-upgrade install php8.3-fpm php8.3-cli php8.3-common
sudo systemctl restart php8.3-fpm   # hoặc php-fpm / httpd / frankenphp

Sau đó hãy kiểm tra tiến trình chứ đừng chỉ kiểm tra CLI. Lệnh php -v chỉ phản ánh binary trên đĩa, không cho biết gì về những worker đã chạy từ tuần trước. Hãy xác nhận rằng thời điểm khởi chạy của worker mới hơn lần nâng cấp:

ps -o pid,lstart,cmd -C php-fpm8.3   # tên tiến trình tùy bản phân phối

Nếu dùng shared hosting hay nền tảng managed, hãy hỏi nhà cung cấp xem worker đã được khởi động lại chưa. Biết gói đã được cập nhật là chưa đủ.

Giải pháp tạm thời và giám sát trong lúc kiểm thử

Nếu việc kiểm thử tương thích làm chậm quá trình nâng cấp, hai cấu hình sau giúp tấn công kém ổn định hơn:

session.upload_progress.enabled = 0
client_max_body_size 8M;

Cấu hình thứ nhất tắt bề mặt upload progress liên quan tới CVE-2026-109619. Cấu hình thứ hai cắt bỏ request body quá lớn trước khi chúng tới PHP. PHP Security Team nói rõ rằng cả hai đều không phải bản sửa lỗi. Nâng cấp runtime mới là cách phòng thủ hoàn chỉnh duy nhất.

Để phát hiện, hãy dùng công cụ giám sát runtime như các công cụ dựa trên eBPF hoặc Falco. Đặt cảnh báo khi worker php-fpm hay httpd sinh ra /bin/sh, /bin/bash hoặc python dưới user web, chẳng hạn www-data hay nobody. Hành vi này hiếm khi hợp lệ.

Bài học rộng hơn

PHP2Shell không phải lý do để từ bỏ worker chạy lâu dài. Nó là lý do để coi “đã vá” là thuộc tính của tiến trình đang chạy. Nó cũng là lý do để kiểm kê mọi nơi PHP đang hoạt động, kể cả container cũ và những máy mod_php bị lãng quên. Tấn công đã bắt đầu từ trước khi có bản vá, nên việc kiểm kê ấy cần nằm trong danh sách việc của tuần này.

Nguồn tham khảo

  1. en.wikipedia.org
  2. dynatrace.com
  3. aws.amazon.com
  4. nvd.nist.gov
  5. sara-open.sirp.io
  6. success.trendmicro.com

Có hỗ trợ AI · PKN duyệt lại

0

Phản hồi

Đang tải bình luận…