Signs of a hacked WordPress site include spam pages or links you did not write, visitors (often only mobile visitors or those arriving from Google) redirected to other sites, admin users you do not recognize, a “This site may be hacked” label in search results, a red Deceptive site ahead warning in browsers, a suspension notice from your host, or files that changed when nobody touched them.
This guide is the order of operations. Speed matters, but order matters more: if you restore a backup before you know how the attacker got in, they walk back in through the same hole, often within days.
Before you start
- Use a clean device. If your own computer is compromised, every new password you type is stolen too. If you have any doubt, change passwords from a different, up-to-date device.
- Access you need: your hosting panel, SFTP or SSH, WP-CLI if available, your DNS and CDN accounts, and access to the email account that receives password resets.
- Keep a log. Write down what you find and what you change, with times. You will need it for your host, for Google’s review and possibly for legal reasons.
- Where to run the commands. Commands in gray boxes (
wp, find, grep) run in an SSH terminal on the server, from the site’s folder: the one that contains wp-config.php. A . in a command means “this folder and everything inside it”, so being in the right folder matters. Most hosting panels offer a terminal or SSH access; on some hosts the command is wp-cli instead of wp. Words in CAPITALS are placeholders for your own values, explained under each command. Most commands here only read and list; the few that change or delete something say so.
Step 1: Contain it
Stop the damage spreading before you investigate.
- Change the passwords that control everything else first: your hosting account, the email account that receives password resets, and your domain registrar. An attacker who holds your email can reset every other password you change.
- Take the site offline or put it behind maintenance mode. If you can, restrict access to your own IP address at the server level. This protects visitors from malware and stops the attacker’s tools running.
- If other sites share the same hosting account, assume they are affected too. Many infections spread across every site the same system user can write to.
Step 2: Preserve evidence
Take a full snapshot of the site as it is now, before you clean anything: all files, a database export (wp db export hacked-site.sql), and the web server access and error logs. hacked-site.sql is just a file name you choose; the export is saved in the folder you run it from. Download everything to storage off the server and label it clearly, then delete the .sql file from the site folder, because anyone who guesses its name could download your whole database. Logs are often rotated or deleted after a few days, so grab them now.
This copy is evidence and a reference for comparison. Never restore it.
Step 3: Check for rogue accounts
Attackers almost always add an administrator so they can get back in. List every admin with WP-CLI:
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
Type it as shown. --role=administrator limits the list to administrators, and --fields picks the columns: the account number (ID), login name, email and the date it was created. Drop --role=administrator to see every user.
Anything you do not recognize, especially one registered recently, is a red flag. Note it as evidence, then delete it:
wp user delete USER_ID --reassign=KEEP_USER_ID
Replace USER_ID with the number in the ID column of the list above for the account you are removing, and KEEP_USER_ID with the ID of a real administrator you trust. --reassign moves that user’s posts and pages to the account you keep; leave it off and WordPress deletes their content. This change is permanent. WP-CLI confirms with a line starting Success: Removed user.
Also check your hosting account for unknown SFTP or FTP users and, if you use SSH, for unknown keys in ~/.ssh/authorized_keys (~ means your home folder). Run cat ~/.ssh/authorized_keys to see them: each line is one key, and the label at the end of the line usually names the person or computer it belongs to.
Step 4: Find what changed
WP-CLI can compare your files with the official copies:
wp core verify-checksums
wp plugin verify-checksums --all
Type both as shown; --all means every installed plugin. A clean result ends with a Success line. Anything listed above it is a file to look at. The first checks WordPress core against WordPress.org checksums and reports files that were modified (“File doesn’t verify against checksum”) or that should not exist. The second does the same for plugins from the WordPress.org directory. Premium plugins cannot be verified this way and will be reported as such, so compare those against a fresh download from the vendor.
Then look for recently changed PHP files:
find . -name "*.php" -mtime -7
find . -name "*.php" -ctime -7
. means search this folder and everything under it, and -name "*.php" limits the search to PHP files. -mtime -7 means modified less than seven days ago. Attackers can fake that date, so the second line uses -ctime -7, the change time, which is much harder to fake. To widen the window, change 7 to the number of days you want to look back, for example -mtime -30. Each line of output is a file path; no output means nothing matched. Expect legitimate files from any plugin, theme or core update in that period, and compare their dates with your update history.
Check the places attackers favor:
- PHP files in uploads. Run
find wp-content/uploads -name "*.php" as shown; it lists every PHP file in the uploads folder. Uploads should hold images and documents, and a PHP file there is almost always a backdoor (a hidden script that lets the attacker back in). A few plugins leave harmless index.php placeholder files; open and check each one.
- wp-config.php. Look for code that does not belong, especially at the very top or bottom, or
include lines pointing to odd files.
- .htaccess files in the site root and in subfolders, for rules that redirect visitors based on where they came from or what device they use.
- wp-content/mu-plugins/, which loads automatically and does not appear in the normal plugin list.
- The active theme’s functions.php and any unfamiliar files in the theme folder.
Obfuscated code is a strong signal. This search finds common patterns, but expect some harmless matches in legitimate plugins:
grep -rlE "eval\(|base64_decode\(|gzinflate\(|str_rot13\(" --include=*.php .
Type it as shown. -r searches every folder below this one, -l prints only the names of matching files, -E allows the | (meaning “or”) between the patterns, and --include=*.php limits it to PHP files. Open each file it lists and look at the matching line: a long unreadable string fed into eval is bad; a well-known plugin using base64_decode for images or settings is usually fine.
Step 5: Check scheduled tasks and the database
WordPress cron can quietly reinstall malware after you delete it:
wp cron event list
It lists every scheduled WordPress task, one per row; the hook column is the task’s name. Look for hook names you cannot match to a plugin you use. Also check the server’s own scheduled jobs (crontab) in your hosting panel.
Malware also hides in the database. Check that the site address has not been changed with wp option get siteurl and wp option get home. Look for injected scripts in posts (replace wp_ if wp db prefix shows a different table prefix):
wp db query "SELECT ID, post_title, post_status FROM wp_posts WHERE post_content LIKE '%<script%' LIMIT 50;"
Run wp db prefix first. If it prints anything other than wp_, replace wp_ in wp_posts with your prefix (for example wpx7_posts). The query lists up to 50 posts (LIMIT 50) whose content contains a script tag. Some will be legitimate embeds such as forms or videos. To read one, run wp post get POST_ID --field=post_content, replacing POST_ID with a number from the ID column.
And list the largest options that load on every page, where injected code and spam settings tend to sit:
wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options WHERE autoload IN ('yes','on','auto-on','auto') ORDER BY size DESC LIMIT 20;"
As above, replace wp_ in wp_options if your prefix differs. It lists the 20 biggest options that load on every page, largest first, with their size in bytes. Names you cannot link to WordPress or a plugin you use deserve a closer look with wp option get OPTION_NAME, replacing OPTION_NAME with a value from the option_name column.
Also scan for spam posts or pages you did not write, often unpublished or with pharmacy, casino or loan keywords.
Step 6: Find the entry point
Use your access logs and the dates you found. Look for POST requests to unusual files around the time the first malicious file appeared, and for repeated requests to wp-login.php or xmlrpc.php. In the common log format, this counts which paths received POST requests:
grep '"POST ' ACCESS_LOG | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
Replace ACCESS_LOG with the path to your access log file. Your hosting panel’s logs section shows where it is, or lets you download it so you can run the command on your own computer (macOS, Linux, or WSL on Windows). If the file ends in .gz, use zgrep instead of grep. The command keeps only POST requests, takes the seventh field of each line (the requested path), counts how often each path appears and shows the top 20, busiest first. Expect wp-login.php, admin-ajax.php and wp-cron.php; an unfamiliar PHP file, especially inside wp-content/uploads, is a lead.
Common entry points are an outdated plugin or theme with a known vulnerability, a reused or leaked admin password, a nulled (pirated) premium plugin, or an old forgotten copy of the site in a subfolder. Check every plugin you use against public vulnerability databases. Whatever you find, that is the hole you must close before restoring.
Step 7: Rebuild from clean sources
Restoring a backup from before the infection is fastest, but only if you know when it started and you close the hole immediately after. Otherwise, rebuild:
- Core: delete
wp-admin and wp-includes and download fresh copies, for example with wp core download --force --skip-content after removing those folders. --force lets it write over files that are already there, and --skip-content leaves out the default themes and plugins so your wp-content folder is not touched. Keep a reviewed wp-config.php.
- Plugins and themes: delete them and reinstall fresh copies from WordPress.org or the vendor (
wp plugin install PLUGIN_SLUG --force, where PLUGIN_SLUG is the plugin’s folder name, the same as the last part of its address on WordPress.org, and --force replaces any copy already installed). Do not copy plugin folders back from the infected site. Leave out anything you do not need.
- Uploads: keep your media, but remove every PHP file and anything else that is not a media or document file.
- Database: remove the injected content and rogue users you found.
Step 8: Rotate every credential
Assume everything stored on the site was read. Change:
- Hosting panel, SFTP and SSH passwords, and remove unknown SSH keys.
- The database password (update
wp-config.php to match).
- WordPress salts, which signs everyone out, including you:
wp config shuffle-salts, typed as shown. It rewrites the secret keys in wp-config.php.
- Every WordPress user’s password, starting with administrators.
wp user reset-password USER_ID resets the password for that account and emails the user; replace USER_ID with a number from the ID column of wp user list, and list several IDs separated by spaces to do more than one at once.
- Email accounts on the domain, and API keys stored in plugins: payment gateways, SMTP services, CDN and backup services.
Step 9: Harden
- Add
define( 'DISALLOW_FILE_EDIT', true ); to wp-config.php (a line in the file, above “That’s all, stop editing”, not a command) to disable the built-in theme and plugin file editors.
- Turn on two-factor sign-in for every administrator.
- Keep only the plugins and themes you use, and keep them updated.
- Set up off-server backups with several weeks of history.
Step 10: Repair your reputation
Open the Security Issues report in Google Search Console. It lists what Google detected and, once you have cleaned up, lets you request a review. Check your domain in Google’s Safe Browsing site status tool and other public blocklists. If your host suspended the account, send them your cleanup notes.
Check your notification duties
If the site stores personal information (customer accounts, orders, form entries, mailing lists), the breach may trigger legal notification duties. In the United States most states have breach notification laws. If you serve people in the EU or UK, GDPR generally requires reporting a qualifying breach to the regulator within 72 hours of becoming aware of it. This is not legal advice: talk to a lawyer promptly, and keep the evidence from Step 2.
When to contact your host
Contact your host early, especially if other sites on the account are affected, if you find files owned by an unexpected system user, or if malware returns after a clean rebuild. Send them the times you noticed each sign, the suspicious file paths and log lines you found, the rogue accounts you removed, and the steps you have already taken.