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:
+46
-14
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user