diff --git a/uaf-rce/README.md b/uaf-rce/README.md index 4a2cc46..a9c32a6 100644 --- a/uaf-rce/README.md +++ b/uaf-rce/README.md @@ -159,23 +159,39 @@ python3 rce_exploit.py calibrate --docker-image php:8.3.35-apache \ ## The patch -The fix (PHP 8.3.36, 8.4.27, 8.5.12) adds three independent defense -layers. Any one of them blocks the attack: +The fix (PHP 8.3.36, 8.4.27, 8.5.12) adds five independent defense +layers. Any one of the first three alone is sufficient to block the attack: -1. **`php_basic_early_rinit()`**: Called from `php_request_startup()` - AFTER `zend_activate()` but BEFORE `sapi_activate()`. Zeros - `BG(serialize_lock)`, `BG(serialize)`, and `BG(unserialize)` with - `memset`. Eliminates the stale state window entirely. +1. **`php_rinit_session_globals()` at `MULTIPART_EVENT_START`**: The + session RFC1867 callback now calls `php_rinit_session_globals()` at + the very start of multipart parsing, unconditionally zeroing `PS(id)`, + `PS(session_status)`, and `PS(send_cookie)`. This destroys the + dangling `PS(id)` from the previous request before B's parsing begins. + This single defense is sufficient to kill the entire exploit chain. -2. **Delayed OOM**: `zend_bailout()` on OOM is removed. The allocator - returns NULL (fallible mode) or succeeds beyond the soft limit - (non-fallible mode) and queues a delayed error. No longjmp, so all - cleanup code runs normally. A hard limit calls `_exit(137)`. +2. **Delayed OOM**: `zend_mm_safe_error` (which called `zend_bailout` / + longjmp) is replaced by `zend_mm_delayed_verror`. OOM no longer + triggers a longjmp during allocation. Instead, a soft limit records + the error and sets `vm_interrupt`. Allocations return NULL in fallible + mode or succeed up to a hard limit (default 200% of `memory_limit`). + A hard limit calls `_exit(137)`. No longjmp means all cleanup code + runs normally, so stale state never persists. -3. **Session handler hardening**: RFC1867 session decode runs in +3. **`post_max_size` enforcement in `multipart_buffer_fill()`**: A new + check in `fill_buffer()` enforces `post_max_size` against + `SG(read_post_bytes)` regardless of Transfer-Encoding. The chunked TE + bypass (which previously allowed sending 140MB despite 8M limit) is + blocked. OOM cannot be triggered via multipart data alone. + +4. **`php_basic_early_rinit()`**: Called from `php_request_startup()` + before `sapi_activate()`. Zeros `BG(serialize_lock)`, `BG(serialize)`, + and `BG(unserialize)`, eliminating the stale serialization state + window from CVE-2026-109618. + +5. **Session deserialization hardening**: RFC1867 session decode runs in `incomplete_classes_only` mode (no `__wakeup`/`__unserialize`). - `php_rinit_session_globals()` at `MULTIPART_EVENT_START`. - `$_SESSION` cleanup at `MULTIPART_EVENT_END`. + `$_SESSION` is deleted at `MULTIPART_EVENT_END`. A `PG(modules_activated)` + guard prevents `request_parse_body()` from corrupting session state. ## Test environment @@ -190,7 +206,8 @@ docker run -d --name stock -p 8093:80 php:8.4.26-apache The exploit has been tested on: - `php:8.4.26-apache` stock Docker image, 5/5 RCE (default settings) - `php:8.4.26-apache` with single worker + `memory_limit=8M`, 9/9 RCE -- `php:8.3.35-apache` stock Docker image (stages 1+2 confirmed, stage 3 calibrating) +- `php:8.3.35-apache` stock Docker image, RCE confirmed (first attempt) +- `php:8.4.27-apache` patched: all three exploit primitives blocked (see below) ## display_errors and WordPress @@ -219,6 +236,21 @@ exploit code path runs. The official `wordpress:*` Docker images ship a custom `error-logging.ini` that sets `display_errors = Off` at the INI level, which does block the leak. +**Why display_errors=Off cannot be bypassed**: The info leak requires +`SG(headers_sent) = 1` before `php_session_send_cookie()` runs during +`MULTIPART_EVENT_FILE_START`. The only code path that sets +`SG(headers_sent)` during multipart parsing is the error display path +(`php_error_cb` -> `zend_write` -> `sapi_module.ub_write`), which is +gated on `display_errors`. With `display_errors = Off`, warnings go to +the error log only, producing no output. There is no alternative +mechanism to force HTTP output during multipart parsing: the SAPI write +function is the only path, and it requires display_errors or explicit +script output (which has not started yet at this stage of request +processing). Precision OOM calibration (attempting to trigger OOM inside +the ~500-byte `php_session_send_cookie` window) was tested and fails +due to Zend MM bin-size boundaries causing ~1MB allocation jumps that +cannot be calibrated to the required precision. + ## References - CVE-2026-109618: OOM during multipart parsing triggers longjmp past session cleanup