CSC 472 Software Security

2026-Fall Course Website

Special Topic · after Heap Overflow / UAF

PHP2Shell — CVE-2026-109618 & -109619

Two critical unauthenticated-RCE bugs in core PHP (Oct 2026): the same memory-corruption classes we study in this course, in the runtime behind a large share of the web.

CVEs2026-109618 & 2026-109619
SeverityCVSS v4.0 9.2 (Critical)
AffectedPHP 5.4 → 8.5.11
Fixed in8.2.35 / 8.3.36 / 8.4.27 / 8.5.12

Why this is a special topic, not a numbered class

Everything in PHP2Shell is a bug class you already know from this course — just inside the runtime behind roughly a fifth of the public web, with about 22.6 million internet-exposed instances potentially vulnerable. To follow the root cause you need two ideas we cover in Week 11:

  • Use-after-free — reusing memory after it has been freed (the heart of CVE-2026-109619)
  • Heap grooming — arranging allocations so attacker bytes land in a reclaimed slot
PrerequisiteIf you have not yet done the heap overflow / UAF class, park this page until you have. The exploitation reasoning will not land otherwise.

The two vulnerabilities

CVE-2026-109618 RCE

RCE via OOM corruption. Out-of-memory handling can interrupt PHP while request data is only partially built. Request cleanup then treats that invalid data as valid.

Primary CWE: 824 (uninitialized pointer) / 1419

CVE-2026-109619 RCE

RCE via stale global state. Request-owned state outlives the memory it points to; a later request on the same worker follows the dangling pointer before init resets it.

Primary CWE: 416 (use-after-free)

The key difference from our lab binariesOur targets start fresh and exit. A PHP worker (Apache mod_php or PHP-FPM child) is long-lived and serves request after request. So a bug triggered by request A can be used by a later request B or C on the same worker — a "producer" request leaves bad state, a "consumer" request trips over it.

Root cause 1 — OOM corruption (CVE-2026-109618)

When PHP hits a fatal error — most importantly running out of memory against memory_limit — it does not unwind cleanly. It calls zend_bailout(), which is a longjmp(): a jump across functions straight to request shutdown, skipping any local cleanup on the way out.

  1. Non-atomic updatePHP bumps a counter or length before the new entry is fully written into a core structure.
  2. OOM in the gapAn allocation inside that window fails; zend_bailout() longjmps out. The entry is never finished, but the count already says it exists.
  3. Shutdown trusts itTeardown walks the structure and treats the half-built slot as a valid object — access of an uninitialized pointer.
  4. Attacker-controlled bytesWith heap grooming the uninitialized slot holds attacker data → a corruption primitive → RCE as the web-server user.
The trick to reach OOMCrafted multipart/form-data (RFC 1867) bodies build huge internal structures that do not count against upload limits — so a request under post_max_size can still exhaust memory_limit. The vulnerable code path dates back to PHP 5.4.0.

Root cause 2 — use-after-free across requests (CVE-2026-109619)

The advisory frames it as producers and consumers of stale request-owned state. A fatal error creates the dangling pointer; an operation early in a later request dereferences it.

Producers (leave bad state)

A fatal error abandons an operation before its reset runs: parse/compile failure, failed require, uncaught Error, out-of-memory, or "Maximum execution time exceeded." Leftover error / session / object / extension globals now point to freed memory.

Consumers (use it too early)

Anything that runs before normal request init resets those globals: any INI diagnostic (even a suppressed warning), mod_php .htaccess php_value directives, or session upload-progress — which is on by default and needs no diagnostic at all.

Why "@", display_errors=Off and error_reporting do not helpThe suppressed warning is still generated — and generating it is what touches the dangling pointer. Hiding the output changes nothing.

Confirmed instances (from the PHP advisories and Wiz Research):

Internal symbolWhat goes wrongResult
EG(last_fatal_error_backtrace)PHP 8.5 keeps the last fatal error's backtrace in an executor global; OOM frees the heap but it is never reset.basis of the confirmed mod_php RCE
PS(id) / upload-progressRFC 1867 parsing starts session state early and sets PS(id) before module init; OOM frees the heap while it still points in.leaks memory into the PHPSESSID cookie
BG(unserialize)Upload-progress decoding leaves unserializer bookkeeping published after a failed allocation destroys the heap.stale decoder state → UAF
OPcache: CG(map_ptr_last)Script persistence advances the process-local pointer-map frontier before publishing the shared one.type confusion across workers

A teachable primitive: leaking memory into a cookie

