Clean up after a hack
A site of yours has been hacked, and you need it clean and safe again.
This article describes how to restore from a backup, what to do when restoring isn't an option, and when you have to report the hack to the authorities.
Restoring is safer than cleaning by hand
An attacker almost never leaves just one file behind. A typical hack leaves several: a backdoor hidden inside a plugin directory, a second one somewhere in the uploads folder, a modified CMS core file, an extra administrator account, and sometimes a scheduled task that downloads everything again.
Cleaning by hand means finding every one of those yourself. If you miss a single file, the attacker still has a way back in, and the site is hacked again within days.
Restoring from a backup replaces the whole site at once with a copy from before the hack. Everything the attacker added is gone, whether you found it or not.
Cleaning by hand is never certain. Do it only when restoring isn't realistic — for example a webshop, where going back to yesterday's backup would throw away a day of orders, customer accounts and stock changes.
Step 1: stop the malicious code that's still running
Do this first, on both routes.
Malicious code often runs inside a PHP-FPM worker that stays alive in memory. While that worker is running, it rewrites files the moment you replace them, so your cleanup or your restore is undone within seconds.
Restart the FPM pool that serves the site. A restart kills every worker for the pool and starts fresh ones, so the malicious process is gone too. A 'Reload' is not enough: it lets the current request finish first, which gives the malicious code time to write the files back.
See Restart and reload for both routes to the 'Restart' button, and File permissions keep reverting after a hack for the full picture.
Step 2: restore from a backup
Restoring is done on the 'Backups' page of the UNIX user, the database, or the project. For where to find it, what the archive list shows, and how restoring a whole archive differs from restoring a single directory, see Working with your backups.
Three things matter when the reason for the restore is a hack.
Pick an archive from before the hack
If you're not sure when the site was first compromised, choose an older archive rather than a newer one. A backup taken after the attacker got in contains the attacker's files too, and restoring it leaves you exactly where you started.
You can browse a files archive before restoring it, so you can check whether the backdoor is already in there.
Restore both the files and the database
Attackers plant things in both. A backdoor sits in the files; an extra administrator account, or injected content such as spam links, sits in the database. Putting back only one of the two leaves half the hack in place.
There may be nothing to restore from
Restoring only works for a UNIX user or database that has a Borg repository. Without one, the 'Backups' item is disabled and shows 'No backup repositories configured'. In that case, ask Cyberfusion support whether a restore from the local backups on the master node is possible, and continue with the manual cleanup below in the meantime.
Update the software before you consider it done
A restored site runs the same software version it ran before the hack, so it still has the same hole. Continue with step 3, otherwise the site will be hacked again.
Step 3: update the CMS and everything around it
Update the CMS core, all plugins, all themes, and any framework or library your application uses, to a version that still gets security updates.
An outdated CMS or plugin is the most common way in. Attackers scan the internet for sites running versions with a publicly known weakness, so a site usually gets hit because of the version it runs, not because someone picked it out.
The 'CMS Versions' page under 'WebOps' lists every WordPress installation Cyberfusion found on your clusters and marks the ones running a version that no longer gets security updates. See CMS versions.
Step 4: replace the sensitive information stored in the site
Anyone who could read your files could read every secret in them. Treat all of them as known to the attacker and replace them:
- The database password, usually in a
.envfile or a configuration file such aswp-config.php. Change it under 'Advanced' > 'Database Users': select the database user, and click 'Change' in the 'Password' panel. Then set the new password in the site's configuration file as well, otherwise the site can't connect to its database any more. - API keys and tokens of external services: payment providers, mail services, shipping, CRM, and anything else the site talks to. Generate new ones with the service in question and revoke the old ones.
- Mailbox passwords, if the site sends mail with a mailbox password in its configuration.
The UNIX user's own password, used for SSH and SFTP, usually doesn't need to be changed. The attacker almost always gets in through outdated website software, not by logging in with that password.
Change it anyway if there's an indication the login itself was used. The node records every SSH login: the time, the UNIX user, the IP address it came from, and the fingerprint of the SSH key. A login you can't account for, or one from an unexpected country, is a reason to change the password and to remove SSH keys you don't recognise. See SSH logins on a node and Change or disable a UNIX user's password.
Step 5: remove users you don't recognise
Attackers create an extra account so they can get back in after you've patched the hole.
Check, in the CMS itself, the list of users, and delete every administrator you didn't create. Check the ordinary user accounts too, since an attacker can raise a normal account's permissions later.
Also check, in Core:
- The FTP users on the UNIX user. See All about FTP users.
- The SSH keys on the UNIX user. See All about UNIX users.
- The crons on the UNIX user. A cron that downloads the backdoor again is a common way for a hack to come back. See All about crons. If the UNIX user has shell access, also run
crontab -lover SSH.
WordPress
Two extra steps apply to WordPress.
Remove plugins you didn't install
Delete every plugin you don't recognise, including the ones in wp-content/mu-plugins/.
Those are 'must-use' plugins. WordPress loads every PHP file in that directory on every single request, and there's no way to deactivate them from the WordPress interface — you have to delete the file. A backdoor placed there keeps running even after you've deactivated everything else, which is exactly why attackers put it there.
Rotate the salt keys
The salt keys in wp-config.php are the secret values WordPress uses to sign login sessions. Replacing them makes every existing session invalid, so everyone who is logged in — including an administrator account the attacker created, and including the attacker's own browser session — is logged out immediately.
Generate a fresh set at api.wordpress.org/secret-key/1.1/salt and replace the corresponding lines in wp-config.php. You'll have to log in again yourself afterwards.
Check the malware list afterwards
The cluster runs a daily malware scan (maldet) across UNIX user home directories. Hits appear under 'Malware', on the UNIX user page and on the project page, with the file path and a 'Last Seen' timestamp. See Malware scanning.
Use the list to find malicious files you haven't cleaned up yet. A record disappears automatically once the file is no longer on disk, so an entry that stays is a file that's still there.
The scan covers files modified in the last day, so give it a day after your cleanup and check again. An entry that reappears means the attacker still has a way in.
Reporting a data breach
If the site or application stores personal data, a hack is a data breach, and you have legal obligations beyond cleaning up.
Under the GDPR, you have to report the breach to the data protection authority without undue delay, and where possible within 72 hours of finding out about it. In the Netherlands that authority is the Autoriteit Persoonsgegevens; in other EU countries it's that country's own data protection authority.
You also have to inform the people whose data was involved, when the breach is likely to put them at high risk — for example when passwords, addresses, or payment details were exposed.
This is the site owner's obligation, not the hosting provider's. If you build and manage sites for clients, agree in advance who reports what.
Organisations that fall under NIS2 have further obligations on top of this. See NIS2 voor webbureaus, tech agencies en SaaS (in Dutch).
Related concepts
- File permissions keep reverting after a hack: why your uploads and permission changes are undone within seconds, and how to stop it.
- All about backups: the three backup tracks, and which one you can restore from yourself.
- CMS versions: which of your WordPress installations run a version that no longer gets security updates.
- Security and auditing: what a node records about SSH logins, and what Core records about every change.
- Malware scanning: the daily scan and where its hits show up.
- Staging environments and cloning: a second copy of the site, useful for testing an update before putting it on production.