เจาะลึก session.upload_progress: ฟีเจอร์เก่าแก่ของ PHP ที่กลายเป็นของโปรดสายแฮก LFI to RCEเจาะลึก session.upload_progress: ฟีเจอร์เก่าแก่ของ PHP ที่กลายเป็นของโปรดสายแฮก LFI to RCE

สำหรับนักพัฒนาสาย PHP หรือ System Admin ที่ดูแลเว็บเซิร์ฟเวอร์ การตั้งค่าใน php.ini หลายร้อยบรรทัดมักเป็นสิ่งที่เราปล่อยให้เป็นค่าดีฟอลต์ (Default) เพราะคิดว่าระบบออกแบบมาอย่างปลอดภัยแล้ว แต่หนึ่งในฟีเจอร์ที่ถูกเปิดมาให้ตั้งแต่เกิดและมักกลายเป็น “บันไดขั้นสุดท้าย” ให้นักเจาะระบบยึดเซิร์ฟเวอร์ได้แบบไม่รู้ตัว คือฟีเจอร์ที่มีชื่อว่า session.upload_progress

บทความนี้จะพาไปเจาะลึกว่าฟีเจอร์นี้สร้างมาเพื่อแก้ปัญหาอะไร ทำงานอย่างไร และเหตุใดจึงมักถูกนำไปใช้เป็นอาวุธในการเปลี่ยนช่องโหว่อ่านไฟล์ธรรมดา (LFI) ให้กลายเป็นการยึดเซิร์ฟเวอร์เต็มรูปแบบ (RCE)


จุดเริ่มต้น: ทำไม PHP ต้องมี Session Upload Progress?

ย้อนกลับไปในยุคของ Web 1.0 และ 2.0 ยุคแรก (ช่วงก่อน HTML5 และก่อนที่เบราว์เซอร์จะมี Fetch/XHR Upload API สมบูรณ์) เวลาผู้ใช้งานอัปโหลดไฟล์ขนาดใหญ่ เช่น วิดีโอหรือไฟล์ Zip ขนาดหลายสิบเมกะไบต์ หน้าจอเว็บจะค้างนิ่งอยู่แบบนั้น ผู้ใช้ไม่มีทางรู้ได้เลยว่าไฟล์กำลังส่งอยู่ ส่งไปกี่เปอร์เซ็นต์ หรือเบราว์เซอร์ค้างไปแล้ว

ทีมพัฒนา PHP จึงเพิ่มฟีเจอร์ Session Upload Progress เข้ามาตั้งแต่ PHP 5.4 เพื่อเป็นทางลัดให้ Server ช่วยรายงานความคืบหน้า

  1. ฟอร์ม HTML ส่งไฟล์ขึ้นมาพร้อมระบุคีย์พิเศษ (ดีฟอลต์คือ PHP_SESSION_UPLOAD_PROGRESS)
  2. ขณะที่เว็บเซิร์ฟเวอร์กำลังสตรีมรับข้อมูลไบต์ของไฟล์ PHP จะแอบเขียนสถานะลงในตัวแปร $_SESSION ของผู้ใช้เป็นระยะ
  3. หน้าเว็บใช้ JavaScript ส่ง AJAX ยิงกลับมาถามเซิร์ฟเวอร์เรื่อยๆ เพื่อดึงข้อมูลไคลเอนต์นั้นไปวาดเป็นหลอด Progress Bar

ตัวอย่างโครงสร้างข้อมูลใน $_SESSION

เมื่อเปิดฟีเจอร์นี้ ข้อมูล Session จะถูกเขียนโครงสร้างหน้าตาประมาณนี้โดยอัตโนมัติ

PHP

$_SESSION["upload_progress_avatar_upload"] = [
    "start_time" => 1716300000,
    "content_length" => 15728640, // ขนาดไฟล์ทั้งหมด (Bytes)
    "bytes_processed" => 7864320,  // ส่งมาแล้วกี่ไบต์
    "done" => false,
    "files" => [
        [
            "field_name" => "file",
            "name" => "backup.zip",
            "tmp_name" => "/tmp/phpYzdqkD",
            "error" => 0,
            "done" => false,
            "start_time" => 1716300000,
            "bytes_processed" => 7864320,
        ]
    ]
];

ดาบสองคม: เบื้องหลังที่ทำให้เกิดช่องโหว่ความปลอดภัย

