blog / · updated · 4 min read

PHP2Shell: two pre-auth RCEs in PHP itself, explained

CVE-2026-109618 and CVE-2026-109619 hit the PHP engine before your code runs, are exploited in the wild, and are in CISA's KEV. What they are, what to patch, temporary mitigations and what to look for in your logs.

vulnerabilitiesphphunting

In one paragraph

Two flaws in the PHP engine, nicknamed PHP2Shell, let an unauthenticated attacker run commands on a server just by sending a crafted HTTP request. They trigger while PHP parses the request, before your application code runs, so it does not matter which framework or CMS you use or how well it is written. Exploitation attempts were seen four days before disclosure. Both CVEs are rated 9.2 (CVSS v4.0) and are in CISA’s Known Exploited Vulnerabilities catalog.

What is affected

Every PHP installation that serves web requests: Apache mod_php, PHP-FPM, php-cgi, FrankenPHP and LiteSpeed. That includes PHP bundled inside other products, such as appliances, control panels and vendor images.

BranchFixed in
8.58.5.12
8.48.4.27
8.38.3.36
8.28.2.35
8.1 and olderVulnerable. Minimal source backports exist, but these branches are out of support.

Distributions often backport the fix without changing the PHP version string, so check your vendor’s advisory, not just php -v.

How the bugs work, simply

CVE-2026-109618 lives in the code that builds $_POST. When a request makes PHP run out of memory at exactly the wrong moment, the cleanup code treats half-built data as real. With careful heap grooming, leftover bytes become a fake callback that ends up calling system().

CVE-2026-109619 is a use-after-free across requests. A fatal error interrupts the reset of per-worker state, and a later request handled by the same worker follows a stale pointer. The confirmed paths use file uploads together with session upload progress, which is enabled by default and runs even if the application never calls session_start().

Timeline

Date (2026)Event
17 OctFirst exploitation attempts observed (AWS).
20 OctPHP discloses both vulnerabilities.
21 OctFixed releases. CISA alert. Broad scanning follows.
23 OctCISA publishes a Sigma rule.
26 OctBoth CVEs added to CISA KEV.

A third flaw, CVE-2026-115560, was disclosed days later. It also allows RCE but needs an unusual configuration, and it was still unpatched at the time of writing. Follow the PHP security advisories for it.

What to do

  1. Inventory every PHP runtime, including containers and products that embed PHP.
  2. Patch to the fixed versions, rebuild images and restart or replace every worker.
  3. If you cannot patch today, isolate internet-facing PHP or take it offline, as CISA recommends, and apply the temporary mitigations below.
  4. Hunt for signs of exploitation since 17 October.

Temporary mitigations

They reduce exposure but do not fix the bugs.

; php.ini or a file loaded before request parsing, then restart workers
session.upload_progress.enabled = 0   ; CVE-2026-109619 upload path
fatal_error_backtraces = 0           ; PHP 8.5 only, CVE-2026-109619

For CVE-2026-109618, Datadog reports that memory_limit = -1 set at administrator level blocks the known exploit path. That trades one risk for another, so treat it as a short-term measure. Also cap request body sizes at the reverse proxy or WAF:

client_max_body_size 10m;   # size it to your real uploads

Check what your settings really are on each pool:

php -v
php -i | grep -E 'memory_limit|session.upload_progress.enabled|fatal_error_backtraces'

What to look for

Memory exhaustion before any script runs. “Unknown on line 0” means no script had started:

grep -E 'Allowed memory size of [0-9]+ bytes exhausted.*Unknown on line 0' \
  /var/log/php*-fpm.log /var/log/apache2/error.log /var/log/httpd/error_log 2>/dev/null

Worker crashes:

grep -E 'exit signal Segmentation fault|SIGSEGV' \
  /var/log/apache2/error.log /var/log/php*-fpm.log 2>/dev/null

Suspicious requests (needs body logging, a WAF or a proxy that can see the request body):

  • Multipart POSTs with PHPSESSID and PHP_SESSION_UPLOAD_PROGRESS, often with a file part.
  • Upload filenames containing <?php or |O:.
  • Deeply nested field names such as files[R00001][a][a][a], repeated across hundreds of parts.
  • application/x-www-form-urlencoded POSTs with a single field name of several megabytes.
  • Set-Cookie: PHPSESSID= with long, percent-encoded or binary values.

The payoff: a web server spawning a shell. Microsoft Defender for Endpoint (KQL):

DeviceProcessEvents
| where Timestamp > datetime(2026-10-17)
| where InitiatingProcessFileName in~ ("php-fpm", "php", "php-cgi", "httpd", "apache2", "lsphp", "frankenphp")
   or InitiatingProcessFileName startswith "php-fpm"
| where FileName in~ ("sh", "bash", "dash", "curl", "wget", "nc", "ncat", "python3", "perl", "base64")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine, AccountName

On Linux without EDR, auditd or osquery can answer the same question:

-- osquery: shells whose parent is a PHP or web server process
SELECT p.pid, p.name, p.cmdline, pp.name AS parent
FROM processes p JOIN processes pp ON p.parent = pp.pid
WHERE pp.name IN ('php-fpm', 'php-fpm8.3', 'php-fpm8.4', 'apache2', 'httpd', 'php-cgi')
  AND p.name IN ('sh', 'bash', 'dash', 'curl', 'wget', 'nc', 'python3', 'perl');

Any single signal can be noise, since memory errors happen in normal workloads. Look for them together: memory exhaustion, then a worker crash, then a shell.

Sources

← All posts