A network of medical practicesA hacked web server, cleaned up and rebuilt so it can’t spread

16 websites on one shared server had been compromised, one after another. I cleaned out the backdoors and rebuilt the server so every site is walled off from the rest, with the whole setup kept in version control.

The problem

A network of medical practices ran around 16 WordPress sites and a few custom web apps on one shared server. Over about a year they were compromised again and again: rogue administrator accounts, backdoors hidden in a page builder, fake plugins that stole logins, gambling spam shown only to search engines, injected links, and stolen member logins.

Every site ran as the same user, so an attacker who got into one was in all of them. A single administrator account shared across the sites had turned one stolen password into 16 compromised sites. Cleaning one site never lasted, because another still had a way back in.

What I did

I cleaned the sites out, then rebuilt the server so one compromised site can’t touch the others again.

  • Went through every site and removed the backdoors, fake plugins, rogue accounts and injected spam. Files were quarantined rather than deleted, so the evidence and its timestamps were kept, and rogue database rows were exported before they were removed.
  • Rotated every site’s database password and security keys, reset administrator passwords, ended every logged-in session, and replaced the shared administrator account with one account per person.
  • Rebuilt the hosting on a new server where every site has its own Linux user, its own PHP process pool, its own temporary and session folders, its own logs and its own database user. A test script proves the walls hold: no site can read another’s files.
  • Switched off the dangerous parts of PHP, stopped code running from upload folders, and limited each custom app’s database account to the tables it actually uses.
  • Put nginx in front with real visitor addresses from Cloudflare, blocked XML-RPC, rate-limited login attempts and set fail2ban to ban repeat offenders. Only the reverse proxy can reach the web ports, and server logins are by key only.
  • Kept the whole configuration in version control, with an installer, scripts that add and remove a site the same way every time, and a written security baseline with a monthly checklist and a plan for handling any future incident.
  • Set up automated monitoring of every site, so the organisation gets an alert as soon as one goes down, instead of finding out from a visitor.

What changed

  • The sites are clean, and the ways back in are closed.
  • If one site is compromised now, the damage stops at that one site.
  • When a site goes down, the organisation hears about it from an alert, not from a visitor.
  • Adding a site is a script, not an afternoon of copying settings from the last one.
  • Six months of logs are kept, enough to trace an attack back to where it came in.

Under the bonnet

  • Removed: page-builder backdoors, a fake-plugin login stealer, search-engine cloaking, a blockchain-hosted malware loader, injected links
  • nginx, PHP-FPM 8.3 and MySQL 8.0 behind Cloudflare and a reverse proxy
  • One Linux user, PHP-FPM pool, open_basedir, temp and session folder per site
  • Dangerous PHP functions disabled; PHP denied under upload folders
  • One database user per site; table-level grants for custom apps
  • fail2ban with nginx-level bans; rate-limited logins; XML-RPC blocked
  • Uptime Kuma monitoring every site, with alerts to the organisation
  • Configuration, installer, and add-site, remove-site and isolation-test scripts in Git

Services this touched

Got something like this?

A free chat about the problem, then a written quote before any work starts.