A hacked WordPress site does not fix itself and does not get better with time. Every hour the site stays compromised, the damage compounds. Search engine rankings drop. Visitors see warnings or malicious redirects. The attacker can deepen access or sell it to other attackers.
This guide covers every step in the correct order. Start at Step 1 even if you think you know where the problem is. Skipping steps is how sites get re-hacked because the initial access point was not found and closed.
Before You Start: What You Will Need
- SSH access to your server (or cPanel File Manager if on shared hosting)
- MySQL access (command line or phpMyAdmin)
- WP-CLI installed on the server (recommended)
- A clean local machine to work from (use a different device if possible)
- Access to your domain registrar and DNS provider
- Your hosting account login
- All plugin and theme license keys (you will reinstall from scratch)
Phase 1: Detect and Confirm the Hack
Step 1: Confirm the Site Is Actually Hacked
Before doing anything else, confirm what you are dealing with. Not every site problem is a hack.
Check the site in an incognito browser window. Some malware only redirects visitors who arrive from Google search, not direct visits. Use incognito to get a fresh session with no cached data.
Check from a different device or network. Some attacks target specific user agents or IP ranges.
Run an external malware scan:
# Visit this URL in your browser and check the results
https://sitecheck.sucuri.net/?scan=yourdomain.com
Sucuri SiteCheck scans your site remotely for known malware, blacklist status, and visible injected code. It does not find everything, but it confirms obvious infections and gives you a starting report.
Signs that confirm a hack:
- Browser shows a red security warning when visiting the site
- Google search results show your pages with spam titles or descriptions
- Visitors are redirected to a different site
- Admin login shows an unexpected email address or unknown administrator
- Hosting provider suspended the account with a malware notice
- Google Search Console shows a security issue notification
- The site serves content you did not create
Step 2: Check Google Safe Browsing Status
Google maintains a list of sites flagged for malware, phishing, and unwanted software. If your site is on this list, Chrome, Firefox, and Safari all show red warning pages to visitors.
Check your site at the Google Safe Browsing report. Enter your domain and check the result.
If the site is flagged, note the date of the first flag. This gives you a time boundary for the infection. Backup files from before this date are more likely to be clean.
Step 3: Document Everything Before Touching Anything
The actions you take in the first 10 minutes can destroy evidence you need later. Before changing anything, take screenshots and save logs.
Screenshot the following:
- The current state of the site in a browser
- The WordPress admin user list if you can access it
- Any error messages or warnings
- Your hosting control panel showing account status
- Any suspicious files visible in file manager
Save these server logs before any cleanup:
# Copy access logs before they rotate or get cleared
sudo cp /var/log/nginx/access.log /tmp/pre-cleanup-access.log
sudo cp /var/log/nginx/error.log /tmp/pre-cleanup-error.log
sudo cp /var/log/auth.log /tmp/pre-cleanup-auth.log
This evidence helps identify the attack vector and is required if you need to involve a hosting provider, insurer, or legal process.
Step 4: Take the Site Offline
While you investigate and clean, the infected site is actively harming visitors. Take it offline.
On a self-managed server with Nginx:
sudo systemctl stop nginx
On shared hosting without SSH, create a maintenance page by renaming your index.php temporarily and uploading a simple index.html that says the site is undergoing maintenance.
If the site processes orders or bookings, notify your team immediately. Document the time the site went offline for support tickets and incident records.
Phase 2: Contain the Breach
Step 5: Change Every Password Immediately
Do this from a clean device, not the one you normally use to manage the site.
Change these credentials in this order:
WordPress admin passwords for all users. Go to WordPress admin (or run via WP-CLI). Change every admin account password. Use a password manager to generate 20-character random passwords.
Hosting account password. Log into your hosting provider and change the main account password.
Database password. Change it in both MySQL and in wp-config.php.
FTP and SFTP passwords. Every FTP user on the account needs a new password.
SSH keys. If any SSH key may have been compromised, revoke it and add a new one.
Email account passwords. Any email account associated with the hosting or domain management.
cPanel or control panel password if applicable.
Via WP-CLI for WordPress users:
# Change admin password
wp user update 1 --user_pass="new_strong_password_here"
# Or change by username
wp user update admin --user_pass="new_strong_password_here"
Step 6: Regenerate WordPress Secret Keys and Salts
WordPress salts are used to secure session cookies. Regenerating them immediately invalidates all active sessions. Every logged-in user, including any attacker with a valid session, gets logged out.
# Regenerate salts via WP-CLI
wp config shuffle-salts
Or manually replace the keys in wp-config.php with fresh ones from https://api.wordpress.org/secret-key/1.1/salt/.
Step 7: Revoke External Access
Check for and revoke any API keys, OAuth tokens, or application passwords that may have been issued.
In WordPress admin go to Users, then your profile, scroll to Application Passwords and revoke all existing application passwords.
Check for any connected third-party services (social media tools, SEO tools, backup services) that have admin-level access to WordPress. Revoke them all and reconnect with fresh credentials after cleanup.
In your hosting control panel, check for any authorised third-party integrations or API access and revoke them.
Phase 3: Investigate the Breach
Step 8: Identify All Administrator Accounts
In a fresh WordPress installation, only your known admin users should have the administrator role. Attackers frequently create hidden administrator accounts.
wp user list --role=administrator
Or check directly in MySQL:
SELECT u.ID, u.user_login, u.user_email, u.user_registered, m.meta_value
FROM wp_users u
JOIN wp_usermeta m ON u.ID = m.user_id
WHERE m.meta_key = 'wp_capabilities'
AND m.meta_value LIKE '%administrator%'
ORDER BY u.user_registered DESC;
Any user you do not recognise is suspicious. Note their registration date before deleting.
Step 9: Find Recently Modified Files
Attackers modify or create PHP files to install backdoors. Find files modified recently compared to your normal activity pattern:
# Files modified in the last 7 days
find /var/www/yourdomain.com -name "*.php" -mtime -7 -type f
# Files modified in the last 14 days
find /var/www/yourdomain.com -name "*.php" -mtime -14 -type f | head -50
# PHP files in the uploads directory (should not exist)
find /var/www/yourdomain.com/wp-content/uploads -name "*.php" -type f
# Any executable files in uploads
find /var/www/yourdomain.com/wp-content/uploads -name "*.php" -o -name "*.js" -type f
Any PHP file in the uploads directory is malicious. Uploads should contain only media files.
Step 10: Scan for Malicious Code Patterns
Common malware patterns appear in PHP files as obfuscated code. Search for them:
# Base64 encoded payloads (very common)
grep -rl "eval(base64_decode" /var/www/yourdomain.com --include="*.php"
# Gzip obfuscation
grep -rl "gzinflate" /var/www/yourdomain.com --include="*.php"
# System command execution
grep -rn "exec\|system\|passthru\|shell_exec\|popen" /var/www/yourdomain.com --include="*.php" | grep -v "wp-includes\|wp-admin"
# File write functions combined with base64
grep -rl "file_put_contents.*base64" /var/www/yourdomain.com --include="*.php"
# Remote code inclusion
grep -rl "allow_url_include\|allow_url_fopen" /var/www/yourdomain.com --include="*.php"
Save the output of each command. Every file flagged needs manual review.
Step 11: Check the Database for Injected Content
Attackers inject malicious JavaScript or PHP into post content, widget content, and WordPress options.
# Connect to MySQL
mysql -u your_db_user -p your_database_name
Run these queries:
-- Check posts for injected scripts
SELECT ID, post_title, post_status
FROM wp_posts
WHERE post_content LIKE '%<script%'
OR post_content LIKE '%eval(%'
OR post_content LIKE '%base64%'
OR post_content LIKE '%document.write%'
LIMIT 20;
-- Check options table for injected code
SELECT option_name, option_value
FROM wp_options
WHERE option_value LIKE '%eval(%'
OR option_value LIKE '%base64_decode%'
OR option_value LIKE '%<script%';
-- Check for spam links injected in content
SELECT ID, post_title
FROM wp_posts
WHERE post_content LIKE '%<a href=%'
AND post_type = 'post'
AND post_status = 'publish'
ORDER BY post_modified DESC
LIMIT 20;
Step 12: Analyse Server Access Logs
The access log shows every HTTP request. Attackers leave traces.
# Find suspicious POST requests to PHP files
grep "POST" /var/log/nginx/access.log | grep "\.php" | sort | uniq -c | sort -rn | head -30
# Find requests that returned 200 from unusual PHP files
awk '$9 == 200' /var/log/nginx/access.log | grep "\.php" | awk '{print $7}' | sort | uniq -c | sort -rn | head -30
# Find the IPs making the most requests
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Look for web shell access patterns
grep -E "c99|r57|b374k|weevely|webshell" /var/log/nginx/access.log
# Find requests to files that no longer exist but were accessed
grep " 404 " /var/log/nginx/access.log | grep "\.php" | sort | uniq -c | sort -rn | head -20
Look for requests to PHP files in unexpected locations, particularly in wp-content/uploads/, wp-content/themes/, or the root directory.
Step 13: Verify WordPress Core File Integrity
WP-CLI can compare your WordPress core files against the official checksums:
wp core verify-checksums
Any file that fails the checksum check has been modified. This is either a hack or a manual modification you made intentionally. Any core file that should not have been modified is a backdoor.
For an even more thorough check, compare your WordPress files against a fresh download:
# Download a fresh copy of WordPress matching your version
wp core version
wget https://wordpress.org/wordpress-VERSION.tar.gz -O /tmp/wp-fresh.tar.gz
tar -xzf /tmp/wp-fresh.tar.gz -C /tmp/wp-fresh/
# Compare a specific file you suspect
diff /var/www/yourdomain.com/wp-login.php /tmp/wp-fresh/wordpress/wp-login.php
[IMAGE: HACK INVESTIGATION FLOW. A decision tree starting from a central box labeled Site Hacked. Four branches extend outward: Branch 1 labeled Check user accounts leads to Remove unauthorised admins. Branch 2 labeled Check modified files leads to Identify backdoors and injected code. Branch 3 labeled Check database leads to Remove injected scripts and spam links. Branch 4 labeled Check access logs leads to Find attack entry point and attacker IP. All four branches feed into a central action box labeled Clean and reinstall]
Phase 4: Clean the Site
Step 14: Create a Forensic Backup Before Cleaning
Before removing anything, create an archive of the infected state. You may need it later to understand what happened, for insurance claims, or to discover additional injected files you missed in the initial scan.
tar -czf /tmp/infected-site-backup-$(date +%Y%m%d).tar.gz /var/www/yourdomain.com/
mysqldump -u db_user -p database_name | gzip > /tmp/infected-db-$(date +%Y%m%d).sql.gz
Move these to a location outside the web root. Do not serve them.
Step 15: Remove Malicious User Accounts
Delete every administrator account you did not create:
# Delete by user ID
wp user delete USER_ID --reassign=1
# Or in MySQL directly
DELETE FROM wp_users WHERE ID = SUSPICIOUS_ID;
DELETE FROM wp_usermeta WHERE user_id = SUSPICIOUS_ID;
Step 16: Remove Malicious Files
PHP files found in the uploads directory must be deleted immediately:
find /var/www/yourdomain.com/wp-content/uploads -name "*.php" -delete
find /var/www/yourdomain.com/wp-content/uploads -name "*.phtml" -delete
For files in the WordPress installation directory that contain obvious malware patterns and do not belong to core, plugins, or themes:
# Remove files with known web shell names
find /var/www/yourdomain.com -name "c99.php" -delete
find /var/www/yourdomain.com -name "shell.php" -delete
find /var/www/yourdomain.com -name "r57.php" -delete
# For any specific malicious file identified in Step 10
rm /path/to/infected/file.php
Step 17: Reinstall WordPress Core From Scratch
Do not try to clean individual core files. Replace the entire core with a fresh download.
# Download and reinstall core (keeps wp-config.php and wp-content)
wp core download --force
The --force flag overwrites all existing core files with fresh copies from the official WordPress repository. Your wp-config.php, wp-content/, and any files in the root that are not part of core are left untouched.
Verify the reinstall:
wp core verify-checksums
All core files should now pass checksums.
Step 18: Reinstall All Plugins From Scratch
Do not trust your existing plugin files. Reinstall every plugin from its official source.
First, list all currently installed plugins:
wp plugin list
Then reinstall each one from scratch:
# Reinstall a specific plugin
wp plugin install akismet --force
wp plugin install woocommerce --force
wp plugin install contact-form-7 --force
# Activate them again
wp plugin activate akismet
For premium plugins not available from the WordPress repository, download fresh copies from the developer’s site using your license key. Do not reinstall from your existing files.
Delete any plugin you no longer use:
wp plugin delete plugin-name
Step 19: Reinstall All Active Themes
Apply the same approach to themes. Reinstall from official sources.
wp theme install twentytwentyfour --force
For custom or premium themes, download a fresh copy from the theme developer. Do not reuse your existing theme files without reviewing every PHP file for injected code.
Step 20: Clean the Database
For posts flagged in Step 11 with injected scripts, manually review each one and remove the injected content. Use the WordPress editor or a direct SQL update:
# Review the content first
SELECT post_content FROM wp_posts WHERE ID = SUSPECT_ID;
# Remove injected script tags (example)
UPDATE wp_posts
SET post_content = REPLACE(post_content, '<script src="http://malicious.com/bad.js"></script>', '')
WHERE ID = SUSPECT_ID;
For injected content in the options table:
# Verify the option before changing
SELECT option_value FROM wp_options WHERE option_name = 'suspicious_option';
# Update with clean value
UPDATE wp_options SET option_value = 'clean_value' WHERE option_name = 'option_name';
If the injected content is widespread across many posts, use a search and replace:
wp search-replace 'malicious_string' '' --all-tables
Always preview before confirming:
wp search-replace 'malicious_string' '' --all-tables --dry-run
Step 21: Fix File Permissions
Incorrect permissions that allowed the hack may still be in place. Reset to correct values:
# Directories
find /var/www/yourdomain.com -type d -exec chmod 755 {} \;
# Files
find /var/www/yourdomain.com -type f -exec chmod 644 {} \;
# wp-config.php should be more restrictive
chmod 640 /var/www/yourdomain.com/wp-config.php
# Set correct ownership
sudo chown -R www-data:www-data /var/www/yourdomain.com
Step 22: Restore From a Clean Backup If Available
If you have a verified backup from before the infection date (confirmed earlier in Step 2), use it instead of the manual cleaning steps above.
Restoring from a clean backup is more reliable than trying to remove every trace of injected code manually. The challenge is confirming the backup is actually clean. Review the investigation findings from Steps 8 to 13. If the infection predates your oldest backup, a clean backup does not exist and manual cleaning is the only path.
The server recovery process covers the detailed restore procedure including database import verification, file permission reset, and post-restore testing.
After restoring from backup, still apply Step 5 (change all passwords), Step 6 (regenerate salts), Step 15 (check user accounts), and the hardening steps below. The backup restores the clean content but not the security improvements.
Phase 5: Harden and Prevent Re-infection
Step 23: Add wp-config.php Protections
Prevent direct browser access to wp-config.php by adding this to your Nginx configuration:
location = /wp-config.php {
deny all;
return 404;
}
Also block xmlrpc.php unless you specifically need it:
location = /xmlrpc.php {
deny all;
return 404;
}
Step 24: Protect the Uploads Directory
PHP should never execute from the uploads directory. Enforce this in Nginx:
location ~* /wp-content/uploads/.*\.php$ {
deny all;
return 403;
}
This prevents any PHP file placed in uploads from being executed, even if an attacker successfully uploads one.
Step 25: Enable Two-Factor Authentication
Install and activate a two-factor authentication plugin such as WP 2FA or Two Factor:
wp plugin install wp-2fa --activate
Enforce 2FA for all administrator and editor accounts. A compromised password alone is no longer sufficient to gain admin access.
Step 26: Configure a Web Application Firewall
A WAF intercepts attack requests before they reach WordPress. Add your site to Cloudflare and enable the managed WAF ruleset, or install and configure ModSecurity on the server.
The layers of web application firewall protection that matter most after a hack are the rules blocking file upload exploits, PHP code injection attempts, and wp-login.php brute force attacks.
Create a Cloudflare WAF rule to block direct PHP execution attempts in the uploads directory:
URI Path starts with /wp-content/uploads AND URI Path ends with .php
Action: Block
Step 27: Install and Configure a Security Plugin
Install Wordfence or Sucuri Security after completing the manual cleanup:
wp plugin install wordfence --activate
Run a full scan immediately after installation. This provides a second confirmation that the manual cleanup was complete.
Configure Wordfence with:
- Login rate limiting enabled
- Real-time IP blocklist enabled
- File change monitoring enabled
- Email alerts for new admin account creation
Step 28: Harden PHP Settings
Add these settings to php.ini or a .user.ini file in the web root:
# Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
# Disable remote file inclusion
allow_url_include = Off
allow_url_fopen = Off
# Hide PHP version from headers
expose_php = Off
Restart PHP-FPM to apply:
sudo systemctl restart php8.1-fpm
Step 29: Review and Remove Unused Plugins and Themes
Every inactive plugin and theme that remains installed is an attack surface even when deactivated. Deactivated plugins still have their files on disk. A file upload exploit or a directory traversal attack can reach them.
# List all inactive plugins
wp plugin list --status=inactive
# Delete them
wp plugin delete plugin-name
# List all themes
wp theme list
# Keep only active theme and one default theme as fallback
wp theme delete old-unused-theme
Step 30: Set Up Automated Backups Going Forward
After a hack, establish proper backup infrastructure immediately. This was likely missing before the incident.
Configure daily backups that upload to off-site object storage. Verify the backup runs successfully and set up Healthchecks.io monitoring so you receive an alert if the backup job fails silently.
The security requirements for production hosting include verified off-site backups as a baseline, not an optional extra.
Phase 6: Verify the Cleanup Is Complete
Step 31: Run a Full Security Scan
Run multiple scanners since each one catches different patterns:
# WP-CLI checksum verification
wp core verify-checksums
wp plugin verify-checksums --all
# Run Wordfence scan from admin
# Go to Wordfence > Scan > Start New Scan
Also run an external scan:
https://sitecheck.sucuri.net/?scan=yourdomain.com
If either scan returns findings, return to Phase 4 and address what was missed.
Step 32: Test Every Site Function
Before bringing the site back online, test all critical functionality:
- Homepage loads without errors
- Blog posts display correctly
- Contact forms submit and deliver email
- User registration and login works
- If WooCommerce: add to cart, checkout, payment processing
- WordPress admin functions correctly
- Media uploads work
- All menu navigation works
Step 33: Restart Web Services and Bring Site Online
After confirming everything is clean and functioning:
sudo nginx -t
sudo systemctl start nginx
sudo systemctl restart php8.1-fpm
Monitor the access log for the first 30 minutes:
sudo tail -f /var/log/nginx/access.log
Look for any requests to suspicious PHP files or unusual POST request patterns that might indicate the attacker is probing the site again.
Phase 7: Google Recovery and Ongoing Monitoring
Step 34: Check and Resolve Google Blacklist Status
If Google flagged the site in Step 2, you must request a review after cleanup. Google will not automatically remove the flag.
Log into Google Search Console for your domain. Go to Security and Manual Actions. If there is a security issue listed, review the details.
After confirming the site is clean, click Request Review. Provide a clear description of what was found, how it was cleaned, and what measures were put in place to prevent recurrence.
Google’s review typically takes 24 to 72 hours. During this time the red warning pages remain for visitors.
Step 35: Submit an Updated Sitemap
After cleanup, submit your sitemap to Google to prompt recrawling of your pages. This helps Google see the clean versions of your content faster.
In Google Search Console go to Sitemaps and resubmit your sitemap URL (typically https://yourdomain.com/sitemap.xml).
Step 36: Monitor for Re-infection
Set up monitoring that catches re-infection before visitors do.
Enable Wordfence email alerts for file changes and new administrator accounts.
Set up a Google Search Console alert for new security issues.
Configure uptime monitoring with content checking.
UptimeRobot can check for specific strings on your pages. Add a check that looks for the absence of known spam phrases. If a page suddenly contains a string like casino or pharmacy, the monitor fires an alert.
Schedule a weekly WP-CLI scan:
# Add to crontab
0 6 * * 1 wp --path=/var/www/yourdomain.com core verify-checksums >> /var/log/wp-security-scan.log 2>&1
Step 37: Conduct a Post-Incident Review
After recovery, document what happened and what the attack vector was.
Answer these questions:
Which plugin, theme, or configuration allowed the initial access? Was it an outdated plugin with a known CVE? A brute-forced password? A vulnerable file upload handler?
How long was the site compromised before detection? The gap between infection and detection determines what backup is actually clean.
What monitoring would have detected this earlier? Install that monitoring now.
What single change would most reduce the likelihood of this attack succeeding again?
This review should produce at least three actionable changes to the site or server configuration. Run those changes before considering the incident closed.
Frequently Asked Questions
How do I know if the hack is fully cleaned?
Run multiple scanners immediately after cleanup: wp core verify-checksums, the Wordfence full scan, and Sucuri SiteCheck externally. If all three report clean, the cleanup is complete with high confidence. No scanner catches everything, so also manually review the access logs for recurring suspicious request patterns over the 48 hours after cleanup. A re-infection within 24 hours almost always means the initial access point was not fully closed.
Should I rebuild from scratch or clean the existing installation?
Rebuilding from scratch is safer but takes longer. Cleaning the existing installation is faster but carries more risk of missing a well-hidden backdoor. For high-value sites or sites that handle payment data, rebuilding from scratch is worth the time. For content sites where user data risk is lower, thorough cleaning using the steps in this guide is acceptable. If budget allows, rebuilding from scratch and then importing only the database (after scanning it) is the most reliable path.
My hosting provider suspended my account for malware. What do I do?
Most hosting providers suspend accounts that serve malware to protect other customers on the shared server. Contact support immediately and inform them you are working on cleaning the site. Request access to the files even with the site suspended so you can complete the investigation and cleanup. Most providers will restore access once you confirm cleanup is complete. Submit a malware removal report to them showing what was found and removed. Some providers charge a malware cleanup fee. If your account was suspended on shared hosting, this incident is also a strong signal to evaluate a VPS or managed WordPress hosting where you have direct server access during incidents.
How long does Google take to remove the malware warning?
After submitting a review request in Search Console, Google typically responds within 24 to 72 hours for straightforward cases. Complex cases can take up to a week. The warning persists for visitors during the review period. To minimise this window, submit the review immediately after confirming the cleanup is complete and providing detailed documentation of what was cleaned.
How did the attacker get in?
The most common entry points in order of frequency are: outdated plugins with published CVEs (automated scanners hit these within hours of a CVE being published), brute-forced admin passwords, compromised FTP or hosting account credentials, nulled or pirated themes and plugins containing pre-installed backdoors, and vulnerable file upload handlers in contact form or media plugins. Check the access log analysis from Step 12 for requests to specific PHP files around the time of the first malware detection. The file that received unusual POST requests is typically the entry point.



