
Flash CTF - Downtime
Downtime
[!IMPORTANT] Redacted write-up. The target runs a very recent, still-live n-day: a request-lifecycle state corruption in the PHP engine. The exact trigger, heap layout, offsets and primitives for this build are withheld and shown as [REDACTED]. The public material for the same bug family is summarised below, but none of it works against this target because it is calibrated for other builds. The full, unredacted write-up will be released as public exploitation information is more available to bad actors.
Overview
This is a web challenge that ends in remote code execution, but the application is empty: a single static 503 "maintenance" page with a few hard-coded lines, no forms, no uploads, no sessions and no user input. Nothing in the page is exploitable. The flaw is in the interpreter build serving it, an unpatched PHP 8.2.34 with a request-lifecycle state-corruption bug. The objective is to turn that into command execution as www-data and read the flag.
Background
PHP handles a request in two phases. At request startup (sapi_activate()) it parses the POST body, including multipart/form-data, and populates the superglobals and any session upload-progress state; the application script only runs afterwards. The allocator also enforces a memory_limit, and when an allocation would exceed it PHP raises a fatal out-of-memory and bails out via longjmp, unwinding straight to request shutdown instead of returning an error to its caller. Shutdown then frees the per-request structures. The flaw is that some of them can be half-built when the bailout fires, so shutdown trusts corrupt state. That class was fixed in PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 in the "Avoid state corruption via bailout" change; this challenge sits on the last vulnerable 8.2 release.
CVEGHSARoot causeFixed inCVE-2026-109618GHSA-w8hw-w9fx-j79wOOM bailout mid-request leaves partially-initialised request data; cleanup treats it as valid → memory corruption → RCE8.2.35 · 8.3.36 · 8.4.27 · 8.5.12CVE-2026-109619GHSA-6wmf-4rv5-p3rpRequest-owned global state survives a fatal error and is reused by a later request in the same worker → RCEsame patch set
The public work for this family, 9bie's php2shell-bypass-aslr-exp and the php2shell-check detector, demonstrates the mechanics end-to-end, but against PHP 8.4.26 on Debian 13 (trixie), Apache 2.4.69, on a page of <?php echo "OK\n"; ?>. It relies on constants such as zval_ptr_dtor = libphp + 0x65EB60, zif_system = libphp + 0x465370, S = main + 0xF8398 and PS(send_cookie) = ps_globals + 0x15E. None of those resolve on this build, and its heap shapes are tuned to its own page; the public session-rce variant additionally needs a file-write primitive to plant a session record and a gadget extension. The version-specific trigger, leak and shutdown primitive that work here are [REDACTED].
Reconnaissance
Only one thing answers, and it tells us most of what we need:
curl -si http://<host>/
HTTP/1.1 503 Service Unavailable
Server: Apache/2.4.68 (Debian)
X-Powered-By: PHP/8.2.34
Retry-After: 3600
The page body prints the same version again in its footer (Powered by PHP 8.2.34). There is no login, no parameter, no dynamic endpoint and no upload feature, so there is nothing to fuzz in the application. The attack has to target the runtime. PHP 8.2.34 being the release immediately before the state-corruption fix is the thread worth pulling.
Exploitation
With no application surface, the only lead is the version. Driving the family to code execution on this build runs through four stages, each [REDACTED] here:
- Precondition:
[REDACTED]across two requests, to leave the engine holding a piece of state the next request will trust. - Information leak:
[REDACTED], recovering the addresses needed to defeat ASLR (library base, call target and heap base) for the 8.2.34 build. - Memory shaping:
[REDACTED], placing attacker-controlled bytes in the location that request shutdown will follow. - Execution:
[REDACTED], turning the corrupted shutdown into a call of the attacker's command and staging a shell into the web root.
For orientation only: the public 8.4.26 chain does the equivalent with a header-flood OOM (to half-build a request structure), a forged-length Set-Cookie out-of-bounds read (to leak memory and defeat ASLR), and a fake HashTable whose destructor is pivoted at zif_system. As noted above, neither its shapes nor its offsets transfer to 8.2.34. Pointing them at this target merely aborts the request or crashes the worker.
Getting the Flag
The exploit lands code execution as the web user; the mechanism is [REDACTED]. The flag can then be read from /flag.txt.
Key Takeaways
- Patch the runtime as well as the application. This target had zero application attack surface and was still fully compromised through the interpreter.
- A fatal "out of memory" does more than deny service. The bailout path can leave internal state inconsistent in ways that later cleanup follows into memory corruption.
- A chunked request body sidesteps a frontend
post_max_size/Content-Lengthcheck, so limiting request size is not a defence against heap-shaping input. - Memory-corruption exploits of this class are build-specific: offsets and heap layouts have to be recalibrated for each PHP version, distribution and page.