PHP2Shell แพตช์ออกแล้ว เวอร์ชันที่แก้ สาเหตุของบั๊กทั้งสองตัว และค่าที่ตั้งไว้ระหว่างรออัปเดต

PHP 8.5.12, 8.4.27, 8.3.36 และ 8.2.35 แก้ช่องโหว่ RCE สองตัวแล้ว โพสต์นี้สรุปสิ่งที่ประกาศจริงบอก ทั้งเวอร์ชันบน Ubuntu และ ondrej PPA สาเหตุของบั๊ก ใครเสี่ยงที่สุด ค่า config ที่ php.net แนะนำระหว่างรอ และเรื่องข่าวหลุดก่อนกำหนด

เผยแพร่
เวลาอ่าน
8 นาที
สรุปจาก
Yannick Lyn Fatt · thephp.foundation

ผมเขียนโพสต์ก่อนหน้าไว้ตอนบ่ายวันที่ 21 ก่อนแพตช์ออก ตอนนั้นยังมีแต่ข่าวที่หลุดมา โพสต์นี้เป็นภาคต่อ สรุปจากประกาศของ php.net advisory บน GitHub ทั้งสองตัว และโพสต์ของ PHP Foundation ที่เล่าว่าเกิดอะไรขึ้นเบื้องหลัง

เวอร์ชันที่แก้แล้ว และ PHP ที่ลงจากแต่ละช่องทาง

branch เวอร์ชันที่แก้แล้ว
8.5 8.5.12
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35
8.1 ลงไป PHP ไม่ออก release ให้ แต่มี source backport ของ 7.4, 8.0 และ 8.1 ไว้ให้คน build เอง

เวอร์ชันชุดนี้ตรงกับที่ข่าวหลุดบอกไว้ก่อน ส่วนที่ข่าวพลาดคือคะแนน ข่าวว่า CVSS 10.0 แต่ของจริงคือ 9.2 ทั้งสองตัว และเรื่องที่ว่า 8.1 ลงไปจะไม่ได้อะไรเลย ซึ่งไม่จริงทั้งหมด เพราะ PHP ทำ backport ระดับ source ไว้ให้ และ distro บางตัวก็ backport ให้เองด้วย

PHP ที่ลงจากคนละช่องทางจะได้แพตช์คนละแบบ

  • ondrej PPA ตอนผมเช็ค php8.4 ขึ้นเป็น 8.4.27 แล้วทั้งบน Ubuntu 24.04 และ 22.04 เลขเวอร์ชันตาม upstream ดูจาก php -v ได้เลย
  • PHP ของ Ubuntu เอง ไม่ขยับเลขตาม upstream แต่ backport แพตช์ลงเวอร์ชันเดิม ตาม USN-8948-1 ต้องได้ 8.5.4-0ubuntu1.5 บน 26.04, 8.3.6-0ubuntu0.24.04.13 บน 24.04 และ 8.1.2-1ubuntu2.28 บน 22.04 ส่วน PHP 7.4 บน 20.04 ได้แพตช์ผ่าน Ubuntu Pro (ESM) เท่านั้น
  • remi (rpms.remirepo.net) php.net แจ้งว่า repo นี้ยังไม่มีอัปเดตในวันที่ออกแพตช์ และแนะนำให้ย้ายไปใช้ package ของ distro หรือ build เอง
  • Docker image ทางการ ใน docker-library/php ขึ้นเป็นเวอร์ชันชุดใหม่ครบแล้ว แต่ image ที่ build ไว้ก่อนหน้ายังเป็น PHP ตัวเก่า ต้อง pull แล้ว build ใหม่ทุก image ที่ใช้ base นี้

แพตช์บน Ubuntu เองต้องดูจากเลข package ไม่ใช่ php -v

dpkg -l | grep -E '^ii +(php8|libapache2-mod-php)'

อัปเดตแล้วอย่าลืม restart PHP-FPM ทุกเวอร์ชัน และสั่งให้ worker ที่รันยาวอย่าง Horizon หรือ queue:work เริ่มใหม่ เพราะ process ที่รันอยู่แล้วยังใช้ PHP ตัวเก่าใน memory ต่อ วิธีเช็คเขียนไว้ในโพสต์ก่อนหน้า

บั๊กตัวแรกเกิดตอน request ใช้หน่วยความจำจนชน memory_limit

