Dekkapok.Engineering

● 2026-10-21

PHP2Shell: เมื่อโค้ดยังไม่ทันรัน แต่โดนยึดเครื่องไปแล้ว (ฝันร้ายระดับ Core ของชาว PHP)

#php #php2shell #security #devops #rce

PHP2Shell: เมื่อโค้ดยังไม่ทันรัน แต่โดนยึดเครื่องไปแล้ว (ฝันร้ายระดับ Core ของชาว PHP)
FILE: php2shell_rce_analysis.md
SIZE: 21.5 KB
SHA256: 0x4bf43138
CHARS: 12700
WORDS: 1102
READ TIME: 32 min

1. “Security Review ผ่านฉลุย… แต่ตัว Runtime บอก ‘ขอยืมเครื่องแป๊บ’”

ในฐานะ Software Engineer เราถูกพร่ำสอนให้ยึดมั่นในคัมภีร์ความปลอดภัย:

  • ทำ Parameterized Query ทุกจุด ลาก่อน SQL Injection
  • ใส่ Sanitize / Escape ทุก Output ชาตินี้ไม่มีทางเจอ XSS
  • แปะ Middleware ตรวจสอบ Token / JWT แน่นหนาจนมดสักตัวก็ห้ามผ่าน

เราเชื่อมั่นเสมอว่า flow การทำงานของจักรวาลคือ:

flowchart LR
    A[HTTP Request] --> B[Reverse Proxy]
    B --> C[Auth Middleware]
    C --> D[Controller]

แต่ในความเป็นจริงของโลก PHP มันมีด่านลับที่เกิดขึ้นก่อนหน้านั้นเสมอ:

flowchart TD
    CLIENT["Untrusted HTTP Client (แฮกเกอร์)"] --> NGINX["Web Server / Nginx"]
    NGINX --> SAPI["PHP SAPI Layer (PHP-FPM / mod_php)"]
    SAPI --> PARSER["Native Multipart Parser & Session Logic (ภาษา C)"]
    PARSER -->|"💥 PHP2Shell บึ้มตรงนี้! (RCE สำเร็จ)"| SHELL["ยึด Server / Reverse Shell"]
    PARSER -.->|"ยังเดินทางมาไม่ถึงด้วยซ้ำ"| BOOTSTRAP["PHP Engine Bootstrap (index.php)"]
    BOOTSTRAP -.-> AUTH["Middleware: auth()->check()"]
    AUTH -.-> APP["Clean Architecture / Controllers"]

    style PARSER fill:#ff4d4f,stroke:#333,stroke-width:2px,color:#fff
    style SHELL fill:#d9363e,stroke:#333,stroke-width:2px,color:#fff
    style AUTH fill:#52c41a,stroke:#333,stroke-width:2px,color:#fff

คุณยืนเฝ้าประตูห้องรับแขกด้วยชุดเกราะเหล็กกล้า ตรวจบัตรประชาชนทุกคนอย่างเข้มงวด… แต่แฮกเกอร์ระเบิดกำแพงห้องครัวทะลุเข้ามาเปิดตู้เซฟไปเรียบร้อยแล้วตั้งแต่ก่อนจะเดินเข้าประตูบ้าน!

เมื่อ HTTP Request วิ่งเข้ามาถึง PHP ตัวระบบจะต้องทำหน้าที่ Parse ข้อมูลดิบเพื่อเตรียมตัวแปร Superglobals ($_POST, $_FILES, $_SERVER) ช่องโหว่นี้เกิดขึ้นที่เลเยอร์ C Runtime ดังนั้นต่อให้คุณมี auth()->check() หรือเขียนครอบ try-catch คลุมจักรวาลไว้ โค้ดพวกนั้นก็ไม่เคยถูกเรียกเลยแม้แต่ไบต์เดียว

2. เกิดอะไรขึ้นข้างใน: เมื่อ Out of Memory กลายเป็นประตูสู่ RCE

ถ้าอธิบายแบบไม่ต้องกางโค้ด C 10,000 บรรทัดมานั่งปวดหัว ปัญหาหลักมาจาก 2 ช่องโหว่ที่จับมือกันแกล้ง Developer:

1) CVE-2026-109618: แรมเต็มปุ๊บ สติหลุดปั๊บ (OOM Memory Corruption)

เมื่อมีคนส่ง multipart/form-data ที่ออกแบบโครงสร้างมาแปลกประหลาดจนทำให้หน่วยความจำชนเพดาน memory_limit:

  • แทนที่ PHP จะคืนหน่วยความจำอย่างสุภาพ มันดันสะดุดล้ม (zend_bailout) แล้วกระโดดข้ามขั้นตอน Cleanup ขยะทิ้งไปดื้อ ๆ
  • ตัวชี้ในหน่วยความจำกลายเป็น Dangling Pointer (Use-After-Free) ค้างเติ่งอยู่ในระบบ
  • ซ้ำร้ายกว่านั้น ระบบติดตามไฟล์อัปโหลด (session.upload_progress) ดันนำเศษหน่วยความจำของ Worker ตัวเก่าส่งกลับไปใน Cookie PHPSESSID ประหนึ่งเป็น Heartbleed เวอร์ชัน PHP!

