Short answer: Isolate the site, take a forensic copy, then scan it with Imunify360/ImunifyAV or ClamAV. Replace WordPress core and wordpress.org plugins with clean copies (check them with wp core verify-checksums and wp plugin verify-checksums --all). Hunt for PHP in uploads, recently changed files, unknown admin users and code stored in wp_options. Finally reset all passwords, shuffle the salts (wp config shuffle-salts), update everything and close the hole that let the attacker in. If the attacker may have reached root, stop and follow the server-level runbook instead.
We ran the read-only checks on this page on a WordPress 7.1.2 test account on our lab server (AlmaLinux 9.8, cPanel & WHM 11.138, EA-PHP 8.3, ImunifyAV 8.9) on 6 October 2026: the WP-CLI checksum, user, cron and database queries, the find and grep searches, and the scanner --help output. The commands that change the site (core re-download, password resets, salt shuffle) were checked against the WP-CLI documentation but not run. The lab site was clean, so the outputs below show a clean baseline plus one real false alarm.
Table of Contents
Step 1: decide the scope and isolate the site
First answer one question: is this one hacked site, or a hacked server? Signs that it is the server: unknown root SSH keys, new system users, changed binaries, or malware in several accounts at once. In that case, use our server compromise runbook and cPanel root escalation response, not this page.
For a single site:
- Stop the damage. If the site is sending spam or serving malware, suspend the account in WHM (Manage Account Suspension) or block public access with a maintenance page while you work. Check the mail queue too (see our outgoing spam guide).
- Change the cPanel, FTP and database passwords now, and look at
/usr/local/cpanel/logs/login_logfor cPanel logins from unknown IPs. - Take a forensic copy before you clean, so you can still answer “how did they get in” later:
/scripts/pkgacct bob /root/forensics-bob. Keep it outside the web root, mode 600.
Step 2: scan the account
Imunify360 or ImunifyAV: start an on-demand scan of the docroot. These options come from --help on our lab’s ImunifyAV:
imunify360-agent malware on-demand start --path /home/bob/public_html
imunify360-agent malware malicious list # results, after the scan finishes
The start command also accepts --scan-db/--no-scan-db for scanning the database, and --intensity low|moderate|high to control load. The scanner’s cleanup takes a backup of each file it changes (malware malicious restore-original restores it), but read the list before you clean. Scanners have false positives too: see our malware scanning guide.
ClamAV via cPanel: if the cPanel ClamAV plugin is installed, cPanel’s knowledge base gives this command for one account:
/usr/local/cpanel/3rdparty/bin/clamscan -ir /home/bob/public_html
Treat any scanner result as a starting list, not a full answer. New backdoors are often not detected yet. The next steps find what scanners miss.
Step 3: verify WordPress core and plugins against checksums
WP-CLI compares every core file with the official MD5 checksums for your version and locale, without loading WordPress (so injected code in plugins cannot interfere). Run it as the account user, never as root:
cd /home/bob/public_html
sudo -u bob /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/wp core verify-checksums
sudo -u bob /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/wp plugin verify-checksums --all
Why the long PHP path? On our cPanel lab, calling wp through sudo printed Content-type: text/html and “Only CLI access.” instead of running. /usr/local/bin/wp is WP Toolkit’s wrapper with #!/usr/bin/env php, and in that context php resolved to a non-CLI binary. Calling it through the EA-PHP CLI binary of the site’s PHP version worked every time.
What a clean result looks like, and a false alarm we hit:
# plugin check on the lab site
Success: Verified 2 of 2 plugins.
# core check on the same fresh WordPress 7.1.2 install
Warning: File doesn't exist: wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php
Warning: File should not exist: wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonPsr17ClassesStrategy.p
...
Error: WordPress installation doesn't verify against checksums.
All 46 warnings on our lab were inside wp-includes/php-ai-client/. The “should not exist” entries were the missing files with their names cut short. In the samples we measured, the missing paths were 105 to 107 characters long and the extra ones exactly 90. That is likely the 100-character name limit of older tar formats (90 characters plus the archive’s wordpress/ prefix), hit by whatever unpacked the install. It is an installer problem, not malware. The lesson: read the list before you panic. Real infections show up as modified files with normal names (wp-includes/load.php, wp-login.php) or as extra files with innocent-looking names in core folders.
Fix core by replacing it, not by editing files. This re-downloads the same version over the existing core and leaves wp-content and wp-config.php alone (options per the WP-CLI docs):
sudo -u bob /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/wp core download --force --skip-content --version=7.1.2
sudo -u bob /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/wp core verify-checksums
core download --force overwrites core files but does not delete extra files an attacker added to wp-admin or wp-includes. Run verify-checksums again and remove anything reported as “should not exist” once you have checked it. Plugins that fail verify-checksums should be deleted and reinstalled from a fresh copy. Premium plugins are not on wordpress.org and cannot be verified this way, so reinstall them from the vendor’s zip.
Step 4: find backdoors the checksums do not cover
wp-content (themes, uploads, mu-plugins, premium plugins) is not covered by the checksums, and that is where most backdoors live.
D=/home/bob/public_html
# PHP files in uploads (there should be none)
find $D/wp-content/uploads -type f \( -iname "*.php*" -o -iname "*.phtml" \)
# PHP files changed recently (adjust the date to before the first sign of the hack)
find $D -type f -name "*.php" -newermt "2026-09-20" -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort
# must-use plugins load automatically and do not show in the normal plugin list
ls -la $D/wp-content/mu-plugins/
# common obfuscation patterns (expect some false hits in legitimate code)
grep -rlE "eval\(base64_decode|gzinflate\(base64_decode|str_rot13\(" --include=*.php $D
On the clean lab site, the uploads search returned nothing, there was no mu-plugins folder, and the obfuscation grep found 0 files, which is the baseline you want. Attackers can fake file times with touch, so the date search is a hint, not proof. Also check .htaccess files in every folder for redirect rules or auto_prepend_file, and the account’s cron jobs (crontab -l -u bob) for downloaders that reinstall the malware.
The web logs show which files the attacker actually used. Look for POST requests to unusual PHP files:
grep '"POST ' /etc/apache2/logs/domlogs/example.com-ssl_log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
Step 5: check the database for backdoors and rogue admins
W="sudo -u bob /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/wp"
$W user list --role=administrator --fields=ID,user_login,user_registered
$W user list --fields=ID,user_login,roles,user_registered --orderby=user_registered --order=DESC
$W cron event list --fields=hook,next_run_relative,recurrence
$W option get siteurl
$W option get home
$W db prefix
Look for administrators you do not recognise (often created on the day of the hack), cron hooks with random names, and siteurl/home pointing elsewhere. Then search options and posts for injected code. With the SQL in a file, the query is not mangled by shell quoting, which tripped us up when we ran it over SSH. Use the prefix that wp db prefix printed (ours was wp_):
cat > /root/wp-scan.sql <<'SQL'
SELECT option_name, LENGTH(option_value) AS len FROM wp_options
WHERE option_value LIKE '%eval(%' OR option_value LIKE '%base64_decode%' OR option_value LIKE '%<script%';
SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%<script%' AND post_status = 'publish';
SQL
$W db query < /root/wp-scan.sql
On the lab, the options query returned no rows. Not every match is malicious, because some plugins store inline scripts legitimately, so read each hit before you delete anything. Delete unknown admin users with wp user delete ID --reassign=1, after you confirm with the site owner.
Step 6: rotate secrets and close the hole
- Log everyone out and change the salts:
$W config shuffle-saltsreplaces the keys inwp-config.php, which invalidates all existing login cookies. - Reset WordPress passwords:
$W user reset-password $($W user list --format=ids --role=administrator)resets the admins and emails them. Then do the other roles. - Change the database password in cPanel and update
DB_PASSWORDinwp-config.php. Also change any API keys stored inwp-config.phpor plugin settings. - Update everything: core, all plugins and themes. Delete inactive plugins and themes rather than leaving them. In our WP Toolkit security score guide, WP Toolkit can flag vulnerable components across all sites.
- Find the entry point: a vulnerable plugin version, a reused password (check
login_logandwp-login.phpPOSTs), or another hacked site in the same account. Cleaning without fixing the cause usually means reinfection within days.
Step 7: harden and check that it worked
- Disable PHP execution in
wp-content/uploads(an.htaccessrule or the hardening options in WP Toolkit). - Set
define('DISALLOW_FILE_EDIT', true);inwp-config.phpso the theme/plugin editor cannot be used to drop code. - Keep Imunify360’s WAF and proactive defense on, and turn on 2FA for administrators.
- Re-run the checksums, the uploads search and a full scan. All should be clean.
- Check the site’s status in Google Safe Browsing, and request a review in Search Console if Google flagged it.
- Scan again after 24 hours and after 7 days. A reinfection means a backdoor or the original hole is still there.
For hosting-wide hardening (CageFS, ModSecurity, PHP-FPM per user), see harden a shared cPanel server.
Official documentation: WP-CLI: wp core verify-checksums · WP-CLI: wp plugin verify-checksums · Imunify360 docs: command-line interface · cPanel KB: how to start a ClamAV scan
Related: Server hacked: incident response runbook for Linux and cPanel · cPanel Root Escalation Incident Response: Critical First 24 Hours · cxs Replacement: Free Malware Scanning with LMD and ClamAV · Using WP Toolkit Security Risk scores, Smart Update and Vulnerable Components · Harden Shared cPanel Server: Secure CageFS and ModSecurity Setup
See also: WordPress CVE-2026-64638 Patch: Find and Update Every Vulnerable Site · WordPress Version Audit: Find Every WordPress Site and Its Version · Imunify360 False Positives: Find the Rule ID and Fix It
See also: Imunify360 CLI: Scan, List and Clean Malware from the Command Line
Frequently asked questions
How do I check if WordPress core files were modified?
Run wp core verify-checksums as the site user. It compares every core file with the official checksums for your version. Read the warnings: on our lab, a fresh install failed only because long file names had been truncated during install.
Why does wp-cli say “Only CLI access” on cPanel?
The wp wrapper started a non-CLI PHP binary. Call it through the EA-PHP CLI binary, for example /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/wp, as the account user.
Is it enough to run a malware scanner?
No. Scanners miss new backdoors. Also verify checksums, search uploads for PHP, review admin users and cron events, check the database, and fix the entry point.
Should I restore from a backup instead of cleaning?
If you have a backup from before the infection, restoring is often faster. You still have to update everything, change all passwords and close the hole, or the site is reinfected.
How do I log out all WordPress users after a hack?
Run wp config shuffle-salts. New salts invalidate every existing login cookie. Then reset passwords with wp user reset-password.