PHP2Shell is the nickname for two critical PHP flaws, CVE-2026-109618 and CVE-2026-109619, that let an attacker run commands on a web server without logging in. PHP rushed out fixed releases on October 21, 2026, after details leaked online a day early, and Cloudflare reported early attack attempts before the patches even shipped. If your organization runs WordPress, Drupal, Joomla, Laravel, or any other PHP application, this one belongs at the top of your list.
Over the past week, the iSECTECH blue team has been helping clients find every PHP runtime they own, patch it safely, and prove the fix actually landed. This post walks through exactly what we did on one real client server, step by step, with copy-and-paste commands. You do not need to be a PHP expert. If you can open a terminal on your server and follow instructions in order, you can do this.
Patching was the easy part. The work that mattered was finding the second PHP version nobody knew was running, taking a backup we had actually tested, and proving afterward that the website was serving traffic from the fixed build.
— iSECTECH Blue Team field note, October 2026
PHP2Shell in Plain English
Both bugs live inside PHP itself, not in your website code. That is why they are so serious: the application you installed can be fully up to date and still be exposed. According to the PHP project advisory, both are rated CVSS v4.0 9.2 (Critical) and allow remote code execution with the privileges of the web server account.
- CVE-2026-109618 (out-of-memory corruption): a specially crafted request makes PHP run out of memory while it is still setting up. PHP then cleans up data that was never fully built, and that mess can be turned into code execution.
- CVE-2026-109619 (stale state between requests): PHP can keep a pointer to memory from a previous request. When the same worker handles the next request, an attacker can abuse that leftover state to leak memory or run code.
Every PHP setup that serves web pages is in scope: PHP-FPM (common with nginx), Apache with mod_php, php-cgi, LiteSpeed, and FrankenPHP. Rapid7 notes that Apache with mod_php is the highest-risk setup because the attack can work before any application code runs, and Huntress reports that researchers demonstrated attacks against WordPress, Drupal, and Joomla. PHP is clear that configuration tweaks only reduce risk. The only real fix is a patched PHP build.
Which version do you need?
| Your PHP branch | Minimum fixed version (upstream) |
|---|---|
| 8.5 | 8.5.12 |
| 8.4 | 8.4.27 |
| 8.3 | 8.3.36 |
| 8.2 | 8.2.35 |
| 8.1, 8.0, 7.x and older | No regular upstream releases. Use your OS vendor’s backported package or upgrade to a supported branch. |
Important: Linux distributions often patch PHP without changing the main version number. For example, Ubuntu USN-8948-1 ships the fix for Ubuntu 24.04 as package 8.3.6-0ubuntu0.24.04.13, which still reports itself as PHP 8.3.6. So do not panic if php -v shows an older-looking number. Check the full package version against your vendor’s advisory.
| Ubuntu release | PHP package | Fixed package version |
|---|---|---|
| 26.10 | php8.5 | 8.5.9-0ubuntu2.1 |
| 26.04 LTS | php8.5 | 8.5.4-0ubuntu1.5 |
| 24.04 LTS | php8.3 | 8.3.6-0ubuntu0.24.04.13 |
| 22.04 LTS | php8.1 | 8.1.2-1ubuntu2.28 |
How iSECTECH Handled One Client’s Server
The client in this walkthrough runs a business-critical PHP web application on an Ubuntu server, with nginx in front and PHP-FPM behind it. On paper, it was “a PHP 8.3 server.” In reality, our inventory found two PHP-FPM versions installed and running: 8.3, which actually served the website, and 8.4, left over from an earlier test. Both were vulnerable, and patching only one would have left a live, exposed runtime on the box.
We followed the same six steps below: confirm what is running, check that a fix is offered, back up and verify the backup, patch in a short maintenance window, verify the result, and check for signs that someone got in first. The upgrade touched 27 PHP packages, removed nothing, and the application came back healthy on the first restart. Here is how you can do the same.
Before You Start: Three Ground Rules
- Only test systems you own or have written permission to assess. Scanning someone else’s server is not a security check; it can be a crime.
- A version number or a web header is a clue, not proof. Do not announce a server is “vulnerable” or “safe” until you have checked it from the inside.
- Back up before you change anything. Patching restarts PHP and can briefly interrupt your site. Plan a short maintenance window and tell the people who use the application.
Step 1: Find Every Place PHP Runs
Start with a quick outside view. From your own computer, this command asks your website what software it reports. Replace the address with your own site.
curl -sI https://www.your-domain.com | grep -iE '^(server|x-powered-by):'
If you see something like X-Powered-By: PHP/8.3.6, you have found PHP. If you see nothing, that does not mean you are safe; many servers hide this header. IT teams who manage several servers can use Nmap to check common web ports on a server they are authorized to test. We use the documentation-only address 192.0.2.10 here; replace it with your server’s IP.
nmap -sT -sV --version-light -T3 --open -p 80,443,8000,8080,8443,8888,9000 --script http-headers 192.0.2.10
Look for X-Powered-By: PHP or PHP session cookies such as PHPSESSID or laravel_session. Again, these are hints. The next step confirms what is really there. Remember to check internal servers too: intranet sites, monitoring tools, ticketing systems, and admin panels often run PHP and never appear on your public website.
Step 2: Confirm What Is Really Running on the Server
Log in to the server over SSH and paste this block. It only reads information; it changes nothing.
echo "== OPERATING SYSTEM =="; grep PRETTY_NAME /etc/os-release
echo "== PHP COMMAND-LINE VERSION =="; php -v 2>/dev/null | head -n 1 || echo "php CLI not found"
echo "== INSTALLED PHP PACKAGES =="; dpkg -l 2>/dev/null | grep -E '^ii +(php|libapache2-mod-php)' | awk '{print $2, $3}'
echo "== PHP AND WEB SERVICES =="; systemctl list-units --type=service --no-pager | grep -Ei 'php|apache2|httpd|nginx|lsws|litespeed'
echo "== PHP-FPM SOCKETS =="; ls /run/php/ 2>/dev/null
echo "== WHICH PHP NGINX SENDS TRAFFIC TO =="; sudo nginx -T 2>/dev/null | grep -E 'fastcgi_pass'
echo "== APACHE PHP MODULE =="; apache2ctl -M 2>/dev/null | grep -i php
How to read the results:
- Installed packages show every PHP version on the box. In our client’s case, both
php8.3-*andphp8.4-*packages appeared. - Services show which ones are running, such as
php8.3-fpm.serviceandphp8.4-fpm.service. - The fastcgi_pass line shows which PHP version nginx actually uses for your website, for example
unix:/run/php/php8.3-fpm.sock. - An Apache php module means mod_php, the highest-risk setup for these CVEs. Patch it first.
php -vonly shows the command-line version. It can differ from the version serving your website, so never rely on it alone.
Write down every installed PHP version, not just the one serving your site. On Red Hat, Rocky, AlmaLinux, or CentOS servers, use rpm -qa | grep -i php instead of dpkg -l.
Step 3: Check That a Fixed Build Is Available
Refresh your package list and see which PHP updates are waiting:
sudo apt-get update
apt list --upgradable 2>/dev/null | grep -Ei 'php'
Next, do a dry run. The -s flag simulates the upgrade without installing anything, so you can see exactly what will change. The first line collects every installed PHP package automatically.
PHP_PKGS=$(dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' 'php*' 'libapache2-mod-php*' 2>/dev/null | awk '$1=="ii"{print $2}')
echo "$PHP_PKGS"
sudo apt-get -s install --only-upgrade $PHP_PKGS
Read the summary line at the end. Our client’s server showed 27 upgraded, 0 newly installed, 0 to remove. Your numbers will differ. Stop and investigate if the dry run wants to remove packages, replace your web server, or pull in a repository you do not recognize. If no PHP updates appear at all, compare your package version with your vendor’s advisory; you may already be patched, or your PHP may come from a third-party source that has not shipped a fix yet.
Step 4: Take a Backup You Can Trust
This script backs up your database, your application files, and your PHP and web server settings, then checks that each archive can be read. Edit the two lines marked EDIT to match your application folder and database name before running it. It uses the MySQL/MariaDB root login that Ubuntu configures by default, so no password is typed on the command line.
sudo bash <<'BACKUP'
set -euo pipefail
umask 077
APP_DIR='/var/www/html' # EDIT: your application folder
DB_NAME='your_database_name' # EDIT: your database name
DEST='/root/php2shell-backup'
mkdir -p "$DEST"
mysqldump --single-transaction --quick --routines --events --triggers --databases "$DB_NAME" | gzip > "$DEST/database.sql.gz"
gzip -t "$DEST/database.sql.gz"
tar -C "$(dirname "$APP_DIR")" -czf "$DEST/application.tar.gz" "$(basename "$APP_DIR")"
tar -tzf "$DEST/application.tar.gz" > /dev/null
CONF_DIRS=""
for d in /etc/php /etc/nginx /etc/apache2; do [ -d "$d" ] && CONF_DIRS="$CONF_DIRS $d"; done
tar -czf "$DEST/configs.tar.gz" $CONF_DIRS 2>/dev/null
cd "$DEST" && sha256sum database.sql.gz application.tar.gz configs.tar.gz > SHA256SUMS
ls -lh "$DEST"
echo "BACKUP COMPLETE AND READABLE: $DEST"
BACKUP
If your site does not use a database, delete the two mysqldump and gzip -t lines. If any command fails, the script stops immediately instead of producing a half-finished backup.
Copy the backup off the server, encrypted
A backup that lives only on the server you are about to change is not much of a backup. On the server, bundle and encrypt it with a strong passphrase (store the passphrase in your password manager, never in a script or chat), then place a copy in your home folder so you can download it:
sudo tar -C /root -czf /root/php2shell-backup.tar.gz php2shell-backup
sudo gpg --symmetric --cipher-algo AES256 --pinentry-mode loopback --output /root/php2shell-backup.tar.gz.gpg /root/php2shell-backup.tar.gz
sudo install -m 600 -o "$USER" /root/php2shell-backup.tar.gz.gpg ~/php2shell-backup.tar.gz.gpg
sha256sum ~/php2shell-backup.tar.gz.gpg
Now, from your own computer, download it. Replace youruser and 192.0.2.10 with your SSH username and server address. On Windows, run this in PowerShell (the built-in OpenSSH client includes scp):
scp youruser@192.0.2.10:php2shell-backup.tar.gz.gpg .
Get-FileHash .\php2shell-backup.tar.gz.gpg -Algorithm SHA256
On macOS or Linux, use the same scp line, then sha256sum php2shell-backup.tar.gz.gpg (or shasum -a 256 on macOS). The two SHA-256 values must match exactly. Only then is the transfer verified. In our engagement, we also test-decrypted the archive before moving on, because an encrypted backup you cannot open is worthless.
Step 5: Apply the Patch
With the owner’s approval, a verified backup, and the maintenance window started, run the real upgrade. It is the same command as the dry run, without -s. APT will show the plan and ask you to confirm.
PHP_PKGS=$(dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' 'php*' 'libapache2-mod-php*' 2>/dev/null | awk '$1=="ii"{print $2}')
sudo apt-get install --only-upgrade $PHP_PKGS
Then restart PHP so every worker loads the fixed code. Old workers keep running the vulnerable version in memory until they are restarted.
sudo systemctl restart 'php*-fpm.service'
systemctl is-active --quiet apache2 && sudo systemctl restart apache2
systemctl is-active --quiet nginx && sudo nginx -t && sudo systemctl reload nginx
On Red Hat-family servers, the equivalent is sudo dnf upgrade 'php*' followed by restarting php-fpm and httpd. Do not run apt autoremove or jump to a different PHP major version during an emergency change; that turns a patch into a migration.
Step 6: Prove the Fix Worked
“The service is running” is not the same as “the site is fixed.” Check the installed versions, the services, and the website itself:
PHP_PKGS=$(dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' 'php*' 'libapache2-mod-php*' 2>/dev/null | awk '$1=="ii"{print $2}')
dpkg-query -W -f='${binary:Package} ${Version}\n' $PHP_PKGS
systemctl --no-pager --type=service list-units 'php*-fpm*' apache2.service nginx.service
curl -sS -o /dev/null -w 'Website HTTP status: %{http_code}\n' https://www.your-domain.com/
- Every PHP package version should now match or exceed the fixed version from your vendor’s advisory (or the upstream table above).
- Every PHP and web service should show
active running. - The website should return
200, or301/302if it redirects to a login page. - Finally, log in to the application like a real user and test its key features: forms, uploads, reports, scheduled jobs. A status code alone does not prove the business is working.
For our client, all 27 packages showed the patched version, both PHP-FPM services and nginx came back active, and the application’s login and core workflows passed our checklist. We recorded the evidence and closed the change.
Step 7: Check for Signs Someone Got In First
Attack attempts started before the patch shipped, so patching closes the door but does not tell you whether someone walked through it earlier. These three read-only checks are a sensible first look. Adjust /var/www if your site lives elsewhere.
echo "== UNUSUAL PROCESSES STARTED BY PHP OR THE WEB SERVER =="
for pid in $(pgrep -f 'php-fpm|php-cgi|apache2|httpd'); do ps --ppid "$pid" -o pid=,user=,cmd= ; done | grep -Ev 'php-fpm|php-cgi|apache2|httpd' || echo "None found"
echo "== PHP FILES CREATED OR CHANGED SINCE OCTOBER 15, 2026 =="
sudo find /var/www -type f \( -name '*.php' -o -name '*.phtml' -o -name '.htaccess' -o -name '.user.ini' \) -newermt '2026-10-15' -printf '%TY-%Tm-%Td %TH:%TM %u %p\n' | sort
echo "== SCHEDULED TASKS FOR THE WEB USER =="
sudo crontab -l -u www-data 2>/dev/null || echo "No crontab for www-data"
Plugin or theme updates will also show up in the file list, so compare anything unexpected against your own change history. Red flags include PHP processes launching shells, curl, wget, or python; PHP files in upload folders; and scheduled tasks nobody recognizes. If you find any of these, do not clean up on your own: isolate the server, preserve the logs, and bring in an incident response team. Deleting a web shell also deletes the evidence of what the attacker did.
If You Cannot Patch Today
If a fixed package is not yet available for your platform, PHP’s advisory lists temporary measures that reduce exposure but do not remove it. On PHP-FPM, add these lines to your pool file (for example /etc/php/8.3/fpm/pool.d/www.conf; change the version to yours). Settings in .user.ini or set by the application are applied too late and do not help.
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
On PHP 8.5, also add php_admin_flag[fatal_error_backtraces] = off. Then test the configuration and restart every worker:
sudo php-fpm8.3 -t && sudo systemctl restart php8.3-fpm
If your site needs uploads larger than 8 MB, PHP suggests 32M instead. PHP also documents a memory_limit=-1 measure for the out-of-memory bug; it removes PHP’s memory ceiling, so only use it with care and with resource limits in place. Treat all of this as a bridge to patching within days, not a fix. Placing the site behind a web application firewall with PHP2Shell rules, or restricting access to a VPN, buys additional time.
Windows Servers, Shared Hosting, and Appliances
Windows and IIS
PHP on Windows is not updated by Windows Update. Run this read-only inventory in PowerShell as Administrator to find where PHP lives and which build IIS uses:
& {
Write-Host '== PHP IN PATH =='
Get-Command php.exe, php-cgi.exe -ErrorAction SilentlyContinue | Select-Object Name, Source
Write-Host '== RUNNING PHP PROCESSES =='
Get-CimInstance Win32_Process | Where-Object Name -match '^php(-cgi|-win)?\.exe$' | Select-Object Name, ExecutablePath
Write-Host '== COMMON INSTALL FOLDERS =='
@('C:\PHP', 'C:\Program Files\PHP', 'C:\xampp\php', 'C:\wamp64\bin\php', 'C:\laragon\bin\php') | Where-Object { Test-Path $_ }
Write-Host '== IIS PHP HANDLERS =='
$appcmd = "$env:windir\system32\inetsrv\appcmd.exe"
if (Test-Path $appcmd) { & $appcmd list config /section:system.webServer/fastCgi | Select-String 'php' } else { 'IIS not installed' }
}
Then check the version of the exact executable IIS points to, for example & 'C:\PHP\php-cgi.exe' -v. Download the fixed build from PHP for Windows, back up the old PHP folder and IIS configuration, swap the files during a maintenance window, and recycle the application pools. Never overwrite a live PHP folder without a rollback copy.
Shared hosting and managed WordPress
If you rent hosting, your provider patches PHP for you, but you should still confirm it. In WordPress, go to Tools > Site Health > Info > Server to see the PHP version, or check the PHP selector in your control panel (such as cPanel’s MultiPHP Manager). If the version is not on the fixed list, open a ticket and ask your host in writing whether PHP2Shell is patched on your server.
Appliances, containers, and bundled software
Network appliances, NAS devices, monitoring tools, and Docker images often ship their own PHP. Check each vendor’s security advisory, and rebuild container images from patched base images rather than patching a running container that will be replaced on the next deploy.
Common Mistakes We Saw This Week
- Patching the PHP version that serves the website and forgetting a second installed version.
- Trusting
php -v, which shows the command-line version, not necessarily the web version. - Declaring a server safe because the
X-Powered-Byheader is missing. - Treating an APT dry run as an installed update.
- Upgrading packages but never restarting PHP-FPM or Apache, leaving the old code running in memory.
- Taking a backup that was never verified, copied off the server, or tested.
- Forgetting Windows servers, internal tools, containers, and hosted sites.
Do This Today
First, list every server, site, and appliance that might run PHP, including internal ones. Second, confirm the real PHP versions on each one using Step 2. Third, take and verify a backup, then patch your internet-facing systems before anything else, starting with Apache mod_php setups. Fourth, prove the fix with Step 6 and run the Step 7 checks. If you do those four things this week, you will have closed one of the most serious web vulnerabilities of 2026 and have the evidence to show your leadership, auditors, and customers.
If you would rather have experienced hands on it, the iSECTECH team can inventory your PHP estate, patch it in a controlled maintenance window, and hunt for signs of compromise. Explore our vulnerability assessments, managed detection and response, and 24/7 incident response services, or contact us for a confidential conversation. If you suspect an active compromise, call our priority line at +1 800 325 1874.
Sources and Further Reading
- PHP project: October 21, 2026 security advisory and releases
- PHP advisory GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
- PHP advisory GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
- Canonical Ubuntu Security Notice USN-8948-1
- Rapid7: Critical unauthenticated RCE in PHP
- Huntress: Rapid response on PHP2Shell
- BleepingComputer: PHP ships emergency patches for PHP2Shell
This walkthrough is educational and defensive. Commands are drawn from a real iSECTECH remediation and adapted with example addresses. Always confirm your operating system, paths, package sources, and backups before making changes in production, and only assess systems you own or are authorized to test.
