ในการทำ Exploit ช่องโหว่ CVE-2026-109618 เป้าหมายของผู้โจมตีไม่ใช่แค่การยิง DoS ให้ระบบล่ม แต่คือการบังคับให้เกิดภาวะ Out-Of-Memory (OOM) ในเสี้ยววินาทีและตำแหน่งที่เฉพาะเจาะจง (Surgically-timed Allocation) เพื่อขัดจังหวะการทำงานของ Zend Engine ขณะที่โครงสร้างข้อมูลภายใน (Internal Data Structure) ยังสร้างไม่เสร็จสมบูรณ์
กลไกการจัดการ Memory และ Exception ของ Zend Engine
ทำความเข้าใจ 2 กลไกสำคัญของ PHP Core ก่อนพิจารณาวิธีโจมตี
- Zend Memory Manager (ZMM)
PHP จัดการหน่วยความจำผ่าน Heap ของตัวเอง (emalloc(),efree()) โดยจะคอยนับยอดรวมเพื่อเทียบกับค่าmemory_limitในphp.ini - Bailout ผ่าน
setjmp()/longjmp()
เมื่อemalloc()ขอ RAM เกินโควตา ตัว Engine จะเรียกฟังก์ชันzend_error_noreturn(E_ERROR, "Allowed memory size...")ซึ่งภายในจะยิงคำสั่งlongjmp(EG(bailout), 1)ทันที- ผลคือ Execution Stack จะถูกตัดทิ้ง (Unwind) ย้อนกลับไปยังจุด
setjmp()นอกสุดของ Request Loop ทันที โดยไม่มีการรันโค้ดส่วนที่เหลือของฟังก์ชันที่กำลังทำงานอยู่
- ผลคือ Execution Stack จะถูกตัดทิ้ง (Unwind) ย้อนกลับไปยังจุด
ขั้นตอนการ Trigger OOM เชิงลึก (The Exploitation Mechanics)
กลไกที่ผู้โจมตีใช้บีบให้เกิด OOM ประกอบด้วย 3 ขั้นตอนหลัก
ขั้นที่ 1: Memory Grooming (Heap Shaping)
ผู้โจมตีต้องดันการใช้งานหน่วยความจำของ PHP Worker ตัวนั้นให้เฉียดขีดจำกัด memory_limit (เช่น ตั้งไว้ 128MB ต้องดันให้แตะ ~127.8MB) เพื่อให้คำสั่งจอง RAM ถัดไปชนเพดานทันที
- เทคนิคที่ใช้
- ส่ง Multipart Form-Data ที่มีขนาดใหญ่พอดี
- ส่ง Input ที่บีบให้ PHP สร้างอาเรย์ขนาดใหญ่ เช่น Nested JSON, XML Entity Expansion, หรือ Query String ซ้ำซ้อนหลายพันตัวแปร (
foo[]=1&foo[]=2&...)
ขั้นที่ 2: The Critical Allocation Window (จุดเปราะบาง)
ในระดับซอร์สโค้ดภาษา C การสร้าง Object หรือ Data Structure ที่ซับซ้อนมักต้องใช้การ Allocation หลายจังหวะต่อเนื่องกัน (Sequential Allocations)
C
/* ตัวอย่างโค้ดโครงสร้างที่มีช่องโหว่ */
struct zend_my_context *ctx = emalloc(sizeof(struct zend_my_context));
// [จังหวะที่ 1] ctx ถูกสร้าง แต่สมาชิกข้างใน (ctx->destructor_fn, ctx->data_ptr) ยังเป็น Uninitialized / ค่าขยะใน RAM
ctx->data_ptr = emalloc(user_controlled_size);
// [จังหวะที่ 2: The Trap!] ถ้า OOM เกิดขึ้นที่คำสั่งนี้!
// longjmp() จะกระโดดหนีทันที บรรทัดข้างล่างจะไม่ถูกรัน:
ctx->destructor_fn = my_cleanup_handler; // <--- โค้ดนี้ไม่ถูกรัน!
register_shutdown_context(ctx);
ขั้นที่ 3: บังคับให้ OOM ระเบิดตรงตำแหน่งจองลูก
เมื่อผู้โจมตีจัด Heap ให้เหลือช่องว่างขนาดแน่นอน (เช่น เหลือ 4KB ก่อนชน Limit) ผู้โจมตีจะยิง Request ที่บังคับให้
- การจองตัวแม่ (
ctx) ขนาดเล็ก (เช่น 64 bytes) ผ่านฉลุย - การจองตัวลูก (
ctx->data_ptr) ขอขนาด 8KB ซึ่งเกิน Limit ทันที emalloc()สั่งlongjmp()ตัดการทำงานย้อนกลับไปที่ Shutdown Handler ทันที ทิ้งให้ctxมีสมาชิกข้างในเป็น Garbage Data จากหน่วยความจำเดิม
จาก OOM สู่ Arbitrary Code Execution (RCE)
เมื่อ longjmp() กระโดดกลับมาที่ Loop ทำความสะอาด Request (เช่น php_request_shutdown())
- Uninitialized Pointer Traversal
รูทีน Cleanup จะวนลูปตรวจหา Object ที่ค้างอยู่ใน Request เพื่อสั่ง Free หน่วยความจำ - Corrupted Branching
เมื่อถึงคิวของctxฟังก์ชัน Cleanup จะพยายามเรียก Destructor Function Pointer หรืออ่าน Pointer ตำแหน่งถัดไป:Cif (ctx->destructor_fn) { ctx->destructor_fn(ctx->data_ptr); // กระโดดไปรัน Address ตามฟังก์ชันพอยน์เตอร์ } - Control Flow Hijacking
เนื่องจากctx->destructor_fnไม่เคยถูกกำหนดค่าให้ถูกต้อง มันจึงมีค่าเป็นข้อมูลขยะที่ค้างอยู่ใน Heap - ผู้โจมตีที่ทำ Heap Spraying สำเร็จตั้งแต่ ขั้นที่ 1 จะสามารถกำหนดให้ค่าขยะใน RAM ตรงตำแหน่งนั้นตรงกับคำสั่งใน Memory (เช่น ชี้ไปที่ฟังก์ชัน
system()หรือ ROP Gadget) ทำให้ CPU หักเหทิศทางไปรันคำสั่งอันตรายของผู้โจมตีทันที
สรุปจุดตายของสถาปัตยกรรม
ช่องโหว่นี้ไม่ได้เกิดจาก Memory Leak ทั่วไป แต่เกิดจาก Structural Asymmetry
- โค้ดส่วนหนึ่งเชื่อว่าถ้าเกิด OOM ตัว Engine จะพาลำดับงานออกไปคลีนอย่างปลอดภัย
- แต่ระบบ Cleanup กลับเชื่อว่าทุก Pointer ที่ถูกสร้างขึ้นมา จะต้องได้รับการ Initialize โครงสร้างครบถ้วนแล้วเสมอ
เมื่อการสั่ง longjmp() เกิดขึ้นคั่นกลางระหว่างความเชื่อทั้งสองฝั่ง จึงเกิดสถานะ Use of Uninitialized Pointer / Type Confusion ที่นำไปสู่ RCE ในที่สุด