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
127 lines
5.1 KiB
Markdown
127 lines
5.1 KiB
Markdown
# 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.
|