php2shell (CVE-2026-109618 / 109619): patch, mitigate and hunt in 15 minutes

TL;DR: Two critical PHP flaws, nicknamed php2shell (CVE-2026-109618 and CVE-2026-109619, CVSS 4.0 9.2), let an unauthenticated attacker reach remote code execution through any PHP that serves web requests. Patch to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35 (or your vendor’s backport). If you can’t patch today, set session.upload_progress.enabled = Off. Then hunt for the indicators below. It takes about 15 minutes per server.

What is affected

  • Every PHP that serves web requests: PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. The easiest target is Apache mod_php with the prefork MPM and default memory/input limits. That is what the official wordpress:*-apache Docker images use.
  • The exploit runs before your application code. A WordPress security plugin or PHP-level WAF does not stop it.
  • Old branches: PHP 8.1 and earlier are end-of-life. PHP published minimal source backports for 7.4, 8.0 and 8.1, but most images for those versions will never get an official fix.
  • Exploitation is real but not trivial. Amazon saw attempts four days before disclosure. Sophos, VulnCheck and Hunt.io report scanning, failed attempts and botnet payloads (likely Mirai-based). Reliable exploitation still depends on version and configuration.

Step 1: Find every PHP you run

# on the host
php -v 2>/dev/null; php-fpm -v 2>/dev/null; rpm -qa | grep -i php; dpkg -l | grep -i php

# in every running Docker container
for c in $(docker ps -q); do
  n=$(docker inspect -f '{{.Name}}' "$c")
  v=$(docker exec "$c" php -r 'echo PHP_VERSION;' 2>/dev/null) && echo "$n  PHP $v"
done

# which SAPI / MPM does a container use? (Apache images)
docker exec <container> apache2ctl -M | grep -Ei 'php|mpm'

Don’t forget control panels (cPanel, Plesk), vendor appliances and old staging copies.

Step 2: Patch (the real fix)

  • Upgrade to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, or to your distribution’s patched package.
  • For Docker, pull a fresh image and check the version before you switch:
    docker pull wordpress:php8.3-apache
    docker run --rm --entrypoint php wordpress:php8.3-apache -v   # must show 8.3.36 or later
  • Pin a tag (e.g. php8.3-apache) rather than latest, so a future pull doesn’t jump PHP versions unexpectedly. Back up the database and web root first.

Step 3: If you can’t patch today, apply the workaround

PHP’s own guidance for unpatchable versions is to disable session upload progress. It is a PHP_INI_PERDIR setting, so ini_set() can’t change it. Put it in an ini file and restart:

# /usr/local/etc/php/conf.d/zz-php2shell-mitigation.ini  (official Docker images)
session.upload_progress.enabled = Off

# docker-compose.yml (service volumes)
- ./php2shell-mitigation.ini:/usr/local/etc/php/conf.d/zz-php2shell-mitigation.ini:ro

# verify (expect an empty string or "0")
docker exec <container> php -r 'var_dump(ini_get("session.upload_progress.enabled"));'

This blocks the known paths, not every variant. Keep it in place after patching; it costs nothing.

Step 4: Hunt for indicators

Run these read-only checks on each web server. Adjust log paths to your setup. For Docker, use docker logs <container>.

1. Exploit and scanner traffic in access logs

