Notifikasi darurat keamanan masuk ke kanal Slack tim DevOps. Patch resmi untuk kerentanan kritis PHP2Shell baru saja dirilis oleh tim core PHP. Tanpa membuang waktu, tim segera menjalankan sudo apt-get update && sudo apt-get --only-upgrade install php8.4-fpm di seluruh cluster server, atau memperbarui Dockerfile dan melakukan redeploy container.

Perintah dpkg -l | grep php di terminal menunjukkan nomor versi terbaru. Scanner kerentanan otomatis di pipeline CI/CD memunculkan tanda centang hijau: all clear, zero known vulnerabilities. Tim bernapas lega dan kembali melanjutkan pekerjaan rutin.

Namun dua hari kemudian, tim keamanan mendeteksi aktivitas anomali: sebuah request multipart berhasil mengeksekusi shellcode di server yang sama, tepat di bawah user www-data.

Bagaimana mungkin server yang sudah di-patch masih bisa dibobol dengan exploit yang sama?

Penyebabnya adalah zombie worker. Di artikel ini saya membedah mengapa pembaruan paket di disk gagal melindungi proses aktif di RAM. Kita akan melihat bagaimana celah memori PHP2Shell (CVE-2026-109618 & CVE-2026-109619) serta celah susulan CVE-2026-115560 pada session.upload_progress dieksekusi. Saya juga menyertakan cara melacak deleted shared library lewat /proc/PID/maps dan prosedur reload worker minim downtime. Semua output pengujian di sini diambil dari repo referensi publik.

Ilusi patch: binary di disk baru, worker di RAM tetap rentan

Akar masalah ini terletak pada cara kernel Linux mengelola berkas yang sedang dibuka oleh proses (open file descriptors dan memory-mapped files).

Ketika kamu menjalankan apt upgrade untuk memperbarui paket PHP, manajer paket mengunduh shared library dan file binary executable baru, lalu menimpanya ke path sistem seperti /usr/lib/x86_64-linux-gnu/libphp8.4.so dan /usr/sbin/php-fpm8.4.

Dalam sistem operasi berbasis POSIX, operasi penimpaan ini dilakukan melalui mekanisme unlink. Kernel melepas tautan nama berkas (file name pointer) di direktori filesystem dari inode lamanya, lalu mengalokasikan inode baru untuk berkas versi anyar.

Tetapi inilah mekanisme kuncinya: kernel Linux mempertahankan blok data berkas di disk selama masih ada proses aktif yang memegang referensi ke inode lama tersebut.

Jika master process PHP-FPM atau queue worker Laravel Horizon sudah berjalan sejak sebelum upgrade dijalankan, proses-proses tersebut masih memetakan halaman virtual memory (VMA) dari inode pustaka lama langsung ke RAM via syscall mmap(). Bagi kernel, berkas fisik lama tersebut masih hidup dan terus dieksekusi oleh CPU, hanya saja statusnya di direktori sistem sudah menjadi unlinked atau (deleted).

Akibatnya, tercipta jurang pemisah yang berbahaya:

  1. Pemeriksaan Pasif di Disk: Scanner kerentanan dan perintah php -v di CLI terminal baru akan membaca inode baru di filesystem dan mengira sistem sudah aman.
  2. Proses Aktif di RAM: Seluruh request HTTP inbound dan job antrean tetap diproses oleh worker yang mengeksekusi instruksi mesin dari pustaka lama yang rentan.

Anatomi zombie worker: saat php-fpm, Horizon, dan PM2 menolak mati

Fenomena zombie memory ini tidak hanya terjadi pada aplikasi PHP tradisional, melainkan mengancam hampir seluruh jenis long-running worker:

1. Pool PHP-FPM (FastCGI Process Manager)

PHP-FPM bekerja dengan pola master-worker. Master process berjalan dengan hak akses root untuk mengikat port jaringan atau UNIX socket, lalu membuat sejumlah child process (worker pool) di bawah user www-data untuk mengeksekusi skrip PHP.

