Sitemap

Brace for Impact — php2shell

13 min readOct 21, 2026

--

Status: live article updated Wednesday, 21 October 2026 afternoon (CEST) with researcher findings on the root cause. PHP’s official security release with confirmed affected/fixed versions is still scheduled for 14:00 UTC / 16:00 CEST today. Treat the root-cause section as a public reconstruction pending that advisory.

Press enter or click to view image in full size

Somewhere in your estate there is a PHP binary you have forgotten about. It sits in a vendor appliance, a ticketing system on a Windows server, a container image somebody built two years ago, or a shared-hosting account a marketing team still pays for. This afternoon, PHP ships fixes for two critical remote code execution bugs whose existence leaked a day early. From experience, the time between “patch available” and “someone diffs the patch and scans the internet” is now measured in hours. Sansec expects mass attacks within 15 minutes of the release.

A public researcher reconstruction of the root cause appeared this morning (see below). PHP’s own advisory is still due at 16:00 CEST, so treat those details as pending confirmation. What hasn’t changed is the responder job: inventory, patch, and hunt for compromise the same way you would for any PHP remote code execution.

TL;DR

Who should care: anyone who runs PHP anywhere: web servers, CMS and e-commerce stacks (WordPress, Drupal, Joomla, Magento, Shopware, WooCommerce), control panels like cPanel and Plesk, vendor appliances, Windows software that ships php.exe/php-cgi.exe, and containers.

What to do now:

  1. Inventory every PHP install, including the hidden ones. Record version, SAPI (FPM, CGI, mod_php, CLI) and internet exposure.
  2. Pre-stage the patch for 14:00 UTC / 16:00 CEST today. Internet-facing systems go first. Watch your distro or hosting provider for backports.
  3. Isolate end-of-life PHP (8.1 and older) or put it behind a VPN until you have a plan. Upstream won’t patch it.
  4. Turn on visibility now: alert on PHP or web-server processes spawning shells, interpreters or download tools. Baseline your webroots.
  5. If something fires, preserve evidence before you patch.Patching restarts PHP workers, and the volatile evidence goes with them.

What we know and what we don’t

Here’s the timeline as I can reconstruct it from public sources. Times are converted to CEST (UTC+2).

Tue 20 Oct, ~17:20
A Hacker News post claims two critical PHP RCEs will be disclosed on Thursday 22 Oct. It cites CVSS 10.0, “PHP 5 onward” and the IDs CVE-2026–109618 / CVE-2026–109619. The post is later deleted (Sansec, BleepingComputer).

Tue 20 Oct, afternoon/evening
watchTowr publicly confirms the leaked information is real and says it believes it has reproduced CVE-2026–109618. They have not released technical details.

Tue 20 Oct, 21:33
PHP security team posts advance notice of a security release. Two critical vulnerabilities, both CVSS v4.0 9.2. Release set for Wed 21 Oct, 14:00 UTC (16:00 CEST), a day earlier than the original embargo. PHP says it may ship sooner if details go public or exploitation appears.

Tue 20 Oct, evening
CERT-EU publishes advisory 2026–018: prepare to patch, and consider isolating internet-facing apps if you can’t.

Wed 21 Oct
ASD’s ACSC (Australia) issues a critical alert. Huntress, Sansec and The Hacker News publish guidance.

