Getting out from under PHP2Shell
Three PHP sites on an unpatchable Debian Buster box, and how they ended up in FreeBSD jails two days later.
The PHP2Shell disclosure (CVE-2026-109618 and CVE-2026-109619) landed while three sites I run were still sitting on an old Debian Buster box: a phpBB forum, a WordPress site, and a set of PHP endpoints that Second Life scripts talk to. Apache with mod_php 7.4, kept on Buster on purpose for stability. That stability turned into a liability overnight. There is no patched PHP 7.4 for Buster, so the only real fix was to leave.
Contain first
Before planning anything, I cut down what an attacker could reach on the old box:
- Pulled every stray admin and debug script out of the webroots: two copies of
adminer.php, twoinfo.phpfiles, a one-off user-creation script, and a phpBBinstall/directory that should have been deleted years ago. - Moved them to a quarantine directory outside the docroot instead of deleting them, so nothing was lost if a site turned out to depend on one.
- Took full backups: every database dump,
/var/www, the Apache, PHP and Let's Encrypt config, plus asha256baseline of the webroots to diff against later.
find /var/www -type f -print0 | xargs -0 sha256sum | tee www-baseline.sha256
If a box can't be patched, its job becomes "stay up long enough to be replaced." Every script you remove is one less thing to worry about during that window.
The new layout
The old machine was far bigger than these sites ever needed, so I picked a slimmer setup: a small FreeBSD VPS. Each site gets its own jail, the database gets one too, and nginx sits in front as the only thing listening publicly.
| Role | Runs | Writable by the web user |
|---|---|---|
| Reverse proxy | nginx, TLS | nothing |
| Forum | PHP-FPM 8.x, phpBB | upload and cache dirs |
| WordPress | PHP-FPM 8.x, WordPress | uploads dir |
| Script endpoints | PHP-FPM 8.x | nothing |
| Database | MariaDB | n/a |
Code is owned by a separate non-web user. PHP-FPM can write only to the upload and cache directories each app actually needs. If a request gets code execution, it can't rewrite the app it runs in, and it can't see the other sites.
The rest is the boring baseline: pf default-deny with only 80, 443 and SSH open, key-only SSH, no root login.
Moving the data
MariaDB dumps went in one database at a time, each with its own user and a fresh password. I deliberately did not restore the old mysql system database; an old grant table is the wrong thing to drag into a new server. It stayed as a reference for which users and grants to recreate.
The Apache rewrite rules were translated for nginx by hand. That was the only part that needed real care, because the Second Life scripts expect exact URLs.
Cutover
- Point the new box at each hostname through
/etc/hostson my machine and test every site end to end. - Take a final fresh dump from the old box, import it, and switch DNS.
- Leave the old box running, locked down, as a fallback until traffic is clearly gone.
The forum moved first, then WordPress, then the script endpoints. All three were live on FreeBSD within two days of starting.
What I'd do sooner next time
- Don't pin a production box to an old release for stability unless you also have a plan for the day it stops getting fixes.
- Delete installers and admin tools as soon as setup is done. Every one I quarantined this week had been sitting in a webroot for years.
- Give each app its own process and filesystem boundary from day one. Jails made that cheap.