Document five patch defense layers and display_errors=Off analysis

- Expand patch section: five independent defenses, not three
- Defense 1 (rinit_session_globals at MULTIPART_EVENT_START) alone
  is sufficient to block the entire exploit
- Document post_max_size enforcement that blocks chunked TE bypass
- Add display_errors=Off impossibility analysis with technical detail
- Confirm PHP 8.3.35 RCE working (not just stages 1+2)
- Note 8.4.27 tested and confirmed blocked
This commit is contained in:
neko
2026-10-22 19:26:20 +00:00
parent 1d0e9f67fe
commit fedef710e1
+46 -14
View File
@@ -159,23 +159,39 @@ python3 rce_exploit.py calibrate --docker-image php:8.3.35-apache \
## The patch ## The patch
The fix (PHP 8.3.36, 8.4.27, 8.5.12) adds three independent defense The fix (PHP 8.3.36, 8.4.27, 8.5.12) adds five independent defense
layers. Any one of them blocks the attack: layers. Any one of the first three alone is sufficient to block the attack:
1. **`php_basic_early_rinit()`**: Called from `php_request_startup()` 1. **`php_rinit_session_globals()` at `MULTIPART_EVENT_START`**: The
AFTER `zend_activate()` but BEFORE `sapi_activate()`. Zeros session RFC1867 callback now calls `php_rinit_session_globals()` at
`BG(serialize_lock)`, `BG(serialize)`, and `BG(unserialize)` with the very start of multipart parsing, unconditionally zeroing `PS(id)`,
`memset`. Eliminates the stale state window entirely. `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 2. **Delayed OOM**: `zend_mm_safe_error` (which called `zend_bailout` /
returns NULL (fallible mode) or succeeds beyond the soft limit longjmp) is replaced by `zend_mm_delayed_verror`. OOM no longer
(non-fallible mode) and queues a delayed error. No longjmp, so all triggers a longjmp during allocation. Instead, a soft limit records
cleanup code runs normally. A hard limit calls `_exit(137)`. 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`). `incomplete_classes_only` mode (no `__wakeup`/`__unserialize`).
`php_rinit_session_globals()` at `MULTIPART_EVENT_START`. `$_SESSION` is deleted at `MULTIPART_EVENT_END`. A `PG(modules_activated)`
`$_SESSION` cleanup at `MULTIPART_EVENT_END`. guard prevents `request_parse_body()` from corrupting session state.
## Test environment ## 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: The exploit has been tested on:
- `php:8.4.26-apache` stock Docker image, 5/5 RCE (default settings) - `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.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 ## 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 a custom `error-logging.ini` that sets `display_errors = Off` at the INI
level, which does block the leak. 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 ## References
- CVE-2026-109618: OOM during multipart parsing triggers longjmp past session cleanup - CVE-2026-109618: OOM during multipart parsing triggers longjmp past session cleanup