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
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# 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](FINDINGS.md) for the full vulnerability analysis.
|
||||
Reference in New Issue
Block a user