ความสะดวกในวันวานกลายมาเป็นความเสี่ยงระดับวิกฤตในปัจจุบัน ด้วยพฤติกรรมการทำงานเบื้องหลัง 3 ประการของ PHP


ประการที่ 1: บังคับสร้างไฟล์ Session แม้โค้ดจะไม่สั่ง session_start()

โดยปกติ PHP จะสร้างไฟล์ Session บน Disk (มักอยู่ที่ /tmp/sess_<session_id>) ก็ต่อเมื่อในโค้ดมีคำสั่ง session_start() เท่านั้น แต่เมื่อเปิดใช้งาน Upload Progress ทันทีที่มี Request แบบ multipart/form-data ส่งมาพร้อมชื่อฟิลด์ตามที่กำหนด PHP engine จะสร้างไฟล์ Session ขึ้นบนเครื่องทันที แม้โค้ดปลายทางจะเป็นแค่ไฟล์ PHP เปล่า ๆ ก็ตาม


ประการที่ 2: Attacker กำหนดชื่อไฟล์และเนื้อหาในไฟล์ได้ตามใจ

  • กำหนดชื่อไฟล์
    ผู้โจมตีสามารถส่ง HTTP Header Cookie: PHPSESSID=hacked; ทำให้เซิร์ฟเวอร์สร้างไฟล์ชื่อตรง ๆ ว่า /tmp/sess_hacked
  • กำหนดเนื้อหา (Payload Injection)
    ค่าของฟิลด์ PHP_SESSION_UPLOAD_PROGRESS สามารถส่งเป็นโค้ด PHP ลงไปได้ตรง ๆ เช่น
    HTML
    PHP_SESSION_UPLOAD_PROGRESS = "<?php system($_GET['c']); ?>"
    PHP จะนำสตริงนี้ไปบันทึกเป็นชื่อ Key ภายในไฟล์ /tmp/sess_hacked

ประการที่ 3: ปัญหาการแย่งชิงจังหวะ (Race Condition)

แม้ PHP จะมีออปชัน session.upload_progress.cleanup = 1 ซึ่งทำหน้าที่ล้างข้อมูล Upload Progress ทันทีที่การอัปโหลดเสร็จสิ้น แต่ในช่วงเสี้ยววินาทีที่ไฟล์กำลังอัปโหลด (หรือกรณีที่แฮกเกอร์จงใจส่งไฟล์ขนาดยักษ์แบบหน่วงความเร็วเอาไว้) ไฟล์ Session ที่แฝงโค้ด PHP อันตรายจะยังคงค้างอยู่บนดิสก์


ท่าไม้ตายยอดฮิต: Local File Inclusion (LFI) to RCE

เมื่อระบบมีช่องโหว่ประเภท Local File Inclusion (LFI) เช่น โค้ดมีการเขียนแบบไม่รัดกุม

PHP

&lt;?php
// vuln.php
$page = $_GET['page'];
include($page); // เช่น include("/var/log/nginx/access.log");

ในอดีต หากเซิร์ฟเวอร์ไม่มีจุดให้อัปโหลดไฟล์ และไม่มีการเปิด Log Injection ช่องโหว่ LFI อาจทำได้แค่อ่านไฟล์ Config ในเครื่อง (เช่น /etc/passwd หรือ wp-config.php) แต่ไม่สามารถรันคำสั่ง Shell ได้

แต่เมื่อมี Session Upload Progress เข้ามาช่วย

Plaintext

[Attacker]
    │
    ├─► 1. ส่ง POST Request ขนาดใหญ่ค้างไว้
    │      Header: Cookie: PHPSESSID=evil
    │      Form: PHP_SESSION_UPLOAD_PROGRESS = "&lt;?php system('id'); ?>"
    │
[PHP Engine]
    │
    ├─► 2. สร้างไฟล์ /tmp/sess_evil ซึ่งมีโค้ดอันตรายฝังอยู่ข้างในทันที
    │
[Attacker] (ส่ง Request คู่ขนานทันที)
    │
    └─► 3. ส่ง GET Request: vuln.php?page=/tmp/sess_evil
           PHP รันคำสั่ง system('id') สำเร็จ ──► ยึดเซิร์ฟเวอร์ (RCE)

