# Clean a Hacked WordPress Site on cPanel: Step-by-Step

Source: https://srvscripts.com/guides/clean-hacked-wordpress-cpanel/
Updated: 2026-10-07
Publisher: srvScripts (https://srvscripts.com/)

**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.

## 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](/guides/runbook-server-compromised/) and [cPanel root escalation response](/guides/cpanel-root-escalation-incident-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](/guides/find-source-of-outgoing-spam-cpanel/)).

- **Change the cPanel, FTP and database passwords now**, and look at `/usr/local/cpanel/logs/login_log` for 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](/guides/cxs-replacement-malware-scanning/).

**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