The cleanest demo of CVE-2026-109619 needs no full RCE chain — just an information leak, exactly like the GOT leak we used to defeat ASLR:

  1. PrimeSend a multipart POST that triggers upload-progress; PS(id) points into the request heap.
  2. FreeForce OOM in the same request; zend_bailout longjmps out, the heap is freed, PS(id) is left dangling.
  3. ReuseSend a second multipart request to the same worker; cleanup runs before session init resets the pointer.
  4. LeakPHP trusts the stale pointer + length as a live string and URL-encodes its bytes into the response cookie.
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=%00%7f%45%4c%46%02%01...
                      ^ freed heap bytes, served straight back to the attacker

No session_start() in the application is required — the path runs before application code.

How PHP2Shell maps onto what you already know

Concept we taughtThe ideaWhere it shows up here
Buffer overflow (Lab 1–2)Corrupt memory you should not be able to write-109618 writes past a half-built structure
Uninitialized / stale dataTrusting memory before it is validboth CVEs: read before init / after free
Use-after-free (Week 11)Free, then reuse the dangling pointer-109619 is textbook UAF across requests
Info leak → ASLR defeat (ret2PLT/GOT)Leak an address, then aim the exploitthe PS(id) memory disclosure above
ret2libc / PLT–GOTReuse code already mapped executablehow you escalate the primitive to code execution
Heap grooming (Week 11)Arrange the allocator, overlap freed objectshow attacker bytes land in the stale slot

The fix and what to do

The patch stops using longjmp() for OOM (a deferred "soft" OOM at the next safe point, plus an immediate _exit() "hard" OOM at a higher threshold) and resets the wider set of request-owned globals before the request heap is released. Updating is the only complete fix.

Fixed versions: 8.2.35 8.3.36 8.4.27 8.5.12. PHP 8.1 and earlier are end-of-life — no official release (minimal source backports only for 7.4 / 8.0 / 8.1).

Stopgaps only (not a substitute for patching)If you genuinely cannot patch yet:
php_admin_value[memory_limit] = -1   ; apps can't override; removes the OOM trigger
session.upload_progress.enabled = 0  ; removes the default early consumer
fatal_error_backtraces = 0           ; PHP 8.5 only

Also bound request inputs at the edge (decoded size, field count, multipart parts, headers, nesting), avoid RLIMIT_AS/RLIMIT_DATA, restart every worker after any change, and isolate internet-facing apps until patched. These block only the confirmed paths — other variants may exist.

How the internet reacted (Oct 20–21)

The same event at four altitudes — the disclosure, a national government, regional panic, and the ops reality. Non-English posts translated to English.

@sagitz_ · Wiz Research (the discloser)"Today we disclose PHP2Shell — remotely exploitable memory corruptions in PHP. WordPress pre-auth RCE ✓ Drupal pre-auth RCE ✓ Joomla pre-auth RCE ✓ Remote memory disclosure of any PHP application ✓"
@takaichi_sanae · translated from Japanese"An urgent request for everyone to act on tonight. Regarding PHP, used by many websites, the PHP security team is warning of two serious vulnerabilities. Please check whether your public-facing sites use PHP and prepare to apply, as quickly as possible, the fix released tonight at 11:00 PM JST on October 21. If applying it promptly is difficult, temporarily taking the site offline can also be an effective way to reduce risk."
@nyxvoid92 · translated from Chinese"10 PM tonight, PHP servers worldwide are bracing for a battle royale. Two RCE bugs straight to the core leaked early; rumor has it one unauthenticated request gets you a shell. The worst part: PHP 8.1 and below get no patch at all, and up to 59% of public-facing hosts worldwide still run old versions. When the patch drops at 22:00, it's basically the start of the war — the whole internet gets scanned within 15 minutes. Companies still running ancient PHP 7 / PHP 5 code now have nothing left but to pull the network cable and lock the WAF. Good luck."
@IT_unhinged · the ops reality (humor)"Security called me at 6:12 AM: 'Patch every PHP server immediately.' Two critical vulnerabilities, RCE, no auth required. How many PHP servers do we have? According to inventory: 12. According to reality: nobody knows. We found one running WordPress. Three running old internal tools. One under someone's desk."
Teaching pointDisclosure travels in minutes; patching travels in weeks — because almost nobody has a complete, accurate asset inventory. That gap is where real-world compromise lives.

Slides and sources

Credits: Calif.io (LCFR), Wiz Research, OpenAI; remediation by arnaud-lb, iluuu1994, bukka, ndossche.

Last updated 2026-10-22 · CSC 472 home