# How to Find and Clean Malware on a cPanel Server Without Breaking Sites

> A practical incident guide for cPanel/WHM administrators: find infected files with standard tools, tell injected code from pure malware, clean without taking sites down, and close the way back in.

Source: https://www.pyxsoft.com/blog/find-clean-malware-cpanel-server
Published: 2026-10-30
Tags: malware, cpanel, wordpress, security, webshell

---

![A beam of light lifts one infected file out of a server's folders, leaving every other file in place](https://www.pyxsoft.com/_astro/cover.CHK9E5YB.jpg)

A customer reports a redirect, your IP lands on a blocklist, or the abuse desk forwards a complaint. Now you have a server with hundreds of accounts, an unknown number of infected files, and sites that must stay online.

The usual mistake is to delete everything that looks wrong as fast as possible. That takes healthy sites down, destroys the evidence you need, and leaves the attacker's way in wide open. This guide takes a different order: **preserve, find, classify, clean, close the entry point.**

<!--truncate-->

No command below changes anything in the accounts until the cleaning step. Run them as root on the server, and adjust `/home` if you have accounts under `/home2` or similar.

## Step 1 — Preserve before you touch anything

Copy the affected account's files and logs somewhere outside any docroot. You will want the original files to work out how the attacker got in, and you will want a way back if a cleanup breaks something.

```bash
mkdir -p /root/incident
tar -czf /root/incident/ACCOUNT-files.tar.gz -C /home ACCOUNT/public_html
cp -a /etc/apache2/logs/domlogs/DOMAIN* /root/incident/ 2>/dev/null
```

Replace `ACCOUNT` and `DOMAIN` with the real names. Do this before cleaning: logs rotate, and timestamps change as soon as files are edited.

## Step 2 — Find candidates

No single search finds everything. Combine them.

### Recently changed PHP files

Attackers can fake a file's modification time (`mtime`) with `touch`, but not as easily its change time (`ctime`). So search by `ctime`:

```bash
find /home/*/public_html -type f -name '*.php' -ctime -14 \
  -printf '%CY-%Cm-%Cd %CH:%CM  %u  %p\n' | sort -r
```

A cluster of files sharing the same minute, especially across unrelated folders, tells you when the attack happened. Plugin updates also produce clusters, so check the paths.

### Executable code where uploads belong

WordPress `uploads` folders hold media, and PHP files there are almost never legitimate. A few plugins do store some, so review the list rather than deleting it blindly.

```bash
find /home/*/public_html/wp-content/uploads -type f \
  \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \)
```

### Classic obfuscation functions

```bash
grep -rlE --include='*.php' \
  'eval[[:space:]]*\(|base64_decode[[:space:]]*\(|gzinflate[[:space:]]*\(|str_rot13[[:space:]]*\(' \
  /home/*/public_html
```

Treat the result as a list of leads, not a list of infections. Legitimate plugins use these functions all the time. It also misses a lot. In [a real dropper family we analyzed](https://www.pyxsoft.com/blog/one-signature-eight-obfuscated-droppers), this search found none of the eight files, because the function names are assembled at run time.

### Symlinks leaving the account

On shared hosting, a symlink pointing at a system file or another account's folder is a classic way to read data the attacker should not reach.

```bash
find /home/*/public_html -type l -printf '%p -> %l\n'
```

### WordPress checksums

For WordPress sites, compare the files against the official releases with [WP-CLI](https://wp-cli.org/), which you may need to install first. Run it as the account's user, not as root:

```bash
sudo -u ACCOUNT -- wp core verify-checksums --path=/home/ACCOUNT/public_html
sudo -u ACCOUNT -- wp plugin verify-checksums --all --path=/home/ACCOUNT/public_html
```

This catches modified core files without guessing at content. The plugin check only covers plugins published on WordPress.org, so commercial plugins will show up as unverifiable, not as clean.

## Step 3 — Classify each file

This is the step that keeps sites online. Each suspicious file falls into one of three groups, and each needs different handling.

| What you found | Example | What to do |
|---|---|---|
| **Pure malware** | A random-named PHP file in `uploads` | Remove it |
| **Legitimate file with injected code** | `wp-config.php` or a theme's `functions.php` with an extra block | Remove only the injected code, or restore a clean copy |
| **Legitimate code that looks suspicious** | A backup plugin using `base64_decode` | Leave it alone |

Deleting a file in the second group is what breaks sites. If `wp-config.php` or an autoloader disappears, the whole site goes down.

**Watch for decoys.** Some malware pastes a real library into the file and puts a top-level `exit` before it, so a quick look shows plenty of familiar code that never runs. If everything after the malicious block is unreachable, treat the file as pure malware.

## Step 4 — Clean carefully

**Prefer replacing to editing.** For WordPress core and public plugins, reinstall from the official source:

```bash
sudo -u ACCOUNT -- wp core download --force --skip-content --path=/home/ACCOUNT/public_html
sudo -u ACCOUNT -- wp plugin install PLUGIN_NAME --force --path=/home/ACCOUNT/public_html
```

Reinstalling overwrites the official files but doesn't remove extra ones an attacker added, so run the checksum commands again afterward.

**Edit by hand only what you cannot replace**, such as `wp-config.php` and custom themes. Check the syntax before you leave:

```bash
php -l /home/ACCOUNT/public_html/wp-config.php
```

A syntax error from a bad cut takes the site down just as surely as deleting the file would. Then load the site and check that it works.

## Step 5 — Look for what the malware left behind

A clean file does not mean a clean site. Droppers write other files and change the site's configuration, so check these as well:

- **`.htaccess` files:** look for added redirects, `auto_prepend_file`, and handlers that run PHP from image extensions.
- **Permissions:** we have seen malware set a docroot to `0555` so that the owner's tools cannot fix it.
- **Scheduled tasks:** run `crontab -l -u ACCOUNT` for each affected account.
- **WordPress admins:** run `wp user list --role=administrator` and look for accounts nobody recognizes.
- **WordPress options:** injected scripts can live in the database, where no file scan will see them.

## Step 6 — Close the way in

If you skip this step, you will probably repeat the whole process next week, as we explain in [why hacked sites get reinfected](https://www.pyxsoft.com/blog/why-sites-get-reinfected-after-cleaning). Use the file timestamps from Step 2 to search the access logs around the time of the attack:

```bash
grep -h 'POST .*/wp-content/uploads/.*\.php' /etc/apache2/logs/domlogs/DOMAIN*
```

Requests to the infected file show you the attacker's IP. Earlier requests from that IP often point at the vulnerable plugin or the login that was abused. Then:

1. Update or remove the vulnerable plugin or theme.
2. Change the account's cPanel, FTP, database, and WordPress passwords.
3. Replace the WordPress salts in `wp-config.php`.
4. Repeat the searches from Step 2 over the next few days.

If you find evidence that the attacker gained root, cleaning files is no longer enough. Plan to move the accounts to a freshly installed server.

## Doing this on every server, every day

The steps above work for one incident on one account. Across a fleet, the hard part is noticing the infection early and repeating the classification reliably. That is what we built **[Pyxsoft Anti Malware](https://www.pyxsoft.com/anti-malware)** to do on cPanel/WHM servers:

- **It watches files in real time** and runs server-wide, per-account, and scheduled scans.
- **It reads what the code does.** Beyond signatures on the raw file, it parses PHP and rewrites obfuscated code to a canonical form, so a renamed or re-encoded copy can still match the same signature.
- **It separates the groups in Step 3.** With Auto Clean on, injected code is cut out of legitimate files using the PHP syntax tree, not regular expressions, and the result is validated. The original is kept so you can undo the change, and if a clean cut is not safe, the file goes to quarantine.
- **It flags malicious symlinks**, including those pointing to system files.
- **It stays out of the way.** You set a CPU limit, and it keeps to it. A WordPress site with 9,229 files took 12 seconds on the first scan and half a second on an unchanged rescan.[^1]

It cleans files, not whole incidents, so Steps 5 and 6 are still yours to do.

It is licensed per server, whatever the number of accounts, and you can try it free for 7 days without a credit card. The first server-wide scan starts on its own after installation:

```bash
curl -s https://www.pyxsoft.com/install-anti-malware | bash
```

[^1]: Measured on 2 vCPU, 3.5 GB RAM, default CPU limit: 9,229 files, first scan vs. unchanged rescan. Results vary by server and site.
