Blog

PHP2Shell, explained.

Two PHP memory bugs, from the first web request to possible code execution — and how governments and critical services held the line in the 23 hours before a fix existed. A readable guide to the affected systems, the internals that matter, and the evidence defenders can look…

Two PHP memory bugs, from the first web request to possible code execution — and how governments and critical services held the line in the 23 hours before a fix existed. A readable guide to the affected systems, the internals that matter, and the evidence defenders can look for.

Background

PHP2Shell is two critical flaws in PHP’s runtime — CVE-2026-109618 and CVE-2026-109619 — that can let an unauthenticated visitor run code on the server, before the website’s own code even starts.

Affected

Every PHP install serving web requests — mod_php, PHP-FPM and others

Fixed in

8.5.12 · 8.4.27 · 8.3.36 · 8.2.35

No official fix

PHP 8.1 and older (end of life)

How common PHP is — down to governments

PHP runs 69.2% of websites whose server-side language is known, and 59.3% of the 1,000 most popular. VulnCheck counted about 2.6 million internet-facing PHP hosts, roughly 42% on end-of-life versions.

PHP’s share of websites with a known server-side language
All websites69.2%
Top 10,00059.5%
Top 1,00059.3%
Next: JavaScript7.2%

W3Techs, 25 October 2026 (JavaScript: late August 2026).

The public sector is no exception. Government portals lean on PHP systems like Drupal and WordPress, and CISA warned that much of PHP’s footprint may not be directly observable. On 21 October, governments and critical services took sites offline rather than stay exposed:

Dutch NCSC & parliamentFinland’s central governmentFrench prefecturesGerman federal sites32 UK government servicescyber.gov.auCharité hospitalPort of RotterdamMUFG bankWikimedia systems

The 23 hours without a patch

The CVE IDs leaked around 15:00 UTC on 20 October; PHP’s fix shipped at 14:00 UTC the next day. Attackers were already probing.

Times in UTC
Attacker activityDefender actionFix available
17 OctFirst exploitation attemptRecorded by Amazon, four days before any fix.
No installable patch · ≈ 23 h
20 Oct · 15:00CVE identifiers leak
21 Oct · 03:02Cloudflare WAF rules on by default
21 Oct · 08:37Mass scanning begins
21 Oct · pre-fixGovernments take sites offline
21 Oct · 14:00PHP ships the fix

How organizations handled the gap

Complete · costs uptime

Take it offline

The only full protection before the fix. Cyber agencies went first — the Dutch NCSC pulled its own site.

Partial · stopgap

Shield at the edge

WAF rules blocked known exploits, but traffic reaching the origin directly bypassed them.

Partial

Harden the config

Disable session upload progress and cap request size at the proxy. Each closes only some attack paths.

Preparation

Find every PHP

Inventory PHP inside CMSs, panels and appliances so the patch could roll out the moment it landed.

Understanding PHP

When someone opens a PHP website, the web server hands the request to PHP. PHP reads the input, runs the site’s code and sends a page back.

A request’s path through a PHP website

Browser

Sends a request

→

Web server

Passes it to PHP

→

PHP

Reads input, runs code

→

Response

A page comes back

Workers handle one visitor after another

A worker is a running process that handles requests — think of a waiter who serves one table, clears it, then seats the next guest. The same worker serves many visitors in a row. With mod_php, PHP runs inside Apache’s own workers; with PHP-FPM, PHP keeps its own pool of workers.

The cleanup rule

While handling a request, PHP borrows memory and keeps notes of where things are — pointers. A few notes live in the worker itself and outlast the request; we call these worker globals. Before the next guest sits down, PHP must erase those notes and hand the memory back.

Clearing the table between requests

Use memory

Notes point to it

→

Erase the notes

No old addresses left

→

Release memory

Next request starts clean

A note that survives points at memory now owned by someone else — a dangling pointer.

When PHP runs out of memory mid-task