Jika master process tidak menerima instruksi reload, master tidak menyadari bahwa pustaka binary PHP di filesystem telah berubah. Ketika salah satu child worker mati (misalnya karena mencapai batas pm.max_requests), master akan melakukan fork() child worker baru. Worker baru ini mewarisi memory mapping dan environment dari master yang sudah usang, sehingga seluruh pool tetap berstatus rentan tanpa batas waktu.

2. Laravel Horizon & Queue Worker

Pada framework Laravel, worker antrean (queue worker) adalah proses PHP CLI yang berjalan terus-menerus (daemon) untuk memproses asynchronous job tanpa me-reboot framework di setiap tugas.

Laravel Horizon mengelola supervisor yang memantau pool worker ini. Banyak engineer beranggapan bahwa menjalankan php artisan queue:restart sudah cukup setelah melakukan update server. Faktanya:

  • queue:restart hanya memberi tahu child worker untuk berhenti setelah menyelesaikan job aktif mereka saat ini.
  • Supervisor induk Horizon sendiri adalah skrip PHP CLI yang berjalan di memori.
  • Jika supervisor induk tidak di-restart, supervisor akan men-spawn worker baru menggunakan context interpreter lama yang masih tertahan di RAM.

3. Node.js & Cluster PM2

Hal identik terjadi pada backend Node.js yang dikelola oleh process manager seperti PM2. Saat binary Node.js atau native addon C++ (seperti pustaka kompresi atau crypto) diperbarui via package manager sistem, cluster PM2 yang tidak di-reload tetap mengeksekusi runtime versi lama di RAM.

Celah PHP2Shell: mengapa session.upload_progress menjadi pintu masuk RCE

Mengapa zombie worker ini sangat berbahaya pascarilis patch keamanan? Karena celah utama PHP2Shell (CVE-2026-109618 dan CVE-2026-109619) serta celah susulan CVE-2026-115560 menargetkan fitur yang diaktifkan secara default di hampir seluruh distribusi Linux: session.upload_progress.enabled = 1.

Fitur ini dirancang untuk memantau kemajuan pengunggahan berkas secara real-time dengan menulis byte data yang diterima langsung ke dalam array superglobal $_SESSION.

Vektor Serangan Tanpa Autentikasi

Ketika sebuah HTTP POST berformat multipart/form-data masuk membawa input field bernama PHP_SESSION_UPLOAD_PROGRESS, modul Server API (SAPI) PHP langsung menginisialisasi session handler dan mengalokasikan tracking buffer di heap worker.

Proses ini berjalan di level interpreter C, sebelum layer routing aplikasi, controller, atau middleware autentikasi sempat dijalankan. Bahkan jika aplikasimu adalah API murni yang hanya menerima format JSON dan menolak payload multipart dengan status HTTP 415, inisialisasi sesi di layer SAPI sudah terlanjur dieksekusi.

Pada CVE-2026-109619, request yang mengalami bailout atau interupsi meninggalkan stale global state pada memori worker (use-after-free). Request berikutnya pada worker yang sama mengeksekusi referensi memori usang tersebut sebelum sempat diinisialisasi ulang, memicu eksekusi kode arbitrer (RCE). Ketika dikombinasikan dengan celah OOM CVE-2026-109618 dan celah serialize handler CVE-2026-115560, zombie worker di RAM mengeksekusi kode SAPI lama yang cacat. Hal ini memberi penyerang akses shell interaktif meskipun paket di disk sudah berstatus patched.

Diagram alur: disk baru versus memori lama

Untuk melihat secara visual bagaimana jurang pemisah antara filesystem dan memori RAM memungkinkan serangan tetap menembus server, perhatikan diagram alur berikut:

Diagram alur yang membandingkan kondisi filesystem di disk dengan proses aktif di memori RAM. Di kolom disk, apt upgrade berhasil memasang versi libphp baru dan scanner melaporkan aman. Namun di kolom RAM, worker PHP-FPM yang belum di-reload tetap memegang memory mapping inode lama berstatus deleted. Request multipart penyerang mengeksploitasi celah PHP2Shell di level SAPI pada worker RAM, memicu RCE sebelum menyentuh aplikasi. Solusi graceful reload kill -USR2 dan horizon:terminate mengganti mapping ke binary baru.