CVE-2026-109618 (GHSA-w8hw-w9fx-j79w) เกิดตอนที่ PHP กำลังเตรียมข้อมูลของ request อยู่ แล้วหน่วยความจำชน memory_limit พอดี ตอนนั้น PHP จะกระโดดออกจากงานที่ทำค้างอยู่ทันทีด้วย longjmp() ข้อมูลของ request จึงตั้งค่าไปได้แค่ครึ่งเดียว แล้วขั้นตอนเก็บกวาดตอนจบ request ก็หยิบข้อมูลที่ยังไม่สมบูรณ์นั้นไปใช้ต่อเหมือนเป็นข้อมูลปกติ คนโจมตีที่จัดหน่วยความจำได้ถูกจังหวะต่อยอดไปจนรันโค้ดได้

ทางที่ทำให้ request กินหน่วยความจำจนชนเพดานได้ ทั้งที่ยังอยู่ในขีดจำกัดที่ตั้งไว้ advisory ยกตัวอย่างไว้สามแบบ

  • ส่ง multipart/form-data ที่ไม่มี Content-Length ที่ใช้ได้ จึงข้ามการเช็ค post_max_size
  • ใส่ header ใน multipart แต่ละส่วนแบบไม่จำกัด
  • ส่งช่องอัปโหลดไฟล์ที่ว่างเปล่าเป็นจำนวนมาก ซึ่งข้าม max_file_uploads แต่ PHP ยังสร้างข้อมูลของแต่ละช่องครบ

ทั้งหมดนี้เกิดก่อนโค้ดของแอปจะได้รัน advisory ยืนยันว่ารันโค้ดได้จริงบน Apache mod_php ด้วยค่า memory_limit และค่าอื่นที่เป็นค่าเริ่มต้นของ PHP แพตช์แก้ด้วยการเลิกใช้ longjmp() ตอนหน่วยความจำไม่พอ แล้วเปลี่ยนไปจบ process ทันทีเมื่อเกินเพดานอีกขั้นแทน

บั๊กตัวที่สองคือสถานะที่ค้างข้าม request ใน worker ตัวเดียวกัน

worker ของ PHP-FPM หรือ Apache รับ request ทีละตัวต่อกันไปเรื่อย ๆ ตลอดอายุของ process ปกติ PHP จะล้างข้อมูลของ request เก่าก่อนเริ่ม request ใหม่ทุกครั้ง CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp) เกิดเมื่อ fatal error หรือหน่วยความจำไม่พอเกิดผิดจังหวะ จน PHP ไม่ได้ล้างตัวชี้บางตัว ตัวชี้นั้นยังชี้ไปที่หน่วยความจำของ request ก่อนหน้าที่คืนไปแล้ว พอ request ถัดไปใน worker เดียวกันใช้ตัวชี้นั้นก่อนที่ PHP จะตั้งค่าใหม่ ก็กลายเป็น use-after-free

advisory ยืนยันทางเข้าไว้สองทาง

  • session.upload_progress ที่เปิดอยู่เป็นค่าเริ่มต้น สถานะที่ตั้งไว้ตอน parse ฟอร์มแบบ multipart ค้างอยู่หลัง fatal error แล้ว upload progress ของ request ถัดไปก็หยิบไปใช้
  • fatal_error_backtraces ของ PHP 8.5 ที่เก็บ backtrace ตอนเกิด fatal error

ทางแรกตรงกับที่ PoC ที่หลุดมาก่อนอ้างไว้ ค่าที่ผมแนะนำให้ปิดไว้ในโพสต์ก่อนหน้าจึงปิดทางนี้ได้จริง แต่ปิดได้แค่ทางที่ยืนยันแล้ว ยังมีทางอื่นที่ advisory บอกว่าอาจมีอีก

Apache mod_php เสี่ยงที่สุด ส่วน PHP-FPM ก็ยังต้องอัปเดต

php.net บอกว่า PHP ทุกตัวที่รับ request จากเว็บอยู่ในข่าย ทั้ง PHP-FPM, Apache mod_php, php-cgi และ LiteSpeed แต่ความเสี่ยงไม่เท่ากัน

  • Apache mod_php เสี่ยงที่สุด เพราะ mod_php เรียกขั้นตอนเก็บกวาดตอนจบ request เสมอ แม้ตอนเริ่ม request จะพังไปแล้ว ทั้งสองบั๊กจึงยิงได้จาก request เดียวที่ไม่ต้องล็อกอิน ก่อนโค้ดของแอปจะรัน
  • PHP-FPM จะ kill child process ทิ้งเมื่อตอนเริ่ม request พัง จึงยิงยากกว่า แต่ advisory บอกว่ายังยิงได้ผ่านโค้ดบางแบบของแอป และเปิดช่องให้บั๊กตัวที่สองเมื่อ nginx ส่ง PHP_VALUE หรือ PHP_ADMIN_VALUE เข้ามาทาง FastCGI