Each request gets a memory budget, memory_limit. If a request goes over it, vulnerable PHP drops what it was doing on the spot — a bailout — and skips the remaining steps. Both PHP2Shell bugs start here: one leaves half-built data behind, the other leaves an old note pointing at freed memory.

CVE-2026-109618: a half-built entry

An attacker makes PHP run out of memory while it’s in the middle of building a list. PHP abandons the job, but its cleanup later trusts the half-finished entry — and that opening can be turned into code execution.

1. A small request, a big memory bill

The attacker sends a POST made of many tiny upload parts with deeply nested names. Each part is almost empty, but PHP builds internal arrays and strings to describe them — so the bookkeeping costs far more memory than the bytes on the wire. A request well under the post_max_size cap can still blow past the memory_limit cap and trigger an out-of-memory bailout.

A small body creates a large memory bill

Tiny POST

Many nested upload parts

→

PHP describes each one

Arrays, entries, strings

→

Over the limit

Out-of-memory bailout

2. The crash lands mid-entry

PHP builds a list by first bumping its count, then filling in the new slot (its bucket). The bailout can hit in between: the list says it holds three entries, but only two are real. The third slot still holds leftover bytes from earlier memory use.

The count is ahead of what’s actually built
Recorded entries: 3Fully built: 2
Slot 1name → MayaReady ✓
Slot 2city → HaifaReady ✓
Slot 3Leftover bytesNever filled in !

3. Cleanup trusts the count

PHP still runs cleanup on the failed request. Cleanup believes the count of three, walks to the unfinished slot and treats its leftover bytes as real internal data — including what it thinks are memory addresses. Following those false addresses lets PHP read or write the wrong memory. That’s the memory corruption.

How that becomes code execution

By choosing the request carefully, an attacker controls the leftover bytes in that slot — so the false “addresses” point where they want. With enough control they overwrite the value that decides what PHP runs next, redirecting the worker into their own code. Reaching out-of-memory alone isn’t enough; a plain attempt may just crash. This route has been demonstrated end-to-end on Apache mod_php, and the code runs with the worker’s permissions.

In one line: structured POST → excessive memory use → out-of-memory mid-build → cleanup trusts the unfinished entry → controlled corruption → remote code execution.

CVE-2026-109619: a note left behind

Here a worker keeps a note pointing at one request’s memory, a crash skips the step that would erase it, and a later request follows the stale note after that memory has been reused. The pattern: one request leaves state behind, a second request trips over it.

1. Request A leaves a note

PHP’s session upload-progress feature — on by default — tracks how much of an upload has arrived, and it sets up early, while PHP is still parsing the request. That leaves a worker global pointing into request A’s memory. Call it a note that says slot 7.

Worker 1 · same running process

Stored note

→ slot 7

→

Slot 7

Request A’s data

2. A crash skips the erase

A fatal error hits before PHP finishes setting up. The general cleanup frees request A’s memory, but the step that should erase the upload-progress note is skipped. The worker lives on, holding a note that now points at freed — reusable — memory. A dangling pointer.

The data is gone; the note remains

Stored note

→ slot 7 · dangling

→

Slot 7

Free / reusable

3. Request B reuses the slot

The next request to reach the same worker reads that stale note before normal setup resets it — meanwhile fresh data has moved into slot 7. PHP trusts whatever is there now as its own bookkeeping. This is a use-after-free, and it’s the memory-corruption stage.

Same address, different meaning

Stored note

→ slot 7 · old reference

→

Slot 7

Request B’s data

How that becomes code execution

If the attacker gets their own bytes into the reused slot, PHP reads them as trusted counts and addresses. With enough control they steer what PHP runs next, into their own code — again with the worker’s permissions. It depends on the PHP build and on reaching the same worker, so a crash plus one more request isn’t enough on its own.

One flaw, several routes

Upload-progress is the clearest path, but the same stale state survives a crash pattern also shows up via PHP’s unserializer, fatal-error backtraces and OPcache. On PHP-FPM the startup route doesn’t apply — it stops the worker on a startup failure — but a crash after startup can still leave a reusable worker.

