A hacked WordPress site: an outdated plugin and a web shell
An outdated plugin allowed a web shell upload, the site redirected visitors, and Google flagged it. A clean rebuild, updates, secret rotation and hardening.
- Company
- A small firm with a marketing website, 10 people
- Site
- WordPress with several plugins and a contact form
- Maintenance
- An external person maintains it occasionally; nobody tracks updates
- Hosting
- One VPS, Nginx and PHP-FPM, MySQL
01Starting point
The site runs on WordPress with several plugins. One of them, wp-file-manager, is on a version from before the fix. The admin login is publicly reachable, file editing is enabled in the admin, and the WordPress database account has full rights.
Backups exist, but nobody has tried to restore one, and file integrity is not monitored.
- Internet
- Nginx / PHP-FPM
- WordPress (outdated plugin)
- MySQL
02The problem
Customers reported that on mobile the site redirected to a dubious page. Google Search Console flagged the site as harmful and organic traffic dropped.
The cause was CVE-2020-25213 in the wp-file-manager plugin before version 6.9. The plugin exposed a file through which PHP code could be uploaded and run without signing in, that is, remote code execution. The attacker uploaded a web shell, a small PHP file for controlling the server, and injected a conditional redirect into the site (on mobile and from search results) along with spam content for search engines.
Prerequisite: it is enough that the vulnerable plugin is reachable from the internet. This is a publicly known, mass-exploited vulnerability from 2020, nothing we are disclosing here.
A hacked site means the server was under the attacker's control. So it was treated on the basis that nothing on it can be trusted any more.
03Technical root cause
The main cause is an outdated plugin with a known vulnerability. With no update tracking, it stayed open for months after the fix shipped.
The impact was widened by settings: file editing enabled in the admin and a database account with full rights broadened what could be done after the first foothold. PHP could also run from the uploads directory, where the shell landed.
That nobody had tried to restore a backup and integrity was not monitored meant the compromise showed up only through customer complaints, not sooner.
04Investigation
File modification times showed unknown PHP files in the uploads directory and edited theme files. In the database, injected code sat in the settings and in the page content.
The Nginx access logs showed requests to the plugin's file from foreign addresses at the time the unknown files appeared.
Search Console showed which URLs Google had flagged and what it was showing visitors.
The extent was confirmed by comparing against a clean install of WordPress and the plugins from official sources, to make clear what did not belong on the server.
05Remediation
The site was taken into maintenance mode. The shell was not simply deleted: after a compromise there is no reliable way to prove the attacker left no other backdoor, so the site was rebuilt.
The WordPress core, the plugins and the theme were replaced from official sources. Content was restored from a verified clean backup, or the database was cleaned of the injected code. The wp-file-manager plugin was removed, since the site did not need it.
All secrets were rotated: administrator passwords, the database password, the security keys in wp-config.php (which invalidates every login), and the hosting credentials.
// turn off the built-in file editor in wp-admin: no code changes from the browser
define('DISALLOW_FILE_EDIT', true);
// rotate ALL of these after a compromise, with fresh values from the
// WordPress secret-key API; it invalidates every existing login cookie
define('AUTH_KEY', '...'); // also SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY
define('AUTH_SALT', '...'); // also SECURE_AUTH_SALT, LOGGED_IN_SALT, NONCE_SALTRunning PHP from the uploads directory was forbidden, so any further shell dropped there stays inert.
# never run PHP from the uploads directory: a web shell dropped there stays inert
location ~* /wp-content/uploads/.*\.php$ {
deny all;
}The site was hardened: file editing in the admin turned off, the database account cut to only the rights it needs, automatic plugin updates, a second factor and a limit on login attempts for the admin, and file integrity monitoring.
In Search Console the company requested a review so Google would clear the flag.
06Why the fix works
Removing the plugin closes the path the shell was uploaded through. An update would close it too, but the plugin was not needed.
Rebuilding from official sources also removes any backdoor that manual cleaning might miss.
Rotating the security keys in wp-config.php invalidates every login cookie, so the attacker cannot return through an old session.
Forbidding PHP in the uploads directory means that even if another shell lands there, the server will not run it.
File editing off and a least-privilege database account shrink what can be done after a future flaw.
What it does not address: a vulnerability in a plugin you keep, or a flaw in hosting shared with other sites. That is why updates and keeping plugins few matter.
07Validation
- The plugin's file no longer answers an upload; the plugin is removed.
- There is no unknown PHP in the uploads directory, and the server refuses an attempt to run one.
- The injected code is gone from the database and the theme, and the site no longer redirects on mobile.
- After review Google cleared the flag, and file integrity is now monitored against a baseline.
08Results
Fixed
- The path that allowed code upload is closed and the shell removed.
- The injected redirect and spam are gone, and Google no longer flags the site.
Risk reduced
- PHP cannot run from the uploads directory.
- File editing is off and the database account has only the rights it needs.
- Plugins update automatically and file integrity is monitored.
Still open
- For the period before the rebuild, it cannot be shown everything the attacker did with the server.
- As long as the site rests on third-party plugins, each is a possible next path.
09Limitations
- A site's security is only as good as its weakest plugin. Every plugin kept has to be updated.
- Integrity monitoring catches a change only if someone notices the alert and acts.
- On shared hosting a compromised neighbour can endanger a site too. This site runs on its own VPS, which limits that, but does not rule out a flaw in the hosting environment.
10Lessons
- A hacked site means a hacked server. Deleting the shell is not enough; rebuild from clean sources.
- An outdated plugin with a known vulnerability is the most common way into a WordPress site. Update it, or remove it.
- After a compromise, rotate every secret including the security keys, or the attacker returns through an old session.
- Nothing should run PHP in the uploads directory. Forbid it.
- Fewer plugins mean a smaller attack surface. What the site does not need should not be there.
- A backup you have not tried to restore is not verified, and you find a compromise late if you do not monitor integrity.
11Next steps for a small company
- 1.Turn on automatic updates and regularly review which plugins are actually needed.
- 2.A second factor for the admin login and a limit on login attempts.
- 3.Put a protective layer in front of the site (Cloudflare, for instance) and monitor file integrity.
- 4.Verify a restore from backup, so it is certain at the next problem.
Recognise your own company in this?
Tell us what it involves. We reply within one business day and say whether it is work for us, even when the answer is no.