php2shell: three chains BETA
Same advisory, three different attacks. They need different targets and prove different things.
A result belongs to the chain that produced it. "php2shell works" alone means nothing.
| Chain | Needs | Proves |
|---|---|---|
| A — application sink | An include parameter, writable session dir | Code as the web user, in the app |
| B — CVE engine chain | mod_php prefork, one worker, display_errors, unpatched | Command output on the host (no sink) |
| C — availability on FPM | FPM pool, body above memory_limit | Take-down only, never access |
Chain A — the application's own sink
Upload progress writes a file; the app includes it. No memory corruption, no offsets.
- An include / LFI parameter the app already has, discovered not assumed
- A writable session directory
- A body larger than memory_limit
- Works on PATCHED PHP — it is not the 2026 CVE
php2shell lfi https://site/ --attempt --upload / --lfi "https://site/?page={page}"
A marker we uploaded executes as the web user. The finding is the application's: so is the fix.
Calling this "php2shell RCE" is wrong: the PHP patch does not stop it.
Chain B — the CVE engine chain
Out-of-memory plus stale request state corrupts freed memory; a fake HashTable with pDestructor = system() runs a command.
- mod_php under Apache prefork, not FPM
- The same worker must serve both requests: one worker, or pinning. Multiple workers are fine
- display_errors on, memory_limit small vs body, session.upload_progress on, use_strict_mode off
- Unpatched 8.3 / 8.4 / 8.5 and a build the offsets fit
php2shell-chain https://site/index.php --attempt -c "id>p.txt" --fetch /p.txt
PROOF: uid=33(www-data) — command output, with the leaked chunk and libphp bases before it.
Two smaller steps come first, a 4096-byte Set-Cookie leak and a libphp leak. Leaks without a PROOF mean wrong layout, not a safe site.
Proven on the 8.3 / 8.4 / 8.5 official builds; the build is discovered by sweeping the table.
Chain C — availability on FPM
Oversized form fields exhaust the pool; FPM exits each worker with code 70 and replaces it.
- PHP-FPM, any pool size
- memory_limit below the body, no edge body cap
- N requests empty a pool of N
php-dos https://site/ --attempt --workers 20 --mb 12
Request failures and latency. It never yields a shell, and we never report it as access.
This is the honest answer when chain B cannot apply: take-down possible, code execution no.
Which one will the run use?
The plan decides, in this order:
- A sink is recorded for the target → chain A first: seconds, no offsets, works on patched PHP
- No sink, mod_php prefork, unpatched → chain B
- No sink, FPM → chain C, reported as availability only
- A chain never inherits another chain's result
Unverified or out of reach: leak shapes for PHP 5.4–8.2, display_errors=Off, Windows. No PROOF line means not proven, never "safe".