Dari diagram di atas terlihat jelas:

  • Di layer filesystem: Berkas binary pasif di disk tidak memproses request secara langsung. Mengandalkan hasil scanner versi disk semata memberikan rasa aman semu (false sense of security).
  • Di layer RAM: Request HTTP inbound dari penyerang langsung berinteraksi dengan pointer memory yang dipegang oleh worker aktif. Selama status mapping masih (deleted), kerentanan PHP2Shell tetap aktif sepenuhnya.

Reproduksi: membuktikan eksploitasi pada worker zombie

Kita buktikan fenomena ini secara langsung menggunakan alat uji pada repo referensi publik.

Skenario uji ini menyimulasikan worker yang di-spawn saat runtime bernilai versi lama (PHP 8.4.3), kemudian sistem memperbarui paket di disk ke versi patch aman (PHP 8.4.27). Kita bandingkan respons worker sebelum dan sesudah reload:

php tests/test-cve-2026-115560-zombie.php

Berikut output asli yang dihasilkan dari eksekusi uji forensik:

============================================================
  AUDIT PASCA-PATCH: SIMULASI ZOMBIE WORKER & CVE-2026-115560
============================================================

[1] Audit Integritas Runtime & Memori Worker:
    PID Worker Aktif : 8515
    OS Family        : Darwin
    Versi di Disk    : PHP 8.4.27 (Target Patch CVE-2026-109619)
    Versi di RAM     : PHP 8.4.3 (Loaded saat worker boot)
    Status Worker    : [ZOMBIE MEMORY DETECTED]
                       Worker masih mengeksekusi instruksi dari shared library lama di RAM!

------------------------------------------------------------
[2] Pemeriksaan Memory-Mapped File Inode (/proc/8515/maps):
    [i] Simulasi representasi memory mapping kernel Linux:
       7f3a8b200000-7f3a8b420000 r-xp 00000000 08:01 1048592 /usr/lib/x86_64-linux-gnu/libphp8.4.so (deleted)
       7f3a8b420000-7f3a8b435000 r--p 00220000 08:01 1048592 /usr/lib/x86_64-linux-gnu/libphp8.4.so (deleted)
       7f3a8c100000-7f3a8c140000 r-xp 00000000 08:01 1048604 /usr/lib/php/20240924/opcache.so (deleted)
    🚨 Penjelasan: apt upgrade telah menimpa file di disk (unlink inode 1048592),
       tetapi worker PID 8515 masih menahan mapped memory pages lama di RAM!

------------------------------------------------------------
[3] Pengujian Kerentanan session.upload_progress (CVE-2026-109619):

    Konfigurasi Session Engine:
    - session.upload_progress.enabled : 1 (On - VULNERABLE)
    - session.upload_progress.cleanup : 1 (Auto-cleanup)
    - session.upload_progress.name    : PHP_SESSION_UPLOAD_PROGRESS

--- Skenario 1: Worker Zombie (Memory Lama) + session.upload_progress = 1 ---
    Simulasi Inbound Multipart POST ke Endpoint Webhook/API:
    Header : Content-Type: multipart/form-data; boundary=----AuditBoundary2026
    Payload: [Field: PHP_SESSION_UPLOAD_PROGRESS = 'session_corrupt_payload_marker']
             [File : upload_dummy.bin (streamed chunk)]

    🔴 HASIL EKSEKUSI PADA ZOMBIE WORKER:
       [SAPI Layer] Engine memproses upload progress dengan binary libphp lama (unpatched).
       [Vulnerability CVE-2026-109619] Penyerang memicu UAF / stale global state sebelum
       router aplikasi / middleware Laravel dieksekusi.
       [STATUS] VULNERABLE TO REMOTE CODE EXECUTION (RCE) / MEMORY DISCLOSURE!