2) CVE-2026-109619: ซากขยะค้างคืนใน OPcache (Stale Global State)

  • บริการอย่าง PHP-FPM หรือ FrankenPHP ทำงานเร็วเพราะใช้ Worker ตัวเดิมซ้ำ ๆ รับคำขอของคนต่อไป
  • ทว่าความผิดพลาดจาก Fatal Error ในรอบแรกทิ้ง “สถานะผี” ค้างไว้ในโครงสร้างส่วนกลางของ OPcache
  • พอ Request ใหม่ถูกส่งเข้ามา PHP ดันสับสนชนิดข้อมูล (Type Confusion) เปิดโอกาสให้แฮกเกอร์ควบคุม Instruction Pointer ชี้เป้าไปรันคำสั่งอะไรก็ได้ตามใจชอบ

3. 🕵️‍♂️ ผ่าตัดโค้ด C: ดู Patch จริงของ PHP ที่ใช้หยุด RCE (Commit 01a40e89fb)

ถ้าคุณสงสัยว่าแฮกเกอร์เปลี่ยนแค่ “แรมเต็ม” ให้กลายเป็น “ยึดเซิร์ฟเวอร์” ได้ยังไง ลองมาดูโค้ดภาษา C ต้นฉบับจาก Zend Engine กันครับ

ในโครงสร้างเก่า (ก่อนแพตช์) PHP ใช้คำสั่ง emalloc() เพื่อจองหน่วยความจำ แต่ฟังก์ชันนี้มีพฤติกรรมโหดร้ายคือ ถ้าจอง RAM ไม่ได้ มันจะกระโดดข้ามโค้ด (longjmp) เพื่อตัดจบการทำงานทันที

❌ โค้ดก่อนแก้ (Vulnerable Flow)

/* main/rfc1867.c และ ext/session/session.c (จำลองพฤติกรรมก่อนแก้) */

// 1. ระบบพยายามจอง Memory ขนาดใหญ่เพื่อรับข้อมูลที่แฮกเกอร์ส่งมา
progress_array = emalloc(size); 
// 🚨 จุดสลบ: ถ้า memory_limit เต็ม emalloc() จะเรียก zend_bailout() 
// ซึ่งจะทำการกระโดดหนีออกจากฟังก์ชันนี้ไปเลย!

// ---------------------------------------------------------
// ❌ โค้ดโซนด้านล่างนี้ "ไม่เคย" ได้ทำงานเลยถ้าเกิด zend_bailout()
// ---------------------------------------------------------

if (PS(id)) {
    zend_string_release(PS(id)); // คืนหน่วยความจำของ Session ID
    PS(id) = NULL;               // รีเซ็ต Pointer
}

เกิด RCE ได้อย่างไร? เมื่อโค้ดบรรทัด PS(id) = NULL; ไม่ถูกรัน ตัวแปร PS(id) จะยังคงชี้ไปยังตำแหน่ง Memory เดิมที่กลายเป็นขยะไปแล้ว (Dangling Pointer) พอ PHP-FPM เอา Worker ตัวนี้ไปรับ Request ถัดไปของแฮกเกอร์ แฮกเกอร์ก็แค่ส่ง Payload เข้ามาเสียบในช่องว่างนั้น ตัว PHP Engine จะตีความข้อมูลนั้นผิดพลาด (Type Confusion) และถูกบิดเบือนทิศทางการรันคำสั่ง นำไปสู่การเกิด Remote Code Execution ทันที

✅ โค้ดที่แพตช์แล้ว (Commit 01a40e89fb)

ทีมพัฒนา PHP แก้ไขโดยรื้อระบบตัดจบนี้ใหม่ เปลี่ยนมาใช้ Fallible Allocation (ถ้าจองไม่ผ่าน ให้ส่งค่า NULL กลับมา ไม่ใช่กระโดดหนี) และเพิ่ม UnwindExit:

/* หลังแก้: เปลี่ยนมาจัดการ State ให้สมบูรณ์ก่อนตาย */

// 1. ใช้ฟังก์ชันที่ "ไม่" กระโดดหนีเมื่อ RAM เต็ม
void *tmp_ptr = zend_safe_femalloc(nmemb, size, offset);

if (UNEXPECTED(tmp_ptr == NULL)) {
    // 2. ถ้าระบบจอง RAM ไม่ผ่าน จะวิ่งลงมาเคลียร์ขยะให้เรียบร้อย!
    goto oom_cleanup; 
}