Wed 21 Oct, morning
Researcher [@J4ck3LSyN](https://x.com/J4ck3LSyN/status/2112703096443097263) publishes a reconstruction of both CVEs (heap use-after-free in PHP’s session upload-progress handler) and says they recreated the issues in a lab. This is not PHP’s advisory.

Wed 21 Oct, 16:00
Scheduled PHP release with full details, affected and fixed versions.

What’s solid:

  • Two critical RCEs exist, rated 9.2 (CVSS v4.0) by PHP itself. The “10.0” figure came from the leak.
  • An independent party (watchTowr) says the bugs are real and that it reproduced one of them.
  • Upstream fixes will only come for the supported branches 8.2, 8.3, 8.4 and 8.5. Trackers expect 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, but PHP has not confirmed those numbers, I’ll update the numbers after release however.
  • As of 20/21 October, nobody has reported exploitation in the wild: not Huntress, watchTowr, CERT-EU or ACSC. The bugs aren’t in CISA KEV.

What’s not solid:

  • Affected versions. “PHP 5 onward” comes from the leak. Sansec cites a leaked tool banner claiming 5.4.0+. watchTowr’s FAQ says an official advisory hasn’t confirmed the scope. Treat it as plausible but unconfirmed.
  • Preconditions Researcher reporting points at PHP’s session upload-progress feature (session.upload_progress), which is commonly enabled by default and comes into play on multipart file uploads. PHP has not confirmed that yet. SAPI/auth requirements are still unclear.
  • Exposure numbers. Mallory counted about 884,000 internet-facing hosts whose HTTP banner reported PHP ≤ 8.1 on Shodan. That’s 59% of roughly 1.5 million hosts that reported any version, and another ~740,000 reported PHP with no version (via The Hacker News). Those are banners, not confirmed vulnerable systems. Distro backports in particular make banner versions lie in both directions.

My working assumption as a responder: once the patch is public, a reliable exploit follows soon after. For context, Akamai saw exploit attempts within 24 hours of CVE-2024–4577 (PHP-CGI on Windows) going public in 2024.

How the bugs work

[UPDATE AFTER 16:00 CEST RELEASE: root cause, affected/fixed versions]

  • Researcher findings (pending PHP’s 16:00 CEST advisory)
    The summary below is based on a public reconstruction by [@J4ck3LSyN](https://x.com/J4ck3LSyN/status/2112703096443097263) (21 Oct 2026), who reported recreating CVE-2026–109618 and CVE-2026–109619. It is not the official PHP security advisory. Confirmed affected versions, fixed version numbers, and any vendor workaround still wait on PHP’s release. I will update this section again once that lands.
  • Component (reported): the session upload-progress handler that tracks multipart file uploads into the PHP session (session.upload_progress, RFC1867 progress updates in ext/session). That feature is commonly on by default.
  • Bug class (reported): two heap use-after-free issues tied to how that handler keeps stale pointers into session data across progress updates. CVE-2026–109619 and CVE-2026–109618 map to two related pointer-lifetime problems in that path. If an attacker can drive those updates, the end state the researcher describes is remote code execution inside the PHP worker.

Exposure and inventory

You can’t patch what you don’t know about. With PHP, the version you think you run and the one that actually serves traffic are often different binaries. Work through these layers.

Linux hosts (direct installs):

# What the CLI says (may differ from what FPM/Apache actually load!)
php -v
php -r 'echo PHP_SAPI, " ", PHP_VERSION, PHP_EOL;'

# Every PHP binary on disk, not just the one in $PATH
find / -xdev -type f \( -name 'php' -o -name 'php[0-9]*' -o -name 'php-fpm*' -o -name 'php-cgi*' \) -perm -u+x 2>/dev/null

# Package view
dpkg -l | grep -Ei '^ii\s+(php|libapache2-mod-php)' # Debian/Ubuntu
rpm -qa | grep -Ei '^php' # RHEL/Alma/Rocky/SUSE
apk info -vv 2>/dev/null | grep -i php # Alpine

# What is actually running right now
ps -eo pid,user,args | grep -Ei 'php-fpm|php-cgi|httpd|apache2|lsphp' | grep -v grep

A distro package shows the distro’s version string. Debian, Ubuntu, RHEL and others backport security fixes without bumping the upstream version, so a “PHP 8.1” package on a supported LTS might get patched while the banner still says 8.1. Check your distro’s security tracker for the CVE IDs, not just php -v.

Control panels and shared hosting: cPanel (EasyApache/EA-PHP, alt-php on CloudLinux), Plesk and DirectAdmin often run several PHP versions side by side, assigned per vhost. Inventory per site, not per server. Plesk has plesk bin php_handler --list and cPanel has whmapi1 php_get_installed_versions(verify both against your panel version). If you're a hosting customer, open a ticket today and ask your provider when they patch.

Windows: look for php.exe and php-cgi.exe bundled with vendor software (XAMPP/WAMP stacks, monitoring tools, help desks, IIS FastCGI setups).

Get-ChildItem -Path C:\ -Recurse -Include php.exe,php-cgi.exe -ErrorAction SilentlyContinue |
ForEach-Object { [pscustomobject]@{ Path=$_.FullName; Version=$_.VersionInfo.ProductVersion } }

In Defender for Endpoint, DeviceTvmSoftwareInventory | where SoftwareName has "php" gives you a quick first pass. It won't see unmanaged appliances.

Appliances: many network, backup, monitoring and VoIP appliances run a PHP-based web UI. You can’t patch them yourself. List them, find out which ones have a management interface reachable from the internet (that should be none), and track vendor advisories.

Containers: PHP versions get baked into images. Scan registries and running workloads:

# Running containers: what PHP is inside?
docker ps --format '{{.ID}} {{.Image}}' | while read id img; do
echo "== $img"; docker exec "$id" sh -c 'php -v 2>/dev/null | head -1'; done

# Image scanning (update the vuln DB after the release so the new CVEs are known)
trivy image --scanners vuln <registry/image:tag>
grype <registry/image:tag>

Also check base-image tags like php:8.1-fpm or php:7.4-apache in your Dockerfiles and Helm charts. Grep the repos, not just the registry.

External view: use your own attack-surface data, not Shodan alone. X-Powered-By headers are useful but often stripped or misleading.

Patch and isolate plan

  1. Tier your list. Internet-facing and unauthenticated first, then partner-facing, then internal. Once the advisory says which SAPIs or configs are affected, re-tier [UPDATE AFTER RELEASE].
  2. Pre-stage now. Line up change approvals, test environments, a maintenance window starting 16:00 CEST, and owners on call.
  3. Supported branches (8.2–8.5): update to the fixed point release, or the distro/hosting backport, as soon as it lands. Restart PHP-FPM/Apache/IIS app pools so the new binary actually loads, then verify with a phpinfo()-free method such as the package version plus a restart timestamp.
  4. End-of-life branches (≤ 8.1, 7.x, 5.x): upstream won’t fix them. Your options, in order: move to a supported branch; use a vendor/distro backport if one exists (some commercial and LTS distros do this) (verify per vendor); take it off the internet or put it behind a VPN/access proxy; as a last resort, a WAF in front, but don’t count on it until the root cause shows whether a request-level filter can even see the attack. Sansec says the attack “targets PHP itself” before application code runs.
  5. Can’t patch or isolate? Follow CERT-EU’s advice and consider pulling the internet-facing application offline until a fix is in place. That’s a business call, so make it before 16:00, not at 16:15.
  6. Before patching anything already suspicious, preserve evidence first (see triage below).

Bug-agnostic hunting: what PHP RCE post-exploitation looks like

Whatever the root cause turns out to be, an attacker with code execution inside a PHP worker usually does a handful of things. They run recon commands, drop a webshell for persistence, pull down tooling, call back to infrastructure, and go after secrets in config files. Florian Roth and Huntress both make the point that the strongest single signal is PHP or the web server spawning processes it normally doesn’t.

Get Mat Cyb3rF0x Fuchs’s stories in your inbox

Join Medium for free to get updates from this writer.

Everything below is a starting point to tune. Expect noise from apps that legitimately call convert, git, sendmail, composer or cron wrappers. Baseline before you alert.

1. Web-worker parent → suspicious child (Microsoft Defender XDR, KQL)

// php2shell hunt: web/PHP worker spawning shells, interpreters, recon or download tools
// Starting point – tune parent/child lists to your estate.
let webParents = dynamic(["php-fpm","php-cgi","php-cgi.exe","php.exe","httpd","httpd.exe","apache2","nginx","nginx.exe","w3wp.exe","lsphp"]);
let suspChildren = dynamic(["sh","bash","dash","zsh","cmd.exe","powershell.exe","pwsh.exe",
"python","python3","perl","ruby","curl","wget","nc","ncat","socat","certutil.exe","bitsadmin.exe",
"whoami","whoami.exe","id","uname","hostname","ipconfig.exe","systeminfo.exe","net.exe","net1.exe"]);
DeviceProcessEvents
| where Timestamp > ago(14d)
| where InitiatingProcessFileName has_any (webParents)
or InitiatingProcessFileName startswith "php" // php-fpm8.3, php8.2 etc. on Linux
| where FileName in~ (suspChildren)
or ProcessCommandLine has_any ("/dev/tcp/","mkfifo","base64 -d","chmod +x","/tmp/","/dev/shm/")
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName,
InitiatingProcessCommandLine, FileName, ProcessCommandLine, SHA256
| order by Timestamp desc

Then run a companion query for outbound connections from web workers. Most PHP apps talk to a database, a cache and a few known APIs, and almost nothing else:

DeviceNetworkEvents
| where Timestamp > ago(14d)
| where InitiatingProcessFileName startswith "php" or InitiatingProcessFileName in~ ("w3wp.exe","httpd","apache2")
| where RemoteIPType == "Public" and ActionType in ("ConnectionSuccess","ConnectionRequest")
| summarize Conns=count(), FirstSeen=min(Timestamp), LastSeen=max(Timestamp)
by DeviceName, InitiatingProcessFileName, RemoteIP, RemotePort, RemoteUrl
| order by FirstSeen desc

2. Splunk

Process lineage (Sysmon for Linux/Windows or EDR data mapped to CIM):

| tstats summariesonly=false count min(_time) as first max(_time) as last
from datamodel=Endpoint.Processes
where (Processes.parent_process_name IN ("php-fpm*","php-cgi*","php*","httpd*","apache2","nginx*","w3wp.exe","lsphp"))
AND (Processes.process_name IN ("sh","bash","dash","cmd.exe","powershell.exe","pwsh.exe","python*","perl","curl","wget","nc","ncat","socat","certutil.exe","whoami*","id","uname"))
by host Processes.parent_process_name Processes.process_name Processes.process Processes.user
| convert ctime(first) ctime(last)

Access-log anomalies, meaning POST bursts and 5xx spikes per client and path:

index=web sourcetype IN ("access_combined","nginx:plus:access","iis") (method=POST OR status>=500)
| bin _time span=5m
| stats count as hits dc(uri_path) as paths values(status) as statuses by _time clientip uri_path
| eventstats avg(hits) as avg stdev(hits) as sd by uri_path
| where hits > avg + 3*sd OR match(statuses,"5\d\d")
| sort - hits

Crashes. A memory-corruption bug often leaves crashed workers behind while someone tunes an exploit:

index=* ("exited on signal 11" OR "SIGSEGV" OR "Segmentation fault" OR "AH00052" OR "segfault at") (php OR httpd OR apache2)
| stats count min(_time) as first max(_time) as last by host source

3. Sigma-style rule outline

SigmaHQ already has web-server-spawns-shell rules for Windows and Linux (e.g. proc_creation_win_webshell_susp_process_spawned_from_webserver.yml, proc_creation_lnx_webshell_detection.ym). Use those first. For a php2shell-focused variant:

title: PHP Worker Spawns Shell, Interpreter or Download Tool (php2shell hunt)
status: experimental
description: Bug-agnostic post-exploitation signal for PHP RCE. Starting point – tune.
references:
- https://www.huntress.com/blog/php2shell-leaked-critical-php-vulnerabilities
author: Mathias Fuchs
date: 2026-10-21
tags: [attack.execution, attack.t1059, attack.persistence, attack.t1505.003]
logsource:
category: process_creation
product: linux # duplicate for windows with .exe names
detection:
selection_parent:
ParentImage|contains: ['/php-fpm', '/php-cgi', '/lsphp', '/httpd', '/apache2']
selection_child:
Image|endswith: ['/sh','/bash','/dash','/python','/python3','/perl','/curl','/wget','/nc','/ncat','/socat','/whoami','/id','/uname']
filter_known:
CommandLine|contains: ['/usr/sbin/sendmail'] # add your app's legit calls
condition: selection_parent and selection_child and not filter_known
falsepositives: [Apps that legitimately shell out (image processing, mail, deploy hooks)]
level: high

4. Linux one-liners, auditd and process-watcher

# Process tree: anything hanging off php-fpm/apache that isn't another worker?
ps -eo pid,ppid,user,lstart,args --forest | grep -A3 -E 'php-fpm|apache2|httpd' | grep -vE 'php-fpm: (pool|master)'

# Web workers with network connections (look for unexpected public peers)
ss -tnp | grep -E 'php|httpd|apache2'

# PHP files created/changed since the leak (adjust date + webroots)
find /var/www /srv /home/*/public_html -xdev -type f \( -name '*.php*' -o -name '*.phtml' -o -name '.htaccess' -o -name '.user.ini' \) \
-newermt '2026-10-20 00:00' -printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2>/dev/null | sort

# PHP in places it should never be: upload/media/cache dirs
find /var/www -xdev -type f -name '*.php*' \( -path '*/uploads/*' -o -path '*/media/*' -o -path '*/cache/*' -o -path '*/tmp/*' \) 2>/dev/null

# Executables/scripts in world-writable temp dirs
find /tmp /var/tmp /dev/shm -xdev -type f \( -perm -u+x -o -name '*.php' -o -name '*.sh' \) -printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2>/dev/null

# Worker crashes
grep -hE 'exited on signal (11|6)|SIGSEGV|SIGBUS' /var/log/php*-fpm.log /var/log/php-fpm/*.log 2>/dev/null | tail -50
grep -h 'AH00052' /var/log/apache2/error.log* /var/log/httpd/error_log* 2>/dev/null | tail -50
journalctl -k --since '2026-10-14' | grep -Ei 'segfault.*(php|httpd|apache2)'

auditd ideas: log execve by the web user (uid 33 www-data on Debian/Ubuntu, 48 apache on RHEL; check yours) and writes to the webroot. Test the volume on staging first.

-a always,exit -F arch=b64 -S execve -F uid=33 -k web_exec
-a always,exit -F arch=b32 -S execve -F uid=33 -k web_exec
-w /var/www/ -p wa -k webroot_write

No EDR on that Linux box? Florian Roth’s free process-watcher (eBPF, Linux ≥ 5.15 with BTF) logs every process a PHP service starts, with command lines, as JSON. Run it in observe mode. In his own words it “prevents nothing”. It gives you visibility, not protection.

5. Velociraptor

  • Linux.Search.FileFinder: glob your webroots (/var/www/**/*.php, /home/*/public_html/**) with MoreRecentThan set to your lookback and Calculate_Hash=Y. Add a webshell YARA rule in YaraRule to inspect content.
  • Generic.Detection.WebShells (Velociraptor Exchange, by SEC Defence): string/YARA plus recent-change checks under the webroot. Defaults to /var/www/**. Its web-server child-process check is Windows-only, so pair it with the next two on Linux.
  • Generic.System.Pstree / Linux.Sys.Pslist: process lineage under php-fpm/apache.
  • Linux.Network.NetstatEnriched: sockets with owning process.
  • Linux.Sys.Crontab: persistence via cron.
  • Linux.Events.ProcessExecutions: client event monitoring of execs, so you can watch for web-worker children going forward.
  • Generic.Detection.Yara.Glob: ad-hoc YARA over arbitrary globs.

Triage checklist for a suspected hit

Rule zero: don’t patch, restart or “clean up” first.Restarting PHP-FPM kills the processes, connections and in-memory state you need to scope the incident.

  1. Contain without destroying. Network-isolate via EDR, security group or switch port. Don’t power off.
  2. Volatile first: process tree with command lines (ps auxwwf), /proc/<pid>/{cmdline,environ,exe,cwd,fd} for suspicious processes, ss -tanp, logged-in users, loaded kernel modules. Take memory with AVML or LiME on Linux, or your usual tool on Windows.
  3. Logs: web access and error logs, PHP-FPM logs, the system journal/syslog, audit logs, WAF/reverse-proxy/CDN logs (often the only place full request metadata survives), plus EDR telemetry. Copy them off the box.
  4. Webroot snapshot with hashes: tar the docroots, uploads, /tmp, /var/tmp, /dev/shm preserving timestamps, plus a SHA-256 manifest. Diff against your deployment artifact or git. Webshells hide in plain sight as wp-content/uploads/2026/10/x.php, a modified index.php, or .user.ini/.htaccess tricks.
  5. Persistence sweep: crontabs (user and system), systemd units/timers, authorized_keys, new local users, sudoers, modified PHP config (auto_prepend_file), Windows scheduled tasks and services.
  6. Scope: use the first malicious timestamp to pivot across all hosts sharing the same code, credentials or network segment. The bugs existed before the leak, so don’t set your lookback to 20 October. Go back as far as your logs allow.
  7. Rotate secrets the PHP process could read: DB credentials in wp-config.php/.env/config.php, API keys, SMTP, payment-gateway keys, session and app secrets (e.g. Laravel APP_KEY), cloud credentials and instance-role tokens, deploy keys. Assume they're compromised once you confirm code execution.
  8. Evict and rebuild: prefer rebuilding from known-good images and code over “cleaning” a box. Patch PHP on the rebuild, restore data, not code, from backup, then monitor closely for re-entry.
  9. Notify: consider regulatory and contractual timelines (NIS2, GDPR, PCI DSS for shops).

Lessons

Every year we have a week like this. The lesson is never “this bug was special”. It’s that the inventory wasn’t ready, the EOL systems were known and tolerated, and nobody was watching what the web server spawned. A leaked embargo just takes away your buffer. If you do one thing beyond patching this week, make “web worker spawns a shell” a permanent, tuned detection on every web host, Linux included. It caught PHP-CGI in 2024 and it will catch the next one.

Patch at 16:00. Hunt at 16:05. And write down which PHP you found that nobody knew about.

References

--

--