From a POST request to exploitation

A POST request walked through to exploitation.

How you stay protected

Patch, mitigate until the patch lands, then hunt for anyone who got in first.

1. Patch and restart

  1. Install a fixed build — 8.5.12, 8.4.27, 8.3.36, 8.2.35 or a vendor backport.
  2. Restart every worker and container — running processes keep the old code.
  3. Check the PHP that serves the site — command-line PHP may be a different install.

2. Mitigate until patched

Set in server config (not ini_set()) and restart workers. Partial — buys time, doesn’t replace the patch.

PHP configuration · partial coverage

; Apply in startup/server configuration, then restart workers.
session.upload_progress.enabled = 0

; PHP 8.5: mitigation for the confirmed backtrace variant.
fatal_error_backtraces = 0

Also keep WAF rules on and cap request size at the proxy. Leave upload progress off until the next bugfix release (related: CVE-2026-115560).

3. Hunt for exploitation

A hit is a lead, not proof — baseline before alerting.

A web server spawns a shell or recon tool

KQL · Defender XDR · DeviceProcessEvents

// Rule authors: Florian Roth, Nasreddine Bencherchali (Nextron Systems)
DeviceProcessEvents
| where ((InitiatingProcessFolderPath endswith "/apache" or InitiatingProcessFolderPath endswith "/apache2" or InitiatingProcessFolderPath endswith "/caddy"
          or InitiatingProcessFolderPath endswith "/gunicorn" or InitiatingProcessFolderPath endswith "/httpd"
          or InitiatingProcessFolderPath endswith "/lighttpd" or InitiatingProcessFolderPath endswith "/nginx"
          or InitiatingProcessFolderPath endswith "/node" or InitiatingProcessFolderPath endswith "/nodejs"
          or InitiatingProcessFolderPath endswith "/passenger" or InitiatingProcessFolderPath endswith "/php-cgi"
          or InitiatingProcessFolderPath endswith "/php-fpm" or InitiatingProcessFolderPath endswith "/tomcat"
          or InitiatingProcessFolderPath endswith "/unicorn" or InitiatingProcessFolderPath endswith "/uwsgi")
        or (InitiatingProcessCommandLine contains "/bin/java" and InitiatingProcessCommandLine contains "tomcat")
        or (InitiatingProcessCommandLine contains "/bin/java" and InitiatingProcessCommandLine contains "websphere"))
  and (((FolderPath endswith "/ash" or FolderPath endswith "/bash" or FolderPath endswith "/busybox" or FolderPath endswith "/csh" or FolderPath endswith "/dash"
          or FolderPath endswith "/ksh" or FolderPath endswith "/sh" or FolderPath endswith "/tcsh" or FolderPath endswith "/zsh")
         and (ProcessCommandLine contains "-c \"id" or ProcessCommandLine contains ";id" or ProcessCommandLine contains "cat /etc/"
          or ProcessCommandLine contains "crontab" or ProcessCommandLine contains "curl " or ProcessCommandLine contains "hostname"
          or ProcessCommandLine contains "id;" or ProcessCommandLine contains "id\"" or ProcessCommandLine contains "ifconfig"
          or ProcessCommandLine contains "iptables" or ProcessCommandLine contains "perl" or ProcessCommandLine contains "python"
          or ProcessCommandLine contains "ruby" or ProcessCommandLine contains "uname" or ProcessCommandLine contains "wget "
          or ProcessCommandLine contains "whoami"))
       or ((FolderPath endswith "/bin/cat" or FolderPath endswith "/bin/crontab" or FolderPath endswith "/uname" or FolderPath endswith "/curl"
          or FolderPath endswith "/hostname" or FolderPath endswith "/ifconfig" or FolderPath endswith "/ip" or FolderPath endswith "/iptables"
          or FolderPath endswith "/netstat" or FolderPath endswith "/pwd" or FolderPath endswith "/route" or FolderPath endswith "/wget"
          or FolderPath endswith "/whoami" or FolderPath endswith "/perl" or FolderPath endswith "/ruby") or FolderPath contains "/python"))
  and (not((InitiatingProcessFolderPath endswith "/node" and ProcessCommandLine contains "ip neigh show")))