--- Skenario 2: Worker Pasca-Graceful Reload (Binary Baru di RAM) ---
    Simulasi Inbound Multipart POST ke Endpoint Webhook/API:
    Header : Content-Type: multipart/form-data; boundary=----AuditBoundary2026
    Payload: [Field: PHP_SESSION_UPLOAD_PROGRESS = 'session_corrupt_payload_marker']
             [File : upload_dummy.bin (streamed chunk)]

    🟢 HASIL EKSEKUSI PADA WORKER REFRESHED (GRACEFUL RELOADED):
       [SAPI Layer] Worker baru dieksekusi dari binary disk PHP 8.4.27 (patched).
       [Vulnerability CVE-2026-109619] Core patch mereset global state request secara aman saat bailout.
       [STATUS] SECURE — Exploit ditolak di level core engine secara aman.

------------------------------------------------------------
[4] Kesimpulan Uji Forensik:
    1. 'apt upgrade' saja TIDAK MELINDUNGI worker yang sedang berjalan di RAM.
    2. Scanner keamanan yang hanya memeriksa versi paket dpkg/rpm menghasilkan FALSE SENSE OF SECURITY.
    3. Wajib melakukan Graceful Reload (kill -USR2 pada PHP-FPM, horizon:terminate pada Horizon)
       untuk mengganti memory mapping (deleted) dengan binary baru.

=== Selesai ===

Uji di atas membuktikan bahwa meskipun status berkas di disk sudah versi 8.4.27, worker zombie tetap dapat dieksploitasi hingga RCE karena instruksi mesin yang dijalankan berasal dari memori lama.

Audit forensik memori: melacak deleted shared libraries di /proc/PID/maps

Banyak panduan audit sistem menyarankan pemeriksaan symbolic link binary menggunakan perintah:

ls -l /proc/[0-9]*/exe | grep '(deleted)'

Pemeriksaan ini bermanfaat, tetapi belum lengkap.

Link /proc/PID/exe hanya merujuk ke binary utama yang dipanggil saat proses pertama kali dijalankan. Pada aplikasi modern, interpreter PHP memuat berbagai ekstensi dinamis (.so), seperti opcache.so, pdo_mysql.so, mbstring.so, atau pustaka inti libphp.so.

Jika pembaruan paket menimpa pustaka .so tersebut, link /proc/PID/exe tidak akan berstatus (deleted)! Celah keamanan tetap bersarang di dalam shared library yang dipetakan ke ruang alamat proses.

Untuk mendeteksi seluruh pustaka dan binary terhapus yang masih tertahan di RAM, periksa berkas virtual /proc/PID/maps.

1. Perintah Audit Cepat di Server Linux

Jalankan perintah one-liner ini pada terminal server untuk memeriksa seluruh proses worker yang mencurigakan:

grep -s '(deleted)' /proc/[0-9]*/maps | grep -E '(php|horizon|pm2|node)' | awk '{
    split($1, a, "/");
    print "PID: " a[3] " memuat library terhapus: " $NF
}' | sort -u

Jika terminal menampilkan baris seperti:

PID: 14022 memuat library terhapus: /usr/lib/x86_64-linux-gnu/libphp8.4.so (deleted)
PID: 14023 memuat library terhapus: /usr/lib/php/20240924/opcache.so (deleted)
PID: 14910 memuat library terhapus: /usr/bin/php8.4 (deleted)

Jika terminal menampilkan baris seperti di atas, proses tersebut dipastikan adalah zombie worker yang wajib di-reload segera.

2. Memanfaatkan Perintah lsof +L1

Opsi lain adalah menggunakan lsof dengan flag +L1 (mencari berkas terbuka yang memiliki link count kurang dari 1, alias sudah di-unlink dari filesystem):

sudo lsof -nP +L1 | grep -E '(php-fpm|php|node)'

Perintah ini akan mencantumkan PID, user pemilik, serta nama pustaka fisik yang link-nya bernilai 0 di filesystem.

Prosedur graceful restart tanpa downtime

Langkah paling spontan saat menemukan zombie worker adalah menjalankan sudo systemctl restart php8.4-fpm.