ข้อสุดท้ายเช็คได้ง่าย ลองค้นใน config ของ nginx ดูว่ามีเว็บไหนตั้งค่า PHP ผ่าน fastcgi_param อยู่บ้าง

grep -rn -E 'PHP_(ADMIN_)?VALUE' /etc/nginx/

ถ้าเจอ ให้ย้ายค่านั้นไปไว้ใน pool ของ FPM แทน

ค่าที่ php.net แนะนำให้ตั้งระหว่างรออัปเดต

php.net ย้ำว่ามาตรการพวกนี้ ลดได้แค่บางทางเข้า และไม่ได้ทำให้เครื่องปลอดภัย ใช้เพื่อซื้อเวลาเท่านั้น และทุกค่าต้องตั้งในระดับที่ PHP อ่านก่อน parse request คือ php.ini, pool ของ FPM หรือ config ของ Apache ตั้งผ่าน ini_set() หรือ .user.ini ไม่ได้ผล

สำหรับบั๊กตัวแรก

  • ตั้ง memory_limit=-1 ด้วย php_admin_value ใน pool ของ FPM แอปจะได้เขียนทับไม่ได้ ค่านี้ตัดจังหวะที่ชนเพดานออกไป แต่แปลว่า request เดียวกินหน่วยความจำได้ไม่จำกัด จึงต้องคู่กับข้อถัดไป
  • จำกัดหน่วยความจำด้วย container หรือ cgroup แทน และตั้งให้ kill เฉพาะ worker ที่กินเกิน ไม่ใช่ทั้ง service อย่าใช้ RLIMIT_AS หรือ RLIMIT_DATA เพราะจะทำให้ PHP วิ่งเข้าทางที่มีบั๊กพอดี
  • ลด post_max_size เหลือ 8M ถ้าแอปไม่รับอัปโหลด หรือ 32M ถ้ารับ และให้ upload_max_filesize ต่ำกว่านั้น
  • จำกัดขนาด body, จำนวน field, จำนวน part ใน multipart และจำนวน header ที่ nginx ด้วย ถ้าเว็บไม่มีการอัปโหลดเลย จะบล็อก multipart/form-data ที่ nginx ไปเลยก็ได้

สำหรับบั๊กตัวที่สอง

  • ตั้ง session.upload_progress.enabled=0 แล้ว restart worker ทุกตัว แอป Laravel ไม่ได้ใช้ค่านี้ ปิดได้เลย
  • บน PHP 8.5 ตั้ง fatal_error_backtraces=0 แล้ว restart worker
  • ลบค่า ini ที่เลิกใช้หรือเขียนผิดซึ่งตั้งแยกต่อเว็บ เพราะค่าพวกนี้ทำให้ PHP พ่น warning ออกมาก่อนเริ่ม request การซ่อน error ด้วย display_errors หรือ @ ไม่ช่วย
  • ตั้ง pm.max_requests=1 ให้ worker ของ FPM จบตัวเองหลังรับ request เดียว ทางนี้ตัดการค้างข้าม request ได้ แต่ทำให้เว็บช้าลงเพราะต้องเปิด process ใหม่ทุก request
php_admin_value[memory_limit] = -1
php_admin_value[session.upload_progress.enabled] = 0
pm.max_requests = 1

ค่าพวกนี้ควรถอดออกหลังอัปเดตเสร็จ โดยเฉพาะ memory_limit=-1 กับ pm.max_requests=1 ที่ไม่ควรค้างไว้บน production

บั๊กตัวแรกเจอตั้งแต่ปี 2567 แต่ทีม security ตัดสินใจไม่ได้

โพสต์ของ PHP Foundation เล่าเบื้องหลังไว้ตรง ๆ ว่าสัญญาณของบั๊กตัวแรกโผล่ตั้งแต่ปี 2567 ระหว่างที่ทีมแก้ข้อที่ Quarkslab เจอจาก security audit รอบแรกของ PHP แต่ตอนนั้นยังไม่มีใครหาทางโจมตีได้จริง พอบั๊กอยู่ในส่วนจัดการหน่วยความจำที่ทุก request ต้องผ่าน ทางแก้ทุกทางจึงแลกกับอะไรบางอย่าง ทั้ง compatibility, performance และ extension ของคนอื่น ทีม security ตัดสินใจแบบต้องเห็นตรงกันทุกคน จึงเถียงกันอยู่หลายเดือนโดยไม่ได้ข้อสรุป Foundation เรียกเรื่องนี้ว่าความล้มเหลวเชิงโครงสร้าง ทั้งจากวิธีตัดสินใจและจากคนที่ไม่พอ