zgrep -iE 'php2shell|wp2shell|reconx|shellflow|eigen-oppervlak' /var/log/nginx/access.log* /var/log/apache2/access.log*
# multipart boundaries seen in exploit requests (only if you log request bodies / WAF)
zgrep -E 'B7kQpoc618|p2s9rdme2st6guc9g0rafic|PHP_SESSION_UPLOAD_PROGRESS' /var/log/*waf* 2>/dev/null

2. The stage-1 crash signature in PHP/Apache error logs

CVE-2026-109618 is triggered by running out of memory while PHP sets up the request, so the fatal error points to “Unknown”, not a script:

zgrep -E 'Allowed memory size .* in Unknown on line 0' /var/log/apache2/error.log* /var/log/php*-fpm.log*
zgrep -E 'Segmentation fault|exit signal|zend_mm_heap|double free' /var/log/apache2/error.log* /var/log/php*-fpm.log*

A hit means someone tried, not that they succeeded. Check the requests around that timestamp.

3. Shells or downloaders started by PHP workers

ps -eo pid,ppid,user,lstart,args --forest | grep -B1 -A3 -E 'php-fpm|php-cgi|apache2|httpd' \
  | grep -E '\b(sh|bash|dash|curl|wget|nc|ncat|socat|python[0-9.]*|perl)\b|/dev/tcp|mkfifo'
# processes running from temp dirs or deleted binaries
for p in /proc/[0-9]*; do e=$(readlink $p/exe 2>/dev/null); case "$e" in (*deleted*|/tmp/*|/var/tmp/*|/dev/shm/*) echo "${p#/proc/} $e";; esac; done

4. Payloads, webshells and session-file tricks

ls -la /tmp/nfs 2>/dev/null                                          # Potassium/Mirai drop path (Sophos)
grep -rlE "PWNED::|system\(\s*\\\$_GET\[" /var/www 2>/dev/null          # webshell seen by VulnCheck
grep -lE 'upload_progress_.*<\?php' /var/lib/php/sessions/* /tmp/sess_* 2>/dev/null
find /var/www -type f -newermt "2026-10-17" \( -name '*.php*' -o -name '.htaccess' -o -name '.user.ini' \) -not -path '*/cache/*'
wp core verify-checksums --path=/var/www/html                          # WordPress core integrity

5. Known file hashes (SHA-256)

cat > php2shell_sha256.txt <<'EOF'
0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486
150ae88f0999032e2a6113501d6082f54a3ed98d02427b21a96e773a27d7bf16
40caf199ca1ebe056a74427f8e9db06c66899e33ae09c0ec2769ec70bf6dd8e7
27d926a6708d6769d97d065e7a5a860ab496eab272c03d1cebc55ec71b954fa2
1485e14e0f62bc94664eec341cabd275dad7d30b1d496c40ec340c9a0d9f0ba8
EOF
find /var/www /tmp /var/tmp /dev/shm -xdev -type f -size -20M -print0 2>/dev/null \
  | xargs -0 sha256sum 2>/dev/null | grep -Ff php2shell_sha256.txt

6. Network and DNS

ss -tnp | grep -E '92\.118\.112\.100|134\.199\.154\.64|117\.55\.202\.115|45\.198\.224\.'
zgrep -hE '(godofthunder|botlesscucks|gettingheadfromduckysmom)\.st|mangsud\.org' /var/log/* 2>/dev/null

7. YARA (optional)

The community rules from Lexsah/php2shell-detection match the php2shell User-Agent and multipart upload-progress requests. Run them over request captures and logs. For web roots, add the generic webshell rules from Neo23x0/signature-base:

yara -r php2shell_upload_progress.yar /var/log/nginx/
yara -r gen_webshells.yar /var/www/

Indicators at a glance

Type Indicator Source
User-Agent php2shell-scan/109618, php2shell-109618, php2shell-verify/1.0 VulnCheck, Hunt.io
User-Agent PHP2Shell/1.0.0, ReconX-WP2Shell/1.0, wp2shell-audit/3.1 Observed by Neteye
Payload hxxp://92.118.112[.]100/nfs → /tmp/nfs, run as ./nfs php (Potassium, likely a Mirai variant) Sophos
C2 134.199.154[.]64, 117.55.202[.]115, *.godofthunder[.]st, *.botlesscucks[.]st Sophos, Hunt.io
Scanners 45.198.224[.]0/24, 85.11.167[.]243 Hunt.io, VulnCheck
Webshell <?php echo 'PWNED::'; system($_GET['c']); ?> planted via upload progress VulnCheck

IP indicators go stale fast, and some come from shared anonymisation networks. Use them to investigate, not as proof. Check the sources for the full, current lists.

If you find something

  1. Isolate the site: take it off the reverse proxy or firewall it. Don’t power it off yet.
  2. Preserve evidence: web/PHP logs, docker logs (they are deleted when a container is recreated) and a copy of the web root.
  3. Rotate secrets: database passwords, WordPress salts, admin passwords, API keys and SMTP credentials.
  4. Rebuild from a clean, patched image, and restore content from a backup taken before the first indicator.

Sources

Indicators and versions are from public reporting as of 24 October 2026. Need a hand checking your servers? Talk to an engineer.