ผู้โจมตีไม่จำเป็นต้องมีสิทธิ์อัปโหลดไฟล์บนเว็บ และไม่ต้องพึ่งพาไฟล์ใด ๆ บนระบบเลย เพียงแค่ยิงแพ็กเก็ตสองเส้นเข้าเซิร์ฟเวอร์พร้อมกัน ก็สามารถยึดสิทธิ์ระดับ Web User (เช่น www-data หรือ nobody) ได้ทันที


โลกยุคปัจจุบัน: ทำไมฟีเจอร์นี้ถึงหมดความจำเป็น?

ในสถาปัตยกรรมเว็บสมัยใหม่ เบราว์เซอร์ทุกตัวรองรับมาตรฐานการติดตามสถานะไฟล์ผ่านฝั่ง Client-side อย่างสมบูรณ์แบบแล้ว

  • ใช้ Fetch API หรือ XMLHttpRequest.upload.onprogress คำนวณขนาดไบต์ที่ส่งออกจากเบราว์เซอร์แบบ Real-time
  • ไม่กินทรัพยากร Disk I/O ของเซิร์ฟเวอร์ในการอ่าน/เขียนไฟล์ Session
  • ไม่ต้องคอยยิง AJAX ซ้ำซ้อนเข้าหลังบ้านเพื่อขอเช็ค Progress ให้เปลือง Bandwidth
  • สามารถอัปโหลดตรงไปยัง Object Storage (เช่น AWS S3, Cloudflare R2) ได้โดยไม่ต้องผ่านเว็บเซิร์ฟเวอร์ PHP ด้วยซ้ำ

ฟีเจอร์ session.upload_progress ใน PHP จึงกลายเป็นเสมือน Legacy Feature ที่แทบไม่มีการหยิบมาใช้อย่างถูกต้องอีกต่อไป แต่กลับเปิดอ้าไว้เป็นพื้นผิวการโจมตี (Attack Surface) ให้ระบบตกอยู่ในความเสี่ยง


วิธี Hardening ปิดฟีเจอร์นี้บนเซิร์ฟเวอร์

การปิดฟีเจอร์นี้ไม่ส่งผลกระทบต่อระบบส่วนใหญ่ (รวมถึง WordPress, Laravel, Symfony หรือ Custom CMS ทั่วไป)


วิธีที่ 1: ปิดผ่าน php.ini

เปิดไฟล์ php.ini ของระบบ (ทั้งใน CLI และ FPM) แล้วแก้ไขค่าดังนี้:

Ini, TOML

; ปิดการติดตาม upload progress
session.upload_progress.enabled = 0

วิธีที่ 2: ล็อกตายตัวผ่าน PHP-FPM Pool Config (แนะนำ)

เพื่อป้องกันไม่ให้สคริปต์ในระดับแอปพลิเคชันหรือปลั๊กอินแอบเปิดใช้งานผ่านคำสั่ง ini_set() ควรใช้คำสั่งระดับ Admin ในไฟล์คอนฟิก Pool ของ PHP-FPM (เช่น /etc/php/[version]/fpm/pool.d/www.conf):

Ini, TOML

php_admin_flag[session.upload_progress.enabled] = off

หลังจากแก้ไข อย่าลืมสั่งรีสตาร์ตเซอร์วิส PHP-FPM เพื่อให้คอนฟิกมีผลทันที:

Bash

sudo systemctl restart php*-fpm

และสามารถตรวจสอบความถูกต้องได้ด้วยคำสั่ง

Bash

php-fpm -i | grep session.upload_progress

สรุป

ช่องโหว่ความปลอดภัยร้ายแรงหลายครั้งไม่ได้เกิดจากข้อผิดพลาดของโค้ดที่ซับซ้อน แต่เกิดจาก “ฟังก์ชันอำนวยความสะดวกในอดีต” ที่หมดประโยชน์ไปแล้วแต่ยังคงเปิดทิ้งไว้ตามค่าเริ่มต้น การสละเวลาตรวจสอบและปิด session.upload_progress.enabled ในกระบวนการ Server Hardening จึงเป็นวิธีที่ง่าย ใช้เวลาไม่ถึง 2 นาที แต่ตัดช่องทางการโจมตี LFI-to-RCE ยอดนิยมของบรรดาแฮกเกอร์ได้อย่างมีประสิทธิภาพ