progress_array = tmp_ptr;

// ... (ทำงานปกติต่อไป) ...

oom_cleanup:
    // ✅ ทำการคืนค่า Pointer และ Reset Global State ปิดประตู UAF ถาวร
    clean_partial_state();
    reset_globals_properly(); // มั่นใจได้ว่า Worker ตัวนี้จะกลับมาคลีน 100%
    return FAILURE;

บทเรียนราคาแพง: การ Error แล้วให้ระบบพังไปเลยแบบดื้อ ๆ ในภาษา C (การทำ Non-local exit ข้าม Scope) ทิ้งซากขยะใน Memory ที่กลายมาเป็นช่องโหว่ RCE ระดับพันล้านได้!


4. ใครโดนผลกระทบบ้าง? (SAPI Impact Matrix)

สภาพแวดล้อม / SAPIระดับความเสี่ยงความรู้สึกของ Developer / SRE
Apache (mod_php)วิกฤตสูงสุด (Extreme)โค้ด PHP กับ Web Server อยู่ห้องเดียวกัน แฮกเกอร์ยิง Request เดียวติด RCE ได้ง่ายดายที่สุด
PHP-FPM (Nginx)วิกฤต (Critical)มาตรฐานของเว็บ 90% ทั่วโลก Worker reuse ทำให้เกิดบั๊กสถานะค้างข้ามคำขอได้อย่างสมบูรณ์แบบ
FrankenPHPวิกฤต (Critical)เน้น Worker Mode โหลดโค้ดค้างในแรมเพื่อความแรง ยิ่งเปิดทางให้หน่วยความจำที่เสียหายวนกลับมาทำงาน
LiteSpeed (LSPHP)วิกฤต (Critical)ได้รับผลกระทบเช่นเดียวกันเมื่อประมวลผลคำขอ Multipart
CLI (Command Line)ต่ำ (Low)รันสคริปต์เดี่ยวใน Terminal รอดตัวไป เพราะไม่มีใครส่ง HTTP multipart เข้า Terminal ตรง ๆ ได้

5. แผนปฏิบัติการเร่งด่วน: แก้ไขให้ถูกจุด (ก่อนโดน On-Call)

สเต็ปที่ 1: ตรวจสอบและอัปเดต PHP Engine ทันที

อย่าดูแค่ php -v ในเครื่องแล้วสบายใจ เพราะบางที CLI อัปเดตแล้ว แต่ PHP-FPM ของ Web Server ยังรันไบนารีตัวเดิมอยู่! ต้องอัปเดตเป็นเวอร์ชันทางการเหล่านี้เท่านั้น:

สาขาหลักเวอร์ชันที่ปลอดภัย (Fixed Versions)
PHP 8.58.5.12
PHP 8.48.4.27
PHP 8.38.3.36
PHP 8.28.2.35
PHP 7.4 / 8.0 / 8.1 (EOL)ไม่มี official binary release ต้องใช้ source patch หรือ Extended Repo จาก Linux Distro เท่านั้น

สเต็ปที่ 2: สั่ง Restart Service (อันนี้สำคัญมาก!)

แพตช์เสร็จแล้ว ต้องเตะ Worker เก่าทิ้งออกจาก RAM ทันที:

# ตัวอย่างสำหรับ Ubuntu / Debian
sudo apt update && sudo apt --only-upgrade install php8.3-fpm
sudo systemctl restart php8.3-fpm
sudo systemctl reload nginx

6. แผนซื้อเวลา: ถ้าเซิร์ฟเวอร์ยังอัปเดตทันทีไม่ได้ ทำอย่างไร?

หากบริษัทของคุณติดระเบียบ Change Request อันเข้มงวด หรือต้องรอ Maintenance Window ตอนตีสาม นี่คือมาตรการบรรเทาชั่วคราว:

  1. บล็อก Multipart ที่ Nginx ก่อนส่งถึง PHP: ในเมื่อเวกเตอร์หลักมาจากการแกะฟอร์มอัปโหลดไฟล์ ให้ Nginx ปฏิเสธคำขอนั้นทิ้งไปก่อนเลย:

    # วางใน block server ของ Nginx ก่อน fastcgi_pass
    if ($http_content_type ~* "multipart/form-data") {
        return 403;
    }

    (หมายเหตุ: ฟังก์ชันอัปโหลดไฟล์ในเว็บจะใช้ไม่ได้ชั่วคราว แต่ดีกว่าโดนขโมยเซิร์ฟเวอร์)

  2. ปิด Upload Progress ใน php.ini:

    session.upload_progress.enabled = 0
  3. อย่าหลงเชื่อคำสั่ง artisan down หรือโหมด Maintenance ของ Framework: การเปิด Maintenance Mode ในระดับแอปพลิเคชัน ไม่ช่วยอะไรทั้งสิ้น เพราะตัว Parser ภาษา C ของ PHP ได้รับคำขอและเกิด Memory Corruption พังไปก่อนที่ Framework จะได้โหลดไฟล์เช็กสถานะบำรุงรักษาด้วยซ้ำ! ถ้าจะปิดปรับปรุง ต้องปิดด้วย Static HTML ที่ระดับ Reverse Proxy / Cloudflare เท่านั้น

