Files
php2shell/uaf-rce/README.md
T
neko a5396a6ab0 Fix UAF exploit to use confirmed two-request OOM approach
The original exploit put the file upload before the field flood,
so the progress handler's serialize completed before OOM fired.
This meant BG(serialize) was not left stale.

The corrected flow:
  Phase 1: Create ~500KB session file via large upload progress value
  Phase 2: Fill memory with fields, then file upload triggers session
           re-init which OOMs during unserialize of the large session
  Phase 3: Verify stale BG(unserialize) state on the surviving worker

Confirmed on PHP 8.3.6, Apache 2.4.58: error log shows
"tried to allocate 503808 bytes" during session handling,
followed by "Session ini settings cannot be changed when a
session is active" on subsequent requests.
2026-10-21 22:17:29 +00:00

5.7 KiB

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

Use-after-free in PHP's session upload progress handler, exploitable over HTTP against Apache mod_php with default PHP configuration. No LFI, no framework dependency, no non-default settings.

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: Create large session file

A multipart POST with a ~500KB PHP_SESSION_UPLOAD_PROGRESS value causes the progress handler to write a session file containing the ~500KB key. This is a normal request; no OOM occurs.

Phase 2: OOM during session re-init

A second request to the same session ID fills memory with form fields (~7MB toward an 8MB limit), then includes a file upload. The file upload triggers the progress handler, which calls php_session_initialize() to re-read the ~500KB session file.

php_session_decode() calls php_var_unserialize_init(), which allocates a 64-byte struct and sets BG(unserialize).level = 1. The unserialize of the ~500KB key requires a ~500KB allocation, which exceeds memory_limit and fires zend_bailout() (longjmp). php_var_unserialize_destroy() is skipped.

On Apache prefork, the worker survives. BG(unserialize).level stays at 1 and .data points to freed memory.

Phase 3: Verify stale state

A third request with a file upload hits the same worker. The progress handler calls php_var_unserialize_init(), which sees BG(unserialize).level > 0 (stale), increments it, and returns the stale .data pointer. This is the UAF: the pointer was freed when the previous request's heap was destroyed, and now references whatever the new request allocated in the same 64-byte ZendMM bin slot.

Phase 4: Heap spray (for crash testing)

With --crash-test, 39-byte zend_string allocations (24-byte header

  • 39 + 1 NUL = 64 bytes, same ZendMM bin) attempt to reclaim the stale slot. The unserialize code treats the spray data as a struct containing a HashTable. The pDestructor function pointer (offset 48) is under attacker control, giving RIP control 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 unserialize, causing pDestructor to fire

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 (any PHP endpoint)
--session-path /var/www/uaf-test/sessions session.save_path on the target
--progress-size 500000 Size of PHP_SESSION_UPLOAD_PROGRESS value
--oom-fields 0 (auto) Number of OOM padding fields (0 = auto-detect)
--field-size 40960 Size of each padding field value
--spray-rounds 20 Heap spray rounds per worker
--oom-only off Stop after Phases 1-3 (no spray)
--crash-test off Run full chain including heap spray

What --oom-only confirms

  1. OOM via multipart field flood (HTTP 500)
  2. Apache prefork worker survival (PIDs unchanged after OOM)
  3. Large session file creation via upload progress handler
  4. OOM during session re-init (unserialize of large key)
  5. Stale BG(unserialize) state available on subsequent requests

The patch

Commit 3584a98bda0f2c0d367255b046798b0d3a221a19 fixes the vulnerability through three changes:

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

  2. BG state reset: New php_basic_early_rinit() 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.

Confirmed primitives

Tested on PHP 8.3.6, Apache 2.4.58, Ubuntu 24.04, x86_64:

Primitive Status
OOM via multipart field flood Confirmed
Apache prefork worker survival Confirmed (PIDs unchanged)
Large session file creation Confirmed (~500KB via upload progress)
OOM during session re-init Confirmed ("tried to allocate 1003520 bytes")
Stale BG(unserialize).level Confirmed (persists across requests)
Heap spray (64-byte bin) Implemented
Worker crash via UAF Not yet triggered (needs precise heap layout)
Full RCE Not demonstrated (needs ASLR bypass + heap feng shui)

References

  • PHP patch: php/php-src@3584a98bda0f2c0d367255b046798b0d3a221a19
  • CVSS: 9.2 (CVE-2026-109618), 9.2 (CVE-2026-109619)
  • Attack complexity: HIGH (Rapid7)

See FINDINGS.md for the full vulnerability analysis.