Two unauthenticated RCE flaws in the PHP engine — CVE-2026-109618 / CVE-2026-109619, CVSS 4.0 9.2 — leaked ahead of coordinated disclosure, patched in an emergency release October 21. Cloudflare has observed millions of exploitation attempts, some delivering the Mirai-derived Potassium botnet; watchTowr has observed in-the-wild exploitation of both CVEs. As of Oct 26 exploitation is confirmed working, not just scanning: researchers took over an Apache mod_php honeypot, a European government entity was compromised with agentic-AI post-exploitation, and Mirai operators plus ASLR-bypass exploits have joined. CISA listed both CVEs in KEV on Oct 26 (federal remediation due Oct 29, forensic-triage flag set); on Oct 27 industrial vendors — Siemens, Schneider Electric, ABB, Johnson Controls, Belden, HMS, Wibu-Systems — confirmed PHP-based OT/ICS products affected, Belden NetModule NRSW without a patch.
Verdict: patch outside the normal cycle, and hunt while you patch. Unauthenticated RCE in the engine that runs most of the web, with exploitation observed in the wild and mass probing underway — this does not wait for the monthly window. Successful compromises are now confirmed (Oct 24–26) — treat unpatched internet-facing PHP as presumed targeted, and hunt back to Oct 20 when the details leaked. PHP 7.4 / 8.0 / 8.1 get no binary release — official source-backport branches and distro packages are the only routes, and migration is the real fix. The PHP team published official interim mitigations (below) for installations that cannot patch today. Hunting behaviors are published and concrete, which makes this one of the rare patch events that is also immediately huntable.
Both flaws are use-after-frees in PHP core (session upload-progress handling and fatal-error paths), reachable without authentication. Observed exploitation recycles PHP workers, leaks memory through PHPSESSID cookies, and abuses session upload-progress handling to run shell commands; the observed exploit targets Apache mod_php, and exploitation is easier where session upload-progress is enabled. Some attempts fetch a Potassium loader from 92.118.112[.]100.
| Line | Upgrade to | Note |
|---|---|---|
| 8.5 | 8.5.12 | supported lines, all fixed in the emergency release |
| 8.4 | 8.4.27 | |
| 8.3 | 8.3.36 | |
| 8.2 | 8.2.35 | |
| 8.1 / 8.0 / 7.4 | no binary release | EOL — official minimal source-backport branches exist on github.com/php/php-src (PHP-7.4-CVE-2026-109618-109619 etc.) for build-from-source; otherwise distro/vendor backports (e.g. Plesk shipped one). Affected code reaches back to PHP 5.4 |
| 8.2 itself reaches EOL December 31, 2026 — patching to 8.2.35 buys ten weeks, not a destination. Linux distros and LTS vendors received patches in advance; check vendor packages. | ||
From the PHP team's advisory: these reduce exposure to the confirmed paths but "do not make an installation safe" — patching is the fix.
# CVE-2026-109619 (stale global state) — blocks the confirmed session path:
session.upload_progress.enabled=0 ; php.ini / FPM pool / server config,
; NOT ini_set() or .user.ini — then restart every worker
fatal_error_backtraces=0 ; PHP 8.5 only, blocks the fatal-backtrace path
# CVE-2026-109618 (OOM corruption) — starve the allocation-failure primitive:
memory_limit=-1 ; via php_admin_value so apps cannot override
post_max_size = 8M ; (32M if uploads needed); upload_max_filesize below it
# plus web-server/proxy limits on decoded size, field/part/header counts, nesting —
# enforced even without a usable Content-Length
# availability boundary: container/cgroup limit killing only the worker.
# Do NOT use RLIMIT_AS / RLIMIT_DATA — they can trigger the vulnerable allocation failure.
PHPSESSID response cookies (memory-leak primitive)php-fpm, php-cgi, httpd, apache2) spawning shells, interpreters, or download tools; new webshells, cron jobs, accounts| Category | Vendor | Status |
|---|---|---|
| Vendor | PHP project | Emergency release + mitigations — fixed versions Oct 21 14:00 UTC; official interim mitigations published (section below); minimal source-backport branches for EOL 7.4/8.0/8.1; distros and LTS vendors received patches in advance; GHSA advisories for -109618 / -109619; follow-up advisory CVE-2026-115560 issued Oct 23 |
| VM scanners | Tenable | FAQ published for both CVEs; plugin IDs not confirmed in public reporting — check the plugin feed for the version checks |
| Qualys / Rapid7 | Nothing published found — verify QID / InsightVM content directly | |
| Hunting content | Huntress / watchTowr / Wiz | Published — Huntress process-spawn guidance (Florian Roth's process-watcher for Linux without EDR) with Sigma/YARA promised to huntresslabs/threat-intel; watchTowr reproduction + ITW reporting; Wiz behavior write-up (the hunt list above) |
| WAF / edge | Cloudflare | Rules live since Oct 21 — deployed ~9 hours before public disclosure, enabled by default on all plans including Free; Oct 22 emergency release renamed seven rules to carry the CVE IDs. Paid plans: confirm the Managed Ruleset's PHP2Shell rules run in Block. Cloudflare reports attackers already mutating payloads to evade — WAF is cover, not the fix |
| Akamai | Rule shipped — Adaptive Security Engine 3000991 v1 "PHP Remote Code Execution Attack Detected" covers both CVEs; existing command-injection rules catch weaponized payloads. Verify active and blocking on every hostname — default status not stated | |
| Emerging Threats | IDS signatures published for both CVEs (Suricata/Snort rule coverage thread) | |
| Exposure | Censys | Live advisory with exposure search for internet-facing PHP |
| Vendor impact | Palo Alto | Statement: not aware of exploitation against PAN products |
| KEV | CISA | Listed Oct 26 — both CVEs (catalog entries: "PHP Memory Corruption" / "PHP Stale Internal Request-State"); federal due date Oct 29 under BOD 26-04; forensic-triage flag: Yes (CISA expects triage of exposed hosts, not patch-and-forget); ransomware use: unknown. CISA's Oct 21 alert (revised Oct 23 and 26) adds a Sigma rule and says organizations that cannot patch, or still run PHP ≤8.1, should take PHP systems offline until patched |
The behaviors above are published (Wiz, Huntress) but no vendor has shipped ready-made queries. These translate the published behaviors into generic query logic — tune thresholds to your traffic.
Exploit attempt shape — multipart request then memory error, from the same source:
# web / LB logs multipart POST (content-type contains multipart/form-data) followed within 60s by status 500 from same src_ip | count by src_ip over 10m → investigate bursts; mass probing will be loud
Memory-leak primitive — the PHPSESSID leak is visible in responses:
# proxy / web logs with response headers response Set-Cookie contains PHPSESSID AND cookie length > 1KB → near-zero false positives; any hit is an exploitation attempt that got traction
Post-exploitation lineage — the published Huntress behavior as a query:
# EDR process lineage parent.name IN (php-fpm, php-cgi, apache2, httpd) child.name IN (sh, bash, curl, wget, perl, python*) also: php-fpm "child exited on signal" bursts in syslog (worker churn)
Splunk (index=*) / Elastic ES|QL (FROM logs-*) — CIM Web / Endpoint field names for Splunk, ECS for Elastic, Sysmon names for lineage; tune to your add-on or integration:
# exploit attempt shape: multipart POST then HTTP 500 from the same source inside 60 s
index=* earliest=-30d@d ((http_method=POST http_content_type="multipart/form-data*") OR status=500)
| transaction src maxspan=60s
| search http_method=POST status=500
| stats count as pairs min(_time) as first max(_time) as last values(uri_path) as paths by src
| sort - pairs
# memory-leak primitive: oversized PHPSESSID in responses
index=* earliest=-30d@d "Set-Cookie" "PHPSESSID"
| regex _raw="PHPSESSID=[^;\s]{1024,}"
| stats count by src dest uri_path
# post-exploitation lineage (Sysmon EID 1 / EDR)
index=* earliest=-30d@d ParentImage IN ("*/php-fpm*","*/php-cgi*","*/apache2","*/httpd")
Image IN ("*/sh","*/bash","*/dash","*/curl","*/wget","*/perl","*/python*")
| stats count values(CommandLine) as cmds min(_time) as first by host ParentImage Image
# worker churn (syslog)
index=* earliest=-30d@d "php-fpm" "child" "exited on signal"
| timechart span=10m count by host
# campaign infrastructure (Potassium loader)
index=* earliest=-30d@d (dest=92.118.112.100 OR src=92.118.112.100 OR url="*92.118.112.100*")
| stats count min(_time) as first by host src dest
// exploit attempt shape: multipart POST then HTTP 500 from the same source inside the same minute
// (ES|QL has no transaction; one-minute buckets approximate the 60 s window)
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE (http.request.method == "POST" AND http.request.mime_type LIKE "multipart/form-data*") OR http.response.status_code == 500
| EVAL minute = DATE_TRUNC(1 minute, @timestamp)
| STATS posts = COUNT(*) WHERE http.request.method == "POST", errors = COUNT(*) WHERE http.response.status_code == 500, paths = VALUES(url.path) BY source.ip, minute
| WHERE posts > 0 AND errors > 0
| STATS pairs = COUNT(*), first = MIN(minute), last = MAX(minute), paths = VALUES(paths) BY source.ip
| SORT pairs DESC
// memory-leak primitive: oversized PHPSESSID in responses
FROM logs-*
| WHERE @timestamp > NOW() - 30 days AND message LIKE "*Set-Cookie*PHPSESSID*"
| WHERE message RLIKE ".*PHPSESSID=[^;\\s]{1024,}.*"
| STATS count = COUNT(*) BY source.ip, destination.ip, url.path
// post-exploitation lineage (endpoint process events)
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE (process.parent.name LIKE "php-fpm*" OR process.parent.name LIKE "php-cgi*" OR process.parent.name IN ("apache2", "httpd"))
AND (process.name IN ("sh", "bash", "dash", "curl", "wget", "perl") OR process.name LIKE "python*")
| STATS count = COUNT(*), cmds = VALUES(process.command_line), first = MIN(@timestamp) BY host.name, process.parent.name, process.name
// worker churn (syslog)
FROM logs-*
| WHERE @timestamp > NOW() - 30 days AND message LIKE "*php-fpm*child*exited on signal*"
| STATS count = COUNT(*) BY host.name, t = BUCKET(@timestamp, 10 minute)
| SORT t
// campaign infrastructure (Potassium loader)
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| WHERE destination.ip == "92.118.112.100" OR source.ip == "92.118.112.100" OR url.full LIKE "*92.118.112.100*"
| STATS count = COUNT(*), first = MIN(@timestamp) BY host.name, source.ip, destination.ip
serialize_handler runtime-switch setup — keep session.upload_progress.enabled=0 until the next bugfix releasesession.upload_progress.enabled=0 mitigation standsPHP — official announcement · GHSA — CVE-2026-109618 · GHSA — CVE-2026-109619 · Huntress — PHP2Shell · watchTowr — exploitation · The Hacker News — mass attempts, Oct 23 · Wiz — behaviors · Tenable FAQ · Censys live advisory · CISA KEV catalog — both CVEs, added Oct 26 · CISA alert — revised Oct 26, Sigma rule · SecurityWeek — OT/ICS vendor advisories, Oct 27