บทสรุปแด่นักพัฒนาทุกคน

  • Clean Architecture — แยก Layer สวยจน Uncle Bob น้ำตาไหล
  • Unit Test — เขียวทั้งแถบ Coverage 95% อีก 5% ฝากไว้กับสิ่งศักดิ์สิทธิ์
  • Type Safety — TypeScript strict mode, any เป็นอาชญากรรม
  • Security Audit — ผ่านหมด OWASP Top 10 ยืนปรบมือ
  • CI/CD — 47 Checks Passed, GitHub Actions เขียวจนแสบตา
  • Docker — Non-root, Read-only filesystem, Drop all capabilities
  • Code Review — Senior 3 คน Approve พร้อมคอมเมนต์ LGTM 🚀

“คุณใช้เวลา 6 เดือนออกแบบ Architecture เพื่อไม่ให้ใครเข้าถึง Business Logic โดยไม่ได้รับอนุญาต

แต่ Hacker ไม่ได้พยายามเข้าผ่าน Business Logic ตั้งแต่แรก”

เพราะ Clean Architecture ช่วยจัดระเบียบโค้ดของคุณ

ไม่ได้รับประกันว่า Library ทุกตัวที่โค้ดคุณเรียกใช้จะปลอดภัย

และที่เจ็บที่สุดคือ…

คุณเขียน Unit Test ครอบคลุมโค้ดตัวเอง 95%

แต่ช่องโหว่อยู่ในโค้ดที่คุณไม่เคยเขียน และไม่เคยทดสอบเองด้วยซ้ำ 🗿

git commit -m "fix: apparently dependencies are part of the application"

Technical Debt ไม่น่ากลัวเท่า Technical Inheritance — มรดกโค้ด C อายุ 30 ปี ที่ยังมีสิทธิ์ทำ Production คุณพังได้ในปี 2026

นี่คือรายการแหล่งข้อมูลอ้างอิง (References) ทั้งหมดจากการวิจัยและวิเคราะห์ช่องโหว่ PHP2Shell (CVE-2026-109618 และ CVE-2026-109619) ที่ใช้ประกอบการเขียนบทความครับ คุณสามารถนำไปแนบไว้ท้ายบล็อกได้เลย:

แหล่งข้อมูลอ้างอิง (References):

  1. Wikipedia: PHP2Shell

  2. Cyber Security News: PHP Releases Emergency Patches

  3. Tenable: FAQ About Critical PHP Remote Code Execution

  4. Sansec Research: PHP2Shell Vulnerability Analysis

  5. The Next Web: PHP2Shell, OpenAI, and Wiz Research

  6. Huntress: Leaked Critical PHP Vulnerabilities

  7. Imperva: PHP2Shell Vulnerability Updates

  8. Clarin: PHP Vulnerability 2026 Explained

  9. Reddit (r/selfhosted): PHP2Shell in Self-Hosted Apps

  10. The Record: PHP Script Vulnerabilities Warnings

  11. Palo Alto Unit 42: CVE-2026-109618 & CVE-2026-109619 Analysis

  12. OX Security: Two Vulnerabilities Allow Unauthenticated RCE in PHP

  13. Google Cloud Blog: Responding to Core PHP Vulnerabilities

  14. AlphaSignal: OpenAI’s PHP2Shell Exposes 70% of Websites

  15. Help Net Security: PHP2Shell Vulnerabilities Patched

  16. Brandefense: PHP RCE CVE-2026-109618 & 109619

  17. HackRead: PHP Releases Emergency Fixes for PHP2Shell

  18. Cloudflare Blog: PHP Vulnerabilities WAF Protection

  19. Precursor Security: Critical PHP RCE Vulnerabilities

  20. CERT-EU: Security Advisory 2026-018

  21. The Hacker News: Two Unpatched Critical PHP Vulnerabilities

  22. AlmaLinux Blog: PHP2Shell CVE-2026-109618 & CVE-2026-109619

  23. Ubuntu Security: CVE-2026-109618

  24. Wiz Research Blog: Wiz Blog

SYSTEM LOG: TERMINAL CURL ACCESS ONLINE

Retrieve the raw markdown file or structured JSON of this post directly in your command line:

curl -sL https://dekkapok.engineering/blogs/php2shell_rce_analysis.txt
curl -sL https://dekkapok.engineering/blogs/php2shell_rce_analysis.json

← blogs ทั้งหมด