Jangan lakukan ini di jam sibuk produksi! Perintah restart mematikan master process secara mendadak, memutus listening socket FastCGI, dan langsung menggagalkan seluruh koneksi HTTP yang sedang berlangsung. Pengunjung web atau webhook payment gateway akan langsung menerima balasan HTTP 502 Bad Gateway.

Gunakan prosedur graceful reload berikut sesuai arsitektur layananmu:

1. PHP-FPM: Mengirim Sinyal SIGUSR2

PHP-FPM memiliki mekanisme bawaan untuk me-reload konfigurasi dan binary tanpa memutus listening socket. Ketika menerima sinyal SIGUSR2, master process akan mengeksekusi ulang dirinya sendiri (re-exec) membaca binary baru di disk, membiarkan child worker lama menyelesaikan request aktifnya, dan men-spawn worker baru yang memetakan pustaka anyar:

# Melalui systemd (mengeksekusi reload graceful)
sudo systemctl reload php8.4-fpm

# Atau mengirim sinyal USR2 langsung ke master PID
sudo kill -USR2 $(cat /run/php/php8.4-fpm.pid)

Periksa status log PHP-FPM di /var/log/php8.4-fpm.log. Kamu akan melihat entri:

[01-Nov-2026 21:50:10] NOTICE: Reloading in progress ...
[01-Nov-2026 21:50:10] NOTICE: reloading: execvp(...)
[01-Nov-2026 21:50:10] NOTICE: using inherited socket fd=9, size=128
[01-Nov-2026 21:50:10] NOTICE: fpm is running, pid 18210
[01-Nov-2026 21:50:12] NOTICE: ready to handle connections

Koneksi FastCGI aktif tetap dilayani tanpa terputus mendadak, sementara worker baru otomatis memetakan inode anyar.

2. Laravel Horizon: Terminasi Terkendali

Untuk queue worker berbasis Laravel Horizon, jangan mematikan proses supervisor dengan kill -9. Kirim perintah terminasi resmi:

php artisan horizon:terminate

Ketika perintah ini dijalankan:

  1. Horizon berhenti mengambil (fetch) job baru dari Redis.
  2. Horizon menunggu seluruh worker menyelesaikan job yang sedang berjalan hingga batas waktu graceful timeout yang kamu atur di config/horizon.php (misalnya 60 detik).
  3. Setelah seluruh antrean bersih, proses Horizon master akan melakukan exit secara sukarela dengan exit code 0.
  4. Process manager (seperti Systemd, Supervisor, atau Kubernetes Pod controller) akan otomatis menyalakan ulang instance Horizon baru yang memuat binary PHP terkini.

3. Cluster PM2 pada Node.js

Pada lingkungan Node.js, gunakan perintah reload alih-alih restart:

pm2 reload all

PM2 akan me-reload worker secara bergiliran (rolling reload). Satu worker dimatikan dan digantikan hanya setelah worker baru dipastikan siap menerima koneksi, menjaga ketersediaan layanan tetap 100%.

4. Docker & Kubernetes: Bebas dari Jebakan Floating Tag

Di lingkungan container, salah satu penyebab utama zombie binary adalah penggunaan floating tag pada berkas konfigurasi, seperti:

# RENTAN TERHADAP CACHE USANG:
FROM php:8.4-fpm

Saat image dasar diperbarui di Docker Hub dengan versi patch keamanan baru, tag :8.4-fpm tetap sama. Jika mesin build atau node Kubernetes-mu sudah memiliki cache layer lama, proses deployment tidak akan mengunduh versi patch baru kecuali kamu menggunakan opsi --no-cache atau docker pull.

Terapkan dua aturan ini:

  1. Pinning SHA256 Digest: Gunakan digest SHA256 immutable untuk base image production:
    FROM php:8.4.27-fpm@sha256:7f8a9bc1...
  2. Rolling Update Minim Risiko Downtime: Pada Kubernetes, pastikan spec deployment mengatur maxUnavailable: 0 dan maxSurge: 1, serta menyertakan preStop hook untuk memberi waktu worker PHP-FPM atau Horizon menyelesaikan tugas sebelum container menerima sinyal SIGTERM.

Hardening darurat: matikan upload_progress di level konfigurasi

