Files
php2shell/uaf-rce/README.md
T
neko 76bc2bc2a3 UAF RCE exploit via OOM bailout (CVE-2026-109618/109619, no LFI)
Pure memory corruption path: OOM during multipart parsing leaves stale
BG(serialize) state in Apache mod_php prefork workers. Heap spray
reclaims the freed 64-byte slot, giving control of a HashTable's
pDestructor for arbitrary function call.

No LFI, no framework, no non-default PHP settings required.

Tested on PHP 8.3.6 + Apache 2.4.58 (prefork MPM). Confirmed:
- OOM via field flood, worker survival, session file persistence
- Stale BG(serialize).level across requests
- Heap spray targeting ZendMM 64-byte bin
2026-10-21 22:12:06 +00:00

5.1 KiB

CVE-2026-109618 / CVE-2026-109619: UAF RCE via OOM Bailout

Use-after-free in PHP's multipart parser and session upload progress handler. Achieves code execution over HTTP with default PHP configuration, no LFI, no framework, no application code dependency.

Affected versions

PHP 5.4.0 through 8.5.11 running under Apache mod_php with prefork MPM.

Patched in PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35.

PHP-FPM is not vulnerable to the cross-request UAF: FPM workers exit on bailout (code 70), so stale state never persists.

Prerequisites

  • Apache mod_php with prefork MPM (default on Debian/Ubuntu)
  • session.upload_progress.enabled = 1 (default)
  • Any PHP-handled URL (even a 404 page)
  • memory_limit reachable via multipart POST fields

No LFI. No specific framework or library. No non-default PHP settings.

How it works

Phase 1: OOM trigger

A multipart POST includes PHP_SESSION_UPLOAD_PROGRESS (with embedded PHP code), a file upload (which activates the progress handler), and a flood of large form fields. The progress handler calls php_var_serialize(), which allocates a 64-byte struct (php_serialize_data) in the ZendMM slab allocator and sets BG(serialize).level = 1.

The field flood exhausts memory_limit. PHP calls zend_bailout() (longjmp), which skips php_var_serialize_destroy(). The serialize state survives: BG(serialize).level stays at 1, and .data points to a freed 64-byte slot.

On Apache prefork, the worker survives the bailout and accepts new requests with this stale state. The session file also persists on disk with the attacker's PHP code, because MULTIPART_EVENT_END cleanup never ran.

Phase 2: Heap spray

Subsequent requests to the same worker allocate zend_string objects via emalloc. Strings of exactly 39 bytes (24-byte header + 39 + 1 NUL = 64) land in the same ZendMM 64-byte bin, potentially reclaiming the freed BG(serialize).data slot with attacker-controlled content.

Phase 3: UAF trigger

Another multipart POST with PHP_SESSION_UPLOAD_PROGRESS. The progress handler calls php_var_serialize_init(), which sees BG(serialize).level > 0 (stale), increments it, and returns the stale .data pointer. The serialize code treats the attacker-controlled spray data as a php_serialize_data struct containing a HashTable. The pDestructor function pointer (offset 48) is under attacker control, giving arbitrary function call during zend_hash_destroy().

Full RCE (not implemented in this PoC)

A weaponized exploit would:

  1. Leak a PHP base address via CVE-2026-109619 info disclosure (stale session data leaks freed heap pointers) to defeat ASLR
  2. Construct a fake HashTable with pDestructor pointing to system()
  3. Place the command string at the arData pointer location
  4. Trigger serialize, causing pDestructor to fire on the fake data

Usage

python3 exploit.py --target http://host:port/index.php
python3 exploit.py --target http://host:port/index.php --oom-only
python3 exploit.py --target http://host:port/index.php --crash-test

Options

Flag Default Description
--target (required) Target URL
--cmd id Command to embed in session payload
--session-path /var/www/uaf-test/sessions session.save_path on the target
--oom-fields 0 (auto) Number of OOM padding fields
--field-size 40960 Size of each padding field value
--spray-rounds 20 Heap spray rounds per worker
--trigger-rounds 50 UAF trigger attempts per worker
--oom-only off Stop after Phase 1 (OOM + session poison)
--crash-test off Run full chain, report crash status

What --oom-only confirms

  1. OOM via multipart field flood (HTTP 500 response)
  2. Apache prefork worker survival (PIDs unchanged after OOM)
  3. Session file persistence with embedded PHP code (cleanup skipped)
  4. Stale BG(serialize) state available for subsequent requests

The patch

Commit 3584a98bda0f2c0d367255b046798b0d3a221a19 fixes the vulnerability through three changes:

  1. Delayed OOM: Replaces zend_bailout() (longjmp) with a deferred error that sets EG(vm_interrupt) and returns NULL from the allocator. A hard limit (memory_hard_limit_percent, default 200%) calls _exit(137) if exceeded.

  2. BG state reset: New php_basic_early_rinit() function resets BG(serialize), BG(unserialize), and BG(serialize_lock) to zero before SAPI activation, eliminating cross-request stale state.

  3. Session handler hardening: incomplete_classes_only mode during progress handler session decode, $_SESSION cleanup at MULTIPART_EVENT_END, and php_rinit_session_globals() at MULTIPART_EVENT_START.

References

  • PHP patch: php/php-src@3584a98bda0f2c0d367255b046798b0d3a221a19
  • Wiz: Confirmed RCE against WordPress, Drupal, Joomla using framework gadgets
  • Rapid7: RCE over HTTP with default config, mod_php highest risk, AC:H
  • JFrog: Session write-back calls fake object's serialize function, RIP control
  • 0x4148 lab: Heap grooming targeting ZendMM slab bins

See FINDINGS.md for the full vulnerability analysis.