PHP2Shell is spreading fast. Get a FREE PHP2Shell Vulnerability Check!
BlogOctober 24, 20266 min read

PHP2Shell: Step-by-Step Remediation and Exposure Validation

Share this article
PHP2Shell: Step-by-Step Remediation and Exposure Validation

Learn how to detect PHP2Shell vulnerabilities (CVE-2026-109618 and CVE-2026-109619), patch affected PHP servers, check for exploitation, and verify the fix.

CVE-2026-109618 & CVE-2026-109619 | Last updated: October 23, 2026

Step 1: Find vulnerable instances

PHP2Shell has several attack paths that depend on the server API (mod_php or PHP-FPM), memory configuration and application code. Version checks alone fail in both directions: they miss hosts that strip the X-Powered-By header, and they flag Linux LTS packages that carry a backported fix under the old version string.

External exposure

Run the free PHP2Shell pentest by Plainsea against your internet-facing endpoints. Instead of reading banners, Plainsea's agentic testing identifies exposed PHP environments and checks whether each endpoint is actually vulnerable, with safety controls that keep the test from crashing production workers.

Internal Linux host audit

php -v on the command line is not enough. Long-running PHP-FPM and Apache workers keep the pre-patch binary in memory until they are restarted. These commands list every installed PHP binary, find processes still running deleted (replaced) PHP code, and check for distribution backports:

BASH
# 1. List every PHP binary on the host with its version 
find / \( -path /proc -o -path /sys -o -path /dev \) -prune -o -type f \ 
\( -name 'php' -o -name 'php[0-9]*' -o -name 'php-fpm*' -o -name 'php-cgi*' \) \ 
-perm -u+x -print 2>/dev/null | 
while read -r bin; do printf '%s: ' "$bin"; "$bin" -v 2>/dev/null | head -n1; done 

# 2. Processes still mapping deleted (pre-patch) PHP binaries or libphp 
sudo grep -lE '(php|libphp).*\(deleted\)' /proc/[0-9]*/maps 2>/dev/null | 
cut -d/ -f3 | xargs -r ps -o pid,user,cmd -p 
# Any process listed here has NOT been restarted since patching and is still exposed. 

# 3a. Debian/Ubuntu: check the versioned package changelogs for the fix 
zgrep -H -E 'CVE-2026-10961[89]' \ 
/usr/share/doc/php*-common/changelog.Debian.gz 2>/dev/null 

# 3b. RHEL/Rocky/AlmaLinux 
rpm -q --changelog php-common 2>/dev/null | grep -E 'CVE-2026-10961[89]'

Note: No output from step 3 means either the backport is not installed or your vendor has not published it yet. Check your distribution's security tracker before concluding either way.

Kubernetes

Check every container in every pod, not just the first one:

