PlusMagi's Blog By Pitt Phunsanit Uncategorized เจาะลึก CVE-2026-109619: ช่องโหว่ Stale Memory State สู่ Cross-Request RCE บน PHP-FPM

เจาะลึก CVE-2026-109619: ช่องโหว่ Stale Memory State สู่ Cross-Request RCE บน PHP-FPM

ควบคู่กับ CVE-2026-109618 ทาง PHP Group และฐานข้อมูลช่องโหว่ความปลอดภัยแห่งชาติสหรัฐฯ (NVD) ได้เปิดเผยช่องโหว่ความปลอดภัยระดับร้ายแรงสูงอีกหนึ่งตัวคือ CVE-2026-109619 ช่องโหว่นี้เกิดขึ้นที่เลเยอร์จัดการสถานะของกระบวนการประมวลผล (Process State Management) ภายใน Zend Engine ซึ่งทำให้เกิดการรั่วไหลของข้อมูลสถานะข้ามรอบการทำงาน (Cross-Request State Contamination) จนนำไปสู่การควบคุมทิศทางคำสั่งระบบ (Remote Code Execution – RCE)


ต้นตอของปัญหา (Root Cause Analysis)

โดยสถาปัตยกรรมปกติของ PHP-FPM (FastCGI Process Manager) ตัว Master Process จะ Fork ตัว Worker Processes ขึ้นมาเป็นจำนวนหนึ่งเพื่อคอยรับคำขอ HTTP และหมุนเวียนนำ Worker เดิมกลับมาใช้ซ้ำ (Process Reuse) เพื่อประสิทธิภาพสูงสุด

ในแต่ละรอบคำขอ (Request Cycle) PHP มีวงจรชีวิตชัดเจน

  1. php_request_startup(): จัดเตรียม Global Variables, สภาพแวดล้อม และ Zend Memory Manager (ZMM)
  2. php_execute_script(): รันสคริปต์ PHP
  3. php_request_shutdown(): คืนทรัพยากรหน่วยความจำของรอบนั้น และรีเซ็ตสถานะกลับสู่ค่าเริ่มต้น

ปัญหาของ CVE-2026-109619 เกิดจากความบกพร่องในขั้นตอน php_request_shutdown()

  • Stale Global Pointer
    ข้อมูลโครงสร้างระดับลึก (Internal Subsystem State) ในส่วนขยายหรือตัว Core Engine บางตัว เช่น การแคชโครงสร้าง Abstract Syntax Tree (AST), Hash Tables หรือสตรีมคอนเท็กซ์ มีการคืนหน่วยความจำ (Free) ตัวอ็อบเจกต์ไปแล้ว แต่ ไม่ได้เซ็ต Pointer ที่อยู่ในหน่วยความจำระดับ Global/Process ให้เป็น NULL
  • Dangling Global Reference
    เมื่อ Worker นั้นจบการทำงานของ Request แรก Global Struct ยังคงถือ Dangling Pointer ที่ชี้ไปยังตำแหน่งหน่วยความจำเดิมใน Heap ที่ถูกปล่อยว่างไปแล้ว
  • Cross-Request Confusion
    เมื่อมีคำขอใหม่ (Request ที่สอง) วิ่งเข้ามาหา Worker ตัวเดิม ตัวประมวลผลเข้าใจผิดว่าโครงสร้างระบบนั้นถูกเตรียมไว้แล้ว จึงนำ Dangling Pointer เก่าไปใช้งานต่อทันทีโดยไม่ผ่านการจัดสรรใหม่

ลำดับการโจมตี (Exploitation Flow)

ผู้โจมตีอาศัยลักษณะการทำงานวนซ้ำของ Worker Process ในการสร้างสภาวะ Use-After-Free ข้าม Request

[Request 1: Priming Request]
ผู้โจมตีส่ง Payload บีบให้เกิด Partial Failure ในจังหวะ Shutdown
  │
  ▼
ทรัพยากรบางส่วนถูก Free แต่ Global Pointer ยังค้างอยู่ (Stale State)

[Request 2: Grooming & Hijacking Request]
ผู้โจมตีส่ง Request ถัดไปเข้ามายัง Worker Process ตัวเดิม
  │
  ├─ จัดวาง Memory (Heap Spraying) ให้ทับตำแหน่งที่เพิ่งถูก Free ไป
  │
  ▼
PHP นำ Global Pointer เดิมมาเรียกใช้งาน ฟังก์ชันพอยน์เตอร์ถูกชี้ไปยัง Shellcode
  │
  ▼
Remote Code Execution (RCE) สำเร็จ

เนื่องจากช่องโหว่นี้ผูกติดกับวงจรชีวิตของโปรเซส Worker ทำให้ระบบที่ใช้ PHP-FPM แบบดั้งเดิมโดยไม่มีการรีไซเคิล Worker มีความเสี่ยงต่อการถูกทำ Exploit สูงมาก


เวอร์ชันที่ได้รับผลกระทบ

  • PHP 8.2.x
    ต่ำกว่ารุ่น 8.2.35
  • PHP 8.3.x
    ต่ำกว่ารุ่น 8.3.36
  • PHP 8.4.x
    ต่ำกว่ารุ่น 8.4.27
  • PHP 8.5.x
    ต่ำกว่ารุ่น 8.5.12

แนวทางแก้ไขและลดผลกระทบ


อัปเกรดแพ็กเกจทันที

อัปเกรด PHP Core และโมดูลทั้งหมดเป็นเวอร์ชัน 8.5.12 (หรือเวอร์ชันแพตช์ล่าสุดของสาย 8.2, 8.3, 8.4) ซึ่งคอมไพล์โค้ดที่ทำ Explicit Nullification ให้กับ Global Contexts ในขั้นตอน php_request_shutdown()

Bash

sudo apt update && sudo apt --only-upgrade install "php8.5*"
sudo systemctl restart php8.5-fpm

มาตรการบรรเทาความเสียหายในระดับคอนฟิก (Mitigation)

หากยังไม่สามารถอัปเกรดไบนารีได้ทันที ให้ตั้งค่าตัดวงจรชีวิตของ Worker ไม่ให้มีอายุยาวพอที่จะถูกทำ Cross-Request Exploitation:

ใน /etc/php/8.5/fpm/pool.d/www.conf

Ini, TOML

; สั่งทำลาย Worker และสร้างใหม่บ่อยขึ้น
pm.max_requests = 100

; กำหนด Process Management ให้ยืดหยุ่นเพื่อลดอายุค้างของ Worker
pm = dynamic

บันทึกไฟล์แล้วสั่ง reload:

Bash

sudo systemctl reload php8.5-fpm

การลดค่า pm.max_requests จะบังคับให้ OS เคลียร์ Address Space ทั้งหมดของ Worker Process นั้นทิ้งอย่างสมบูรณ์หลังทำงานครบจำนวนครั้งที่กำหนด ช่วยลดหน้าต่างเวลาที่ผู้โจมตีจะส่ง Request ที่สองเข้ามาทับตำแหน่ง Memory เดิมได้


Exit mobile version