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

147 lines
5.7 KiB
Markdown

# 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](FINDINGS.md) for the full vulnerability analysis.