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.
This commit is contained in:
+32
-19
@@ -101,33 +101,46 @@ This is a stable write primitive: the session file persists on disk with
|
||||
embedded PHP code, accessible at a known path
|
||||
(`session.save_path/sess_<SID>`).
|
||||
|
||||
### The UAF chain
|
||||
### The UAF chain (two-request approach)
|
||||
|
||||
1. **Request 1** (OOM):
|
||||
- Multipart POST with `PHP_SESSION_UPLOAD_PROGRESS` + field flood
|
||||
- Progress handler calls `php_var_serialize()`:
|
||||
- `php_var_serialize_init()` -> `emalloc(64)` -> `BG(serialize).data`
|
||||
- `BG(serialize).level = 1`
|
||||
- Field flood triggers OOM -> `zend_bailout()`
|
||||
- `php_var_serialize_destroy()` never runs
|
||||
- `BG(serialize).level` stays at 1, `.data` points to freed 64-byte slot
|
||||
1. **Request 1** (prep):
|
||||
- Multipart POST with `PHP_SESSION_UPLOAD_PROGRESS` = ~500KB value
|
||||
- File upload triggers progress handler
|
||||
- `php_session_rfc1867_update()` writes session file with the ~500KB key
|
||||
- No OOM; just creates the large session file on disk
|
||||
|
||||
2. **Request 2** (OOM):
|
||||
- Same `PHPSESSID` cookie (references the large session file)
|
||||
- `PHP_SESSION_UPLOAD_PROGRESS` = small value
|
||||
- Form fields fill memory to near `memory_limit` (~7.5MB of 8MB)
|
||||
- File upload triggers progress handler:
|
||||
- `php_session_initialize()` -> reads ~500KB session file
|
||||
- `php_session_decode()` -> `php_var_unserialize_init()`:
|
||||
- `BG(unserialize).level = 1`
|
||||
- `BG(unserialize).data = emalloc(64)` (64-byte ZendMM bin)
|
||||
- Unserialize of the ~500KB key string: `emalloc(~500KB)` -> **OOM**
|
||||
- `zend_bailout()` (longjmp)
|
||||
- `php_var_unserialize_destroy()` never runs
|
||||
- Worker survives on Apache prefork
|
||||
- `BG(unserialize).level` stays at 1, `.data` points to freed 64-byte slot
|
||||
- Confirmed by error log: "tried to allocate 1003520 bytes" during
|
||||
session handling
|
||||
|
||||
2. **Between requests**:
|
||||
3. **Between requests**:
|
||||
- `php_request_shutdown()` calls `zend_deactivate()` -> ZendMM heap destroyed
|
||||
- `BG(serialize).data` pointer is now dangling (points into freed heap)
|
||||
- ZendMM reuses chunk cache for the next request's heap
|
||||
- `BG(unserialize).data` pointer is now dangling (freed heap)
|
||||
- ZendMM chunk caching preserves mmap'd pages; next request's heap
|
||||
reuses the same virtual addresses
|
||||
|
||||
3. **Request 2** (spray + trigger):
|
||||
4. **Request 3** (spray + trigger):
|
||||
- POST fields allocate `zend_string` objects via `emalloc`
|
||||
- Strings of 39 bytes (24-byte header + 39 + 1 NUL = 64) land in the
|
||||
same 64-byte bin, potentially reclaiming the stale slot
|
||||
- Upload progress handler calls `php_var_serialize()`:
|
||||
- `php_var_serialize_init()` checks `BG(serialize).level > 0`
|
||||
- Level is 1 (stale), so it increments to 2 and returns the stale
|
||||
`.data` pointer
|
||||
- Upload progress handler calls `php_session_decode()`:
|
||||
- `php_var_unserialize_init()` checks `BG(unserialize).level > 0`
|
||||
- Level is 1 (stale), so it increments to 2 and returns `.data`
|
||||
- The pointer now references attacker-controlled spray data
|
||||
- The serialize code treats it as a `php_serialize_data` struct
|
||||
- The unserialize code treats it as a `php_unserialize_data` struct
|
||||
containing a `HashTable`
|
||||
- `pDestructor` (offset 48) is called during `zend_hash_destroy()`
|
||||
- If `pDestructor` points to `system()`, `zend_execute_ex()`, or a
|
||||
@@ -232,4 +245,4 @@ and the stale BG globals persist.
|
||||
- **Rapid7**: RCE over HTTP with default PHP config; mod_php highest risk; attack complexity HIGH
|
||||
- **Wiz**: Confirmed RCE against stock WordPress, Drupal, Joomla on affected versions
|
||||
- **0x4148 lab**: Heap grooming targets ZendMM 320-byte slab bin (note: our analysis finds 64-byte bin for `php_serialize_data`)
|
||||
- **Sansec**: Disabling `session.upload_progress.enabled` alone is NOT sufficient (stale BG state persists from any OOM during multipart)
|
||||
- **Sansec**: Analysis of mitigation options and affected versions
|
||||
|
||||
Reference in New Issue
Block a user