BASH
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{range .spec.containers[*]}{.name}{","}{end}{"\n"}{end}' | 
while read -r ns pod containers; do
 for c in ${containers//,/ }; do
 ver=$(kubectl exec -n "$ns" "$pod" -c "$c" -- php -r 'echo PHP_VERSION;' 2>/dev/null)
 [ -n "$ver" ] && echo "$ns/$pod [$c]: PHP $ver" 
 done 
done

This needs pods/exec permission and misses distroless images, which have no php binary to call. For full coverage, list the running images and scan them with your container scanner once its database includes both CVEs:

BASH
kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' | sort -u

Step 2: Patch runtimes and restart workers

Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distribution's patched package, then restart every process that runs PHP. Restarting nginx is not needed; it does not load PHP.

BASH
# Debian / Ubuntu
sudo apt-get update
sudo apt-get install --only-upgrade -y $(dpkg-query -W -f='${binary:Package}\n' 'php*' 'libapache2-mod-php*' 2>/dev/null)
sudo systemctl restart 'php*-fpm.service' # every FPM pool
sudo systemctl restart apache2 # only if Apache runs mod_php 
 
 
# RHEL / AlmaLinux / Rocky Linux / Amazon Linux
sudo dnf upgrade -y 'php*'
sudo systemctl restart php-fpm
sudo systemctl restart httpd # only if Apache runs mod_php

Then run check 2 from Step 1 again. It should return nothing.

For Docker and Kubernetes, pin the patched base image, rebuild with --pull --no-cache, and roll the deployments (kubectl rollout restart deployment/<name>). The check below fails the build if PHP is older than the fix for its branch, and keeps passing when later patch releases ship:

DOCKERFILE
FROM php:8.4.27-fpm-bookworm
 
# Fail the build if PHP is older than the October 21, 2026 fix for its branch 
RUN php -r 'exit(version_compare(PHP_VERSION, ["8.5"=>"8.5.12","8.4"=>"8.4.27","8.3"=>"8.3.36","8.2"=>"8.2.35"][PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION] ?? "99", ">=") ? 0 : 1);'

Step 3: Temporary mitigations if you cannot patch yet

Updating is the only complete fix. The PHP security team states that the measures below reduce exposure to specific attack paths but do not make an installation safe. Use them only to cover the gap until a maintenance window.

A. PHP configuration

Set these administratively so applications cannot override them with ini_set() or .user.ini, then restart every worker.

INI
; e.g. /etc/php/8.x/fpm/pool.d/www.conf
php_admin_value[memory_limit] = -1 ; see note below
php_admin_value[post_max_size] = 8M ; 32M if uploads are required
php_admin_value[upload_max_filesize] = 7M ; keep below post_max_size
php_admin_flag[session.upload_progress.enabled] = off
php_admin_flag[fatal_error_backtraces] = off ; PHP 8.5 only
 
; Not from the PHP advisory: recycle workers sooner to shorten stale-state lifetime
pm.max_requests = 50
APACHE (MOD_PHP, IN A VIRTUALHOST OR DIRECTORY BLOCK, NOT .HTACCESS)
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_value upload_max_filesize 7M
php_admin_flag session.upload_progress.enabled off
php_admin_flag fatal_error_backtraces off
  • memory_limit = -1 removes the PHP-controlled out-of-memory failure, but a real allocation failure can still reach the vulnerable path. Put a container or cgroup memory limit around the workers as the availability boundary. Do not use RLIMIT_AS or RLIMIT_DATA: they can trigger the very failure you are trying to avoid.

  • Disabling upload progress blocks the confirmed session attack path for CVE-2026-109619, not every variant. Upload progress bars will stop working.

  • Fix what the attacker can reach: remove invalid or deprecated per-request settings that emit diagnostics before initialization, and fix attacker-reachable fatal errors and timeouts. Hiding warnings with error_reporting, display_errors or @ does not help.

B. Reverse proxy limits (nginx)

Cap request body size before traffic reaches PHP, and reject multipart/form-data outside the endpoints that genuinely accept uploads:

NGINX
http {
    # 8m if the site takes no uploads, 32m if it does 
    client_max_body_size 8m; 

    # Allow multipart only on known upload endpoints; list allowlist entries first
    map "$content_type:$uri" $deny_multipart {
       default 0;
       "~*^multipart/form-data.*:/(api/v1/upload|wp-admin/async-upload\.php)$" 0;
        "~*^multipart/form-data" 1;
 } 
    server {
         listen 443 ssl;
         server_name example.com;
 
         if ($deny_multipart) {
           return 403;
       }
    }
}

Caution: Blocking multipart breaks every form that takes a file, including CMS media uploads and contact forms with attachments. Build the allowlist from your access logs before enforcing it.

Step 4: Hunt for exploitation and verify the fix

Patching closes the hole; it does not remove anything an attacker already planted. Check for signs of exploitation before you declare a host clean:

BASH
# 1. Out-of-memory spikes in PHP error logs (CVE-2026-109618 attempts) 
# Noisy on its own: look for bursts from a single source. Disappears once memory_limit = -1.
grep -rh "Allowed memory size of .* bytes exhausted" /var/log/php* /var/log/apache2/ /var/log/httpd/ 2>/dev/null

# 2. Known PHP2Shell scanner and PoC user agents in access logs (indicators published by VulnCheck)
grep -rhE 'php2shell-(scan|verify|poc)' /var/log/apache2/ /var/log/httpd/ /var/log/nginx/ 2>/dev/null

# 3. Session files carrying planted PHP code (confirm your path with session.save_path)
sudo grep -rlE 'PWNED::|<\?php' /var/lib/php/sessions/ /tmp/sess_* 2>/dev/null

# 4. Web server user spawning shells or network tools
# Requires an auditd execve rule; it records from the moment you add it, not retroactively.
sudo auditctl -a always,exit -F arch=b64 -S execve -F euid=33 -k web_exec # 33 = www-data on Debian/Ubuntu
sudo ausearch -k web_exec -i | grep -E 'exe=".*/(sh|bash|dash|curl|wget|nc|ncat|python3?|perl)"'

Access logs do not record POST bodies, so searching them for PHP_SESSION_UPLOAD_PROGRESS finds nothing. If you log request bodies at a WAF, look for multipart POSTs carrying that field, the boundary ------p2s9rdme2st6guc9g0rafic and the upload filename p2s.txt.

If you find a hit: treat the host as compromised, not just vulnerable. Isolate it, preserve logs and memory, and rotate every secret the PHP process could read.

Once patches or mitigations are in place, rerun the free PHP2Shell pentest by Plainsea. The retest confirms the exposure is closed on every endpoint it reaches and gives you a dated result for your audit trail. Behind a load balancer, target each node directly; otherwise you have only tested whichever node answered.

PHP2Shell vulnerabilities
PHP2ShellOctober 23, 20263 min read

PHP2Shell: CVEs, Affected Versions & Fixes

PHP2Shell (CVE-2026-109618 & 109619): understand the vulnerabilities, affected PHP versions, exploitation risks, patches, and how to verify remediation

Read article
A Christmas Carol for Modern Security Teams
PentestingDecember 22, 20255 min read

A Christmas Carol for Modern Security Teams

Follow Ebenezer CISO through visits from the ghosts of pentests past, present and future – and see why annual tests aren’t enough for modern teams that need continuous security.

Read article
PTaaS vs. Traditional Pentesting: What Security Leaders Need to Know
PTaaSDecember 16, 20258 min read

PTaaS vs. Traditional Pentesting: What Security Leaders Need to Know

Traditional pentests give you snapshots of a moving target. Learn how PTaaS lets security teams test more often, fix faster and finally align pentesting with modern release cycles.

Read article