October 23, 2026
PHP2Shell โ TryHackMe Walkthrough
Prefer the deep-diveโฆ

By Rotimi Ishola
7 min read
Prefer the deep-dive: https://timiphil9.medium.com/php2shell-how-a-progress-bar-became-unauthenticated-rce-on-14-years-of-php-b21d90a4fd6b?sharedUserId=timiphil9
A task-by-task walk through the PHP2Shell room, with the reasoning behind each answer. The exploitation tasks are explained but not reproduced step-by-step โ see the note in Task 5.
23.10.26
Defender's quick checklist
Patch now_ โ PHP 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, then restart every worker. The only complete fix._
Can't patch yet? Set
session.upload_progress.enabled = 0andopcache.enable = 0; on PHP 8.5 alsofatal_error_backtraces = 0. Restart workers โ none is complete alone.
Watch logs for oversized
PHPSESSIDvalues inSet-Cookie, multipart POSTs carryingPHP_SESSION_UPLOAD_PROGRESS, and PHP worker crashes / out-of-memory errors.
Won't help_ โ a mod_php in-process WAF; the attack finishes during request parsing, before your app runs._
Task 1 โ Introduction
What this room is about. On 21 October 2026, PHP shipped an emergency release across every supported branch and disclosed two critical vulnerabilities โ CVE-2026โ109618 and CVE-2026โ109619, both scored 9.2 on CVSS v4.0. Together they allow unauthenticated remote code execution with nothing but ordinary HTTP requests: no login, no file upload the app accepts, no application bug at all. The flaw is in the PHP runtime, below whatever framework sits on top, and has been present since PHP 5.4.0 in 2012 โ fourteen years of releases.
Task 1 just starts the lab machine and has you connect over SSH as the ubuntu user. There's no answer to submit; give the box ~2 minutes to boot and mark it complete once you're connected.
Task 2 โ A harmless feature: upload progress
The idea. When a browser uploads a large file, the page needs a way to show how far it has got. PHP provides session.upload_progress: while it is still reading a multipart/form-data body off the wire, it writes the upload's name, size and bytes-received into the user's $_SESSION, and a second request reads that back to draw a progress bar.
Why it matters later. Two details make this the foundation of the whole attack. First, it is on by default. Second โ and this is the key โ it runs before any application code: the progress callback fires during body parsing, before PHP initialises the request or compiles your script. So reaching it needs no login, no session_start(), and no application logic. Any URL that accepts a POST is enough.
The feature switches on when the request includes a form field whose name matches session.upload_progress.name.
Answer the questions below
What is the default name of the form field that switches on PHP's upload-progress tracking?
PHP_SESSION_UPLOAD_PROGRESS
Task 3 โ The bug: a bailout at the wrong moment
The mechanism. PHP caps per-request memory with memory_limit. When a request exceeds it, the engine can't continue, so it performs a bailout โ a longjmp() straight to a recovery point, abandoning what it was doing. A normal request frees its memory and clears its pointers on the way out; a bailout skips that cleanup. It's an emergency exit, not an orderly shutdown.
The flaw. If the out-of-memory condition hits after a block of memory is freed but before the pointer to it is cleared, the bailout leaves a dangling pointer behind. On its own that's harmless โ the request is dead. What makes it dangerous is the server model: under Apache prefork mod_php, one worker serves request after request without restarting. The stale pointer survives into the next request on that worker, and an attacker who controls what gets allocated into the freed space controls what the pointer now points at.
Answer the questions below
What one-word name does the PHP engine give to the longjmp it performs to abort a request that hits a fatal error such as running out of memory?
Bailout
What is the CWE identifier for the weakness class behind PHP2Shell? (format: CWE-NNN)
CWE-824
(Access of Uninitialized Pointer) โ note that the vendor advisories mostly file it as CWE-416, use-after-free. Both describe the same dangling-pointer-after-bailout defect
Task 4: Reconnaissance: Confirming the Target
From the SSH session, confirm what you're attacking. The vulnerable server runs as a container on 127.0.0.1:8088.
docker ps --format '{{.Names}} {{.Image}} {{.Status}}'
docker exec php2shell-target php -vdocker ps --format '{{.Names}} {{.Image}} {{.Status}}'
docker exec php2shell-target php -v
The server is PHP 8.4.26 โ one release short of the 8.4.27 fix, so it's vulnerable. Now look at the page itself:
curl -s http://127.0.0.1:8088/index.php
docker exec php2shell-target cat /var/www/html/index.phpcurl -s http://127.0.0.1:8088/index.php
docker exec php2shell-target cat /var/www/html/index.php
ls ~/php2shell-bypass-aslr-expls ~/php2shell-bypass-aslr-exp
This is the whole point of the room. The entire application is one line that prints OK โ no login, no upload, no database, nothing to exploit in the code. If the attack works against this, it works against the runtime, and a real WordPress or Laravel site is no safer just because its own code is clean.
Answer the questions below
What version of PHP is the target server running?
8.4.26
What single word does the target page at /index.php print in response?
ok
Task 5: Leaking Memory and Defeating ASLR (CVE-2026โ109618)
A note on the exploitation tasks (5 and 6)._ This room provides a working exploit and has you run it. Because PHP2Shell was disclosed on 21 October 2026 and is being exploited in the wild while many servers are still unpatched, I'm describing what these stages achieve rather than reproducing the commands or the run output. A copy-paste reproduction works against production servers just as well as the lab box, so publishing one during the patch window does more harm than good. If you solved the room, keep your own run notes private for now. The answers below are safe facts a defender needs._
What this stage does. Modern systems randomise where code and data load (ASLR), so an attacker doesn't know addresses in advance. CVE-2026โ109618 strips that away by turning the dangling pointer into a memory leak: the stale pointer is made to describe a string with an attacker-inflated length, and when PHP writes the session cookie, it reads that oversized string out of the worker's memory and returns it to the client. In a real deployment that leaked memory can hold other users' passwords, tokens, API keys and even TLS private keys; for the exploit, it holds the addresses needed to locate the heap and the PHP library, which is all that was missing to defeat ASLR.
Answer the questions below
Which HTTP response header does CVE-2026โ109618 abuse to return leaked process memory to the attacker?
Set-Cookie
Task 6: From a Leak to a Shell (CVE-2026โ109619)
What this stage does. With the addresses known, the second bug turns the corrupted state into execution. PHP calls a destructor โ a function pointer stored alongside a value โ when it tidies that value up. The exploit reoccupies the freed memory with a forged internal structure whose destructor pointer has been aimed at a function inside the PHP library (made locatable by the Task 5 leak). When the engine cleans up, it thinks it's calling a harmless destructor and instead calls into the library with attacker-controlled bytes. The command runs as the web-server user. The whole finish is built from the engine's own code โ no external gadgets needed.
The result is remote code execution from nothing but multipart POSTs to a page that prints OK, against a server with no application vulnerability at all.
As noted in Task 5, I'm not reproducing the exploit invocation or its output here. The flag only exists as the output of running the provided exploit, so there's no "just the flag" that isn't running the weapon โ run the final command yourself in the room to retrieve it.
python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'cat /f>/out/p1' --proof-file out/p1python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'cat /f>/out/p1' --proof-file out/p1
Answer the questions below
When the exploit runs, which system user do the commands execute as?
www-data
(uid 33 โ the Apache/
mod_phpworker user; note the RCE is not root)
What is the flag returned by the exploit?
THM{***} โ retrieve it from your own run; not reproduced here.
Task 7: Detection, Mitigation and the Real Fix
The real fix is to patch the PHP runtime itself โ the distro package or the container base image โ to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, then restart every worker. Updating WordPress, Drupal or Laravel does nothing, because the bug is underneath all of them.
Stopgaps until you can patch (PHP is clear none is a complete substitute): session.upload_progress.enabled = 0 removes the delivery trigger for most reported paths; opcache.enable = 0 frustrates the execution path; on PHP 8.5, fatal_error_backtraces = 0 addresses CVE-2026-109619; rejecting multipart/form-data at the edge blocks delivery but breaks legitimate uploads; and least-privilege limits the damage if the chain lands.
One defence that doesn't help: an in-process (mod_php) WAF never sees the attack, because the damage is done during request parsing before the application runs. Protection has to be the patched runtime or a layer in front of PHP.
Detection signatures: abnormally large PHPSESSID values in Set-Cookie headers; large multipart POSTs carrying the PHP_SESSION_UPLOAD_PROGRESS field; and PHP worker crashes / out-of-memory errors in the logs. The combination is high-confidence.
Answer the questions below
Which PHP 8.4 release fixes PHP2Shell?
8.4.27
Which php.ini directive, set to 0, disables the feature the attack relies on for delivery?
session.upload_progress.enabled
Task 8: Conclusion
PHP2Shell is a reminder that your stack doesn't stop at the application. A single mistake in how the PHP engine recovered from running out of memory โ unnoticed since 2012 โ exposed an unauthenticated path to code execution on a huge share of the web at once, no matter which framework sat on top. The lesson for defenders: treat the runtime as part of your attack surface, and patch it with the same urgency as the app, because a flaw this deep can't be reached by securing the layers above it.
Across the room you saw the harmless upload-progress feature that gave the attack its pre-auth foothold, the memory bailout that left a pointer dangling into the next request, the disclosure that read the server's own addresses out of a cookie header to defeat ASLR, and the forged destructor that finished the job as RCE โ all against a server whose entire application was <?php echo "OK\n"; ?>.
To go deeper, read : https://timiphil9.medium.com/php2shell-how-a-progress-bar-became-unauthenticated-rce-on-14-years-of-php-b21d90a4fd6b?sharedUserId=timiphil9
read: PHP's own security advisories for the engine internals (the room cites GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp โ confirm both on PHP's advisory page before citing), and compare this with memory-safety bugs in compiled languages. The underlying weakness โ a pointer used after the thing it pointed to was gone โ is the same story in a new setting.
Thanks for reading. If this helped, a clap and a follow are appreciated โ I write up security labs and projects regularly. Questions and corrections welcome in the comments.
Follow me for more: