The scanner says the account is clean, passwords are rotated, and 48 hours later the same redirect is back. That pattern means the files you cleaned were the payload, not the persistence. Current WordPress campaigns layer five or six regenerators, and one of them now lives in the site admin's browser, where no server-side scanner can reach.
This is the root-level hunt to run on a cPanel or CloudLinux server after
Imunify360 cleanup has done its
pass and the site reinfects anyway. The examples use the account baduser.
Work down this list in order
Check each layer before moving to the next one. Each layer is enough on its own to rebuild the infection.
- Immutable files. Did the cleanup report files it couldn't modify?
- WordPress drop-ins and mu-plugins. Does
wp-content/contain anything that loads before the plugins? - PHP prepend. Does any
.user.inior.htaccesssetauto_prepend_file? - Shared memory. Does the account own SysV shared memory segments?
- Scheduled jobs. Are there unknown WP-Cron hooks, user crontabs, or admin users?
- The database. Are payloads stored in
wp_optionsorwp_posts? - The browser. Did the reinfection start right after an admin logged in?
Immutable files
Attackers with a foothold sometimes run chattr +i on their backdoors. The account user
can't delete them, and neither can a cleanup running as that user. Look for them as root:
lsattr -R /home/baduser 2>/dev/null | grep -E '^[-a-zA-Z]*i[-a-zA-Z]* '
Clear the flag before cleaning or deleting:
chattr -i /home/baduser/public_html/wp-content/mu-plugins/craft-system-orb.php
Drop-ins, mu-plugins, and PHP prepend
WordPress loads wp-content/db.php, advanced-cache.php, and object-cache.php before
any plugin, and it loads everything in mu-plugins/ on every request. Nothing can disable
these from the dashboard. List them along with their sizes:
ls -la /home/baduser/public_html/wp-content/{db.php,advanced-cache.php,object-cache.php} \
/home/baduser/public_html/wp-content/mu-plugins/ 2>/dev/null
A legitimate advanced-cache.php comes from a cache plugin and names that plugin in its
header. A db.php you can't trace to a plugin (Query Monitor ships one) is a backdoor.
Recent samples hide gzip-compressed, base64-encoded payloads between marker comments and
rebuild the other components when they're deleted.
Then check for PHP-level prepends, which run before WordPress loads:
grep -rlE 'auto_(prepend|append)_file' /home/baduser --include='.user.ini' \
--include='php.ini' --include='.htaccess' 2>/dev/null
Shared memory and scheduled jobs
Some regenerators cache a copy of themselves in SysV shared memory. That copy survives file deletion and rewrites the backdoor on the next request. Look for segments owned by the account:
ipcs -m -c | awk 'NR<=3 || $3=="baduser" || $5=="baduser"'
ipcrm -m 131074
Use the shmid from the first command in the ipcrm call. Restarting PHP doesn't remove
these segments. Only ipcrm or a reboot does.
Then check every scheduler, running WP-CLI as the account user:
crontab -l -u baduser
cd /home/baduser/public_html
sudo -u baduser wp cron event list --fields=hook,next_run_relative,recurrence
sudo -u baduser wp user list --role=administrator --fields=ID,user_login,user_registered
sudo -u baduser wp db search 'base64_decode' --all-tables --stats
Random-looking cron hook names and admin accounts registered during the infection window
are your persistence. Delete them with wp cron event delete and wp user delete --reassign.
The browser layer: malicious service workers
This layer catches experienced operators out. Some 2026 campaigns register a JavaScript
service worker scoped to /wp-admin/ and /wp-login.php in the browser of every admin who
logs in during the infection. The worker intercepts admin page loads and can inject script
that uses the admin's live session to reinstall the backdoor through the plugin installer
or the theme editor.
You can clean every file on the server and the worker survives. Once a worker is registered, it keeps running from the browser's copy. If its script now returns 404, the update check fails quietly and the old worker stays registered. The symptom is reinfection that starts within minutes of a specific person logging in.
Two ways to remove it:
- Per browser. Each admin opens DevTools → Application → Service Workers on the site
and clicks Unregister, or uses
chrome://serviceworker-internals. They need to do this on every device they logged in from during the infection, which is why it rarely happens. - From the server. Send
Clear-Site-Data: "storage"on admin responses. Browsers unregister the site's service workers when they receive it. Add this to the account's.htaccessfile for two weeks, then remove it:
<IfModule mod_headers.c>
SetEnvIf Request_URI "^/(wp-login\.php|wp-admin/)" CLEAR_SW
Header always set Clear-Site-Data "\"storage\"" env=CLEAR_SW
</IfModule>
The header also clears localStorage, so admins lose some dashboard preferences. It doesn't clear cookies, so they stay logged in. Browsers only honour it over HTTPS.
When to stop hunting and restore
If you find immutable files or a hidden regenerator, or you've done two cleanup rounds without success, stop hunting. Restore files and the database from a backup taken before the first detection, using a JetBackup point-in-time restore. Reinstall core and plugins from wordpress.org, then rotate the cPanel password, the database password, and the WordPress salts. Then switch on Proactive Defense, so a regenerator you missed can't execute.