How we patched 30+ enterprise client sites for a Critical PHP Vulnerability within 24 Hours (and what we found when we looked back)
Last Wednesday started like most days. Then, in the evening, a severity-9.2 PHP vulnerability dropped.
Two CVEs: 109618 and 109619, collectively called PHP2Shell, let an attacker send a single HTTP request containing PHP code, and get it executed on the server. No authentication required. No user interaction. Just a crafted POST request, and if PHP wasn't patched, the game was over.
What made it worse: AWS's threat intelligence network (MadPot) had already detected active exploitation four days before the public disclosure. By the time the advisory hit our feeds, attackers had a four-day head start.
We host production sites for county governments, nonprofits, and enterprise clients. For the most vulnerable ones, we had to move fast.
Key Takeaways
- PHP2Shell lets attackers run code on unpatched servers with a single request.
- We patched 30+ client sites in 24 hours with zero downtime.
- Attackers had a four-day head start. Our audit found no compromise.
- For end-of-life PHP 7.2, disabling session upload progress blocks the exploit.
- WAF rules help early. Only the patch fully fixes it.
The scope of what needed patching
Our client sites run across a mix of environments: containerized services on AWS, dedicated servers, and client-managed cloud setups. All of it had to be patched or mitigated within 24 hours.
Here's roughly what we were looking at:
- 13 ECS services running PHP 8.3 → needed 8.3.36
- 3 ECS services running PHP 8.4 → needed 8.4.27
- An EC2 server hosting a large group of nonprofit sites running PHP 8.2 → needed 8.2.35
- A Government municipal county site on GovCloud running a custom PHP build
- A healthcare client on client-managed cloud
- A few legacy services on end-of-life PHP, handled separately
That last one is a separate problem we're solving, but for everything else, we had a clear path forward.
How we patched the ECS sites (zero downtime)
For the containerized services, we updated the PHP package in each image, rebuilt it, and shipped it through a rolling deployment.
ECS rolling deployments are genuinely elegant for this kind of thing. The new container starts, passes the load balancer health check, and only then does the old one drain and stop. From a user's perspective, nothing happened. We ran this across 16 services, staggered, monitoring each one.
We also added a hard build-fail guard to the Dockerfiles. A PHP one-liner that exits with a non-zero code if the PHP version is below the patched release. If someone tries to build the image without the fix, the build breaks loudly. That felt like the right thing to leave in place permanently.
Dedicated servers: reload instead of restart
On the server hosting the nonprofit sites, we updated PHP and reloaded PHP-FPM instead of restarting it.
The reload sends SIGUSR2 to PHP-FPM, new worker processes start with the updated binary while the existing ones finish handling their current requests. No dropped connections, no downtime.
The public sector site needed a different approach. Its PHP is compiled from source, so we rebuilt it on the patched version and restarted the service.
The audit
Patching was day one. Day two was the harder question: what happened during those four days before disclosure?
We ran a forensic review of every environment covering October 17–22. Here's what we actually checked and why:
PHP session files
This is the primary artifact of PHP2Shell. The exploit writes PHP code into session files at `/var/lib/php/sessions/`. If we found a session file containing `<?php`, that's a direct indicator of a successful injection. We found zero session files on any server, either empty directories or containers that had been recycled during deployment (which destroys local session state).
Web server logs
We searched for all six attacker IPs published by AWS MadPot, plus broader patterns: POST requests returning 5xx errors, high-volume POSTs from single IPs, any multipart uploads to non-expected paths. Zero hits on attacker IPs across all environments.
Webshell scan
If an attacker gains RCE (Remote Code Execution), their next step is usually uploading a PHP webshell to the Drupal files directory. We scanned every `sites/*/files/` path for `.php`, `.phtml`, and `.phar` files. Everything we found was either Drupal core scaffold files (installed by Composer) or Twig template cache — both completely normal.
New admin accounts
We queried the Drupal database directly via Drush for any user accounts created between Oct 17–22 with an administrator role. A backdoor admin account is the typical persistence mechanism after RCE. We found no such accounts on any site.
Verdict: No evidence of compromise on any environment.
A few things worth noting for other teams
Cloudflare WAF deployed PHP2Shell blocking rules roughly nine hours before the public disclosure. Before most teams even knew the CVE existed. If you're running sites behind Cloudflare, that layer was working for you. But WAF rules are a stopgap, not a fix. Attackers iterate on payloads specifically to evade WAF rules; the patch is the only complete answer.
Legacy sites on end-of-life PHP
These sites have no official patch available. We applied a configuration-level mitigation that closes the exploit’s entry point without touching application code. A full PHP upgrade is now a separate track of work.
Final thoughts
The thing that stuck with me from this whole exercise: the four-day window before disclosure matters more than the vulnerability itself. By the time a CVE is public, exploitation is already in progress. The question isn't whether to patch immediately, it's whether your patching process is fast enough and your logging is good enough to know what happened while you were still getting there.
Last updated on October 26, 2026