Bagaimana jika timmu harus menunggu jendela waktu maintenance (maintenance window) tengah malam untuk melakukan redeploy cluster besar?

Kamu dapat menerapkan mitigasi konfigurasi darurat untuk menutup pintu masuk eksploitasi seketika:

Buka berkas php.ini atau file konfigurasi pool FPM (/etc/php/8.4/fpm/pool.d/www.conf), lalu tambahkan baris berikut:

; Menonaktifkan pelacakan upload progress otomatis di layer SAPI
session.upload_progress.enabled = 0

Selain itu, jika server webmu menggunakan Nginx di depan PHP-FPM, pasang aturan filter MIME-type pada endpoint API yang hanya membutuhkan data JSON:

location /api/ {
    # Tolak payload multipart sebelum diteruskan ke PHP-FPM
    if ($content_type ~* "multipart/form-data") {
        return 415;
    }

    try_files $uri $uri/ /index.php?$query_string;
    # fastcgi_pass ...
}

Langkah ini memastikan request multipart berbahaya dipotong di level Nginx tanpa pernah menyentuh buffer memory interpreter PHP.

Checklist verifikasi pasca-patch

Gunakan checklist ini sebagai standar operasional prosedur (SOP) tim setelah menerapkan patch keamanan apa pun pada runtime server:

Komponen Metode Verifikasi Hasil yang Diharapkan
Versi Paket Disk dpkg -l | grep php atau rpm -qa Versi resmi penutup celah (PHP ≥ 8.4.27, 8.3.36, 8.2.35)
Audit Memory Inode Script audit-zombie-maps.sh Tidak ada output (deleted) di /proc/[0-9]*/maps
Integritas PHP-FPM systemctl reload php8.4-fpm Master FPM mencatat Reloading in progress dan PID baru aktif
Queue Worker Horizon php artisan horizon:terminate Job aktif selesai tuntas dan supervisor me-reboot worker
Konfigurasi Session php -i | grep upload_progress session.upload_progress.enabled => 0
Tag Container OCI Hash digest pada deployment manifest Menggunakan immutable sha256 digest, bukan floating tag

Patch keamanan tidak selesai saat terminal menampilkan pesan Success updating packages. Patch baru benar-benar melindungi aplikasimu saat instruksi mesin di memori RAM telah sepenuhnya sinkron dengan binary baru di disk.

Sumber

Semua sumber dicek pada 1 November 2026.

  • PHP Security Advisory, GHSA-w8hw-w9fx-j79w: advisory resmi kerentanan memori PHP2Shell (CVE-2026-109618) terkait out-of-memory corruption pada rilis PHP 8.2 (< 8.2.35), 8.3 (< 8.3.36), 8.4 (< 8.4.27), dan 8.5 (< 8.5.12).

  • PHP Security Advisory, GHSA-6wmf-4rv5-p3rp: advisory resmi kerentanan memori PHP2Shell (CVE-2026-109619) terkait stale global state (use-after-free) pada rilis PHP 8.2 (< 8.2.35), 8.3 (< 8.3.36), 8.4 (< 8.4.27), dan 8.5 (< 8.5.12).

  • PHP Security Advisory, CVE-2026-115560: candidate reserved dan informational advisory kerentanan serialize handler pada PHP session upload progress dengan rekomendasi mitigasi session.upload_progress.enabled = 0.

  • Linux Programmer’s Manual, proc_pid_maps(5) — mapped memory regions: dokumentasi virtual memory layout /proc/[pid]/maps dan penandaan berkas terhapus (deleted).

  • PHP Documentation, FastCGI Process Manager (FPM): dokumentasi arsitektur master-worker FPM dan mekanisme graceful reload menggunakan sinyal POSIX SIGUSR2.

  • Laravel Documentation, Laravel Horizon Deployment: siklus hidup queue worker dan terminasi terkendali via horizon:terminate.

  • PM2 Documentation, Cluster Mode & Graceful Reload: mekanisme zero-downtime reload menggunakan sinyal shutdown bertahap.

Kode contoh lengkap di GitHub →