PHP memory-corruption error patterns

KQL · Sentinel · web / PHP error log

Syslog
| where TimeGenerated > ago(7d)
// Rule author: Swachchhanda Shrawan Poudel (Nextron Systems). Point at your web/PHP error log.
| where (SyslogMessage has "PHP Fatal error" and SyslogMessage has "Allowed memory size of"
          and SyslogMessage has "bytes exhausted"
          and (SyslogMessage has "rfc1867.c" or SyslogMessage has "in Unknown on line"))
      or (SyslogMessage has "PHP Warning" and SyslogMessage has "open("
          and SyslogMessage has "/ndary=" and SyslogMessage has "sess_" and SyslogMessage has "failed")
| project TimeGenerated, Computer, ProcessName, SyslogMessage

Apache worker segmentation faults

KQL · Sentinel · Apache error log

Syslog
| where TimeGenerated > ago(7d)
// Rule author: Florian Roth (Nextron Systems). Point at the Apache error log.
| where SyslogMessage has "exit signal Segmentation fault"
| project TimeGenerated, Computer, ProcessName, SyslogMessage

4. With us, you are protected

Two Conifers platforms work together: threat hunting finds the attack in your data, and detection engineering turns an emerging attack into a detection rule — and when it fires, our agent investigates and protects. Neither waits for a CVE.

How Conifers keeps you protected
1
Threat intelligenceSpots emerging threats
CampaignsCVEsIOCs
2
Threat huntingFinds them in your environment
HuntInvestigateVerdict
3
Detection engineeringDetects it in real time
RuleAlertActivate

ResultProtected and alerted — now and next time.

Threat intelligence

Spots the threat early

  1. Recognizes emerging threats and campaignsContinuously monitors advisories, vendor research and threat feeds to identify new threats and malicious campaigns as they surface.
  2. Extracts what mattersTurns each report into campaigns, CVEs, indicators and ATT&CK-mapped behaviors.
  3. Scores it for youMatches the threat to your environment — internet-facing PHP servers make PHP2Shell a priority.
  4. Keeps indicators freshRe-checks indicators for as long as they stay useful, so late hits still surface.

Threat hunting

Finds the behavior in your data

  1. Knows where to lookMaps your internet-facing PHP servers and which ones send process and Syslog telemetry.
  2. Hunts behavior, not just indicatorsATT&CK-mapped behavior queries run on your Sentinel — a web server spawning a shell, PHP crash bursts. No CVE needed.
  3. Hunts new indicatorsAs threat intelligence publishes PHP2Shell scanner IPs, user-agents and CVEs, they’re hunted and enriched automatically.
  4. Connects the hitsThe hunt agent pivots on the server — probe → crash → shell → outbound — and returns a verdict.

Detection engineering

From emerging attack to protection

  1. Recognizes the upcoming attackSpots the new technique early — a web server crashing under odd uploads, then spawning a shell.
  2. Generates a detection ruleWrites a rule for that behavior, built for whichever SIEM or EDR you run.
  3. The rule alertsIt fires the moment an exploitation attempt hits one of your servers.
  4. Our agent investigates and protectsChecks whether the attempt succeeded, then contains it — isolate the server, block the source, preserve evidence.

Before the CVE

We catch the attack by its behavior and have a detection live — before it even has a name.

After the CVE

We hunt the new indicators back through your logs, and every alert activates our agent to protect you.

The practical takeaway

Patch, restart, verify. Then hunt behavior and turn it into a rule — a crash burst followed by a shell on one server is an attack, CVE or not.

Sources

Current to 25 October 2026. The list of organizations offline comes from press reports.

← Back to Resources
See it live

Watch an agent investigate a real alert.

CognitiveSOC™ runs the investigation end-to-end on top of your existing SIEM, SOAR and XDR, and shows its work.