ที่ทำให้เรื่องกลับมาเดินคือรายงานสามรอบช่วงสิงหาคมถึงกันยายน

วันที่ (2569) เกิดอะไรขึ้น
11 ส.ค. นักวิจัยของ Calif กับ OpenAI ต่างคนต่างรายงาน RCE ที่มีต้นเหตุเดียวกัน
24 ส.ค. Wiz รายงานทางโจมตีเพิ่ม
18 ก.ย. OpenAI รายงานทางโจมตีเพิ่มอีก
20 ต.ค. 22:00 น. เลข CVE หลุดออกมาในฟอรัม พร้อมเวลา release และคะแนน CVSS 10.0 ที่ผิด
21 ต.ค. 02:30 น. php.net ประกาศล่วงหน้า และเลื่อน release จากวันที่ 22 มาเป็นวันที่ 21
21 ต.ค. 03:00 น. Calif เผลอปล่อยวิดีโอสาธิตการโจมตี แล้วลบทีหลัง แต่สำเนาก็กระจายไปแล้ว
21 ต.ค. 14:41 น. Chainguard ปล่อย container image ที่แพตช์แล้วก่อนกำหนดกว่าหกชั่วโมง
21 ต.ค. 21:00 น. release ทางการ

เวลาในตารางเป็นเวลาไทย แปลงจากเวลา UTC ในโพสต์ของ PHP Foundation

ฝั่ง OpenAI บอกว่า Calif ใช้โมเดล GPT-5.6 Sol หาบั๊กตัวหนึ่งเจอในโครงการ Patch the Planet แล้ว OpenAI ช่วยประสานการเปิดเผยกับผู้ดูแล PHP ผู้ให้บริการโครงสร้างพื้นฐาน และหน่วยงานรัฐ ส่วน PHP Foundation เล่าว่าตอนนี้ใช้ทั้ง Claude ของ Anthropic และเครื่องมือของ OpenAI ช่วยกรองรายงาน แต่ทุกการตัดสินใจและทุกแพตช์ยังผ่านคนเสมอ ตั้งแต่เดือนกรกฎาคมทีมออก CVE ไปแล้ว 15 ตัว และคัดรายงานที่ซ้ำ ผิด หรือทำซ้ำไม่ได้ทิ้งไปกว่า 100 ฉบับ

หลังจากนี้ Foundation เปลี่ยนให้ทีม security ตัดสินด้วยเสียงข้างมากแทนการรอให้เห็นตรงกันทุกคน จ้างคนเพิ่มสำหรับคัดรายงานและงาน hardening และขอทุนจากบริษัท ผู้ให้บริการ hosting และหน่วยงานรัฐที่ใช้ PHP ผ่านหน้า sponsor เพื่อสร้างคนที่เข้าใจ engine ของ PHP ลึกพอจะแก้บั๊กแบบนี้ได้เร็วขึ้น

อัปเดตแล้วยังควรตรวจหาร่องรอย

วิดีโอสาธิตหลุดออกมาตั้งแต่ตีสามของวันที่ 21 เกือบหนึ่งวันก่อนแพตช์ออก อัปเดตเสร็จแล้วจึงควรไล่ดูต่อว่ามีใครเข้ามาก่อนหน้านั้นหรือเปล่า วิธีดูเขียนไว้ในหัวข้อสุดท้ายของโพสต์ก่อนหน้าแล้ว advisory ของบั๊กตัวที่สองเพิ่มอีกจุดหนึ่ง คือเปิด log_errors=On กับ error_reporting=E_ALL แล้วดู log ของ PHP ช่วงก่อนแพตช์ หา warning, fatal error หรือ timeout ที่เกิดก่อนสคริปต์ของแอปจะเริ่มรัน ถ้า request ปกติของแอปไม่เคยทำให้เกิดแบบนั้น ก็เป็นสัญญาณที่ควรดูต่อ

รายละเอียดทางเทคนิคทั้งหมดอยู่ใน advisory สองตัวบน GitHub ส่วนเบื้องหลังการตัดสินใจและข่าวหลุด อ่านต่อได้ที่โพสต์ของ PHP Foundation ครับ

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →