Why You Cannot Reproduce the Redirect (And the Attacker Wants It That Way)
A wordpress redirect hack is malicious code that sends your visitors somewhere else, usually a fake browser update, a push notification scam, a gambling page, or a fake dating site. You hear about it from customers but cannot see it yourself because the payload checks who is visiting before it fires.
Most variants I clean skip logged-in users, skip anyone who typed the URL directly, and drop a cookie so each visitor only gets redirected once. Sucuri documented a January 2025 sample injected into a theme’s functions.php that did all three, filtered out search engine crawlers by user agent, and hid its destination behind hex-escaped strings like \x68\x74\x74\x70s. The Sign1 campaign went further and only fired when the referrer was Google, Facebook, Instagram, or Yahoo.
So before you touch anything, reproduce it the way a victim sees it: private window on your phone, Google search for your site, tap the result, browse a few pages. If that redirects and a logged-in desktop visit does not, you have conditional redirect malware, not a misconfigured plugin.
Where does redirect malware hide in WordPress?
A wordpress redirect hack lives in one of nine places, and a real cleanup checks all nine. Sucuri’s 2024 SiteCheck data shows the biggest campaigns (Balada Injector at 149,351 detections, Sign1 at 96,084) storing payloads in database options, custom HTML widgets, theme files, and snippets inside legitimate plugins like WPCode and Simple Custom CSS and JS, precisely because file scanners miss the database and database scanners miss the files.
| Hiding place | What the infection looks like | Detection command |
|---|---|---|
| wp_options siteurl / home | Both options changed to an attacker domain or a script URL | wp option get siteurl && wp option get home |
| Other wp_options rows | Autoloaded option holding base64 or hex-escaped JavaScript | wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%eval(%' OR option_value LIKE '%atob(%' OR option_value LIKE '%fromCharCode%'" |
| wp_posts post_content | Script tag appended to every post or page | wp db query "SELECT ID FROM wp_posts WHERE post_content LIKE '%script%src=%' AND post_status='publish'" |
| WPCode or code snippet plugins | PHP snippet stored as post_type wpcode, hidden from the admin menu | wp post list --post_type=wpcode --fields=ID,post_title,post_status |
| Custom HTML and block widgets | Obfuscated JS in widget_custom_html or widget_block | wp option get widget_custom_html --format=json |
| .htaccess and nginx config | RewriteCond on user agent or referrer sending traffic offsite | find . -name .htaccess -newer wp-settings.php |
| Theme header.php / functions.php | Injected block at top or bottom, often after a long run of blank lines | grep -rl -e "eval(" -e "base64_decode" -e "str_rot13" wp-content/themes/ |
| Fake plugins and mu-plugins | Innocent name (hello.php, wp-cache-manager) that loads on every request | ls -la wp-content/mu-plugins/ && wp plugin list --status=active |
| Rogue admin users and cron events | New administrator, or a scheduled hook that rewrites files | wp user list --role=administrator && wp cron event list |
I run every row of that table on every redirect job, even when the first hit looks obvious. Fixing siteurl and stopping there is how a site lands back in your inbox next week.
Contain It Before You Clean It
Snapshot the infected site first, then rotate credentials, then restrict public access. You need the infected copy for forensics: file modification times and the exact injected strings tell you where the attacker got in.
Rotate everything the attacker could have captured: admin passwords, the database password in wp-config.php, SFTP and hosting logins, and the salts (wp config shuffle-salts regenerates them and logs every session out). Then put the site behind maintenance mode or an IP allow rule while you work.
The general containment playbook is covered in more depth in our guide to fixing a hacked WordPress site. The rest of this post assumes you have done that and are now hunting the redirect specifically.
Step 1: Verify Core and Plugin Files Against Official Checksums
Start with the two commands that eliminate the most files fastest. WP-CLI compares core and every wordpress.org plugin against official checksums for the installed versions and lists anything modified or added.
wp core verify-checksums
wp plugin verify-checksums --all
Modified core files (wp-blog-header.php, index.php, the bundled jquery.min.js) are a common carrier because they load on every request. The Balada Injector wave that hit Popup Builder sites used wp-blog-header.php as its secondary injection point alongside a database payload.
Two gaps. Premium plugins and all themes are not on wordpress.org, so verify-checksums cannot vouch for them; compare those against a fresh vendor download. And mu-plugins are never checked, which is exactly why campaigns like DollyWay drop small fake plugins there.
Step 2: Search the Database Row by Row
Check siteurl and home first, then hunt for injected JavaScript in posts, options, and widgets. The two URL options are the fastest fix and the fastest reinfection, so I check them before anything else.
wp option get siteurl
wp option get home
If either returns anything other than your domain, set it back with wp option update siteurl https://yourdomain.com and keep going, because whatever wrote it is still on the server. Next, dry-run a search for the attacker’s domain (grab it from the redirect chain in your phone test) across every table:
wp search-replace 'evil-domain.top' 'REMOVED' --dry-run --all-tables
The dry run reports how many rows in which tables reference the domain without changing anything: one tampered option or thousands of injected posts. When you are confident, snapshot, drop the --dry-run flag, and run it for real.
Then check the places file scanners never look:
- WPCode snippets:
wp post list --post_type=wpcode --fields=ID,post_title,post_status. WPCode registers its own post type, so PHP snippets sit in wp_posts and execute on load. Attackers add a snippet and hide the WPCode menu with a CSS rule. - Custom HTML widgets:
wp option get widget_custom_html --format=jsonandwp option get widget_block --format=json. Sign1 lived almost entirely here. - Autoloaded junk:
wp option list --autoload=on --fields=option_name,size_bytes. A 40KB option you do not recognize deserves a look.
Bloated tables slow this down; a pass through the WordPress database maintenance walkthrough first shrinks the haystack.
Step 3: Grep the Filesystem for Obfuscation Signatures
Search for the encoding functions attackers rely on, then search by modification time. Obfuscated PHP almost always calls one of a small set of functions, and injected JavaScript almost always decodes itself with atob or String.fromCharCode.
grep -rlE "eval\(|base64_decode\(|gzinflate\(|str_rot13\(|gzuncompress\(" wp-content/ wp-config.php
grep -rlE "atob\(|fromCharCode|\\\\x[0-9a-f]{2}\\\\x[0-9a-f]{2}" wp-content/themes/ wp-content/uploads/
find . -name "*.php" -mtime -30 -not -path "./wp-content/cache/*"
find wp-content/uploads/ -name "*.php"
Expect false positives from plugins that use base64_decode legitimately. What you want is eval wrapped around a decode, a long single-line string at the top of functions.php, or any PHP file at all inside uploads.
Redirect rules also live in web server config, which grep for PHP functions will not catch. Audit every .htaccess (attackers nest them in subdirectories) and, on nginx, the server block:
find . -name ".htaccess" -exec sh -c 'echo "== $1"; cat "$1"' _ {} \;
A malicious .htaccess redirect typically checks HTTP_USER_AGENT for mobile strings or HTTP_REFERER for google, then issues a 301 to an external URL. Any RewriteRule pointing to a domain you do not own goes.
Step 4: Remove Users, Cron Jobs, and the Backdoor That Feeds Them
Delete every administrator you did not create, every cron event you cannot explain, and every plugin you cannot vouch for, then reinstall the plugins you keep from a fresh copy. Redirect malware is almost never a single file; the visible redirect is the payload, and something else on the server keeps rewriting it.
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp user delete 47 --reassign=1
wp cron event list
wp cron event delete suspicious_hook_name
DollyWay is the clearest example. GoDaddy’s researchers found that its current version reobfuscates itself and reinfects every active plugin plus any WPCode snippets on every page load, drops a fake Hello Dolly plugin at wp-content/plugins/hello/hello.php, stashes settings in encoded options, and creates its own admin users. Clean one plugin while the others stay active and the malware hands itself a fresh copy on the next request.
For that class of infection I deactivate every plugin, then reinstall each one I intend to keep with wp plugin install plugin-slug --force, which overwrites the directory with a clean wordpress.org copy. Premium plugins get deleted and re-uploaded from the vendor. Only then do I reactivate, one at a time, retesting the redirect after each.
Why does a WordPress redirect hack keep coming back?
Reinfection means you removed the payload but not the entry point, the backdoor, or both. Most do-it-yourself cleanups skip this part, which is why the same site gets flagged by Google Safe Browsing three weeks after it was declared clean.
| Reinfection route | How to spot it | What closes it |
|---|---|---|
| Unpatched plugin vulnerability | Plugin last updated months ago, or a known CVE for your installed version | Update or delete; check Patchstack or WPScan for your exact version |
| Leftover backdoor | A stray .php file in uploads or mu-plugins, or a plugin file that fails verify-checksums | Delete it and reinstall the parent plugin from source |
| Stale backup restored | Redirect returns the same day you restore | Restore only from a pre-infection date, then patch before going live |
| Unrotated credentials | New admin appears with a login from an unfamiliar IP | Rotate all passwords, salts, and API keys; enforce two-factor |
The numbers behind this are not encouraging. In Patchstack’s 2025 data, 46 percent of disclosed WordPress vulnerabilities had no patch available on the day they went public, so updating alone could not have closed them. For the heavily targeted ones, mass exploitation began a median of five hours after disclosure. On the backdoor side, Sucuri found at least one backdoor on 49.21 percent of the sites it remediated in its 2023 Hacked Website report.
Put those together: the redirect got in through a plugin, and roughly half the time it left a second door open. If you never asked how it got in, assume it will be back.
Hardening That Stops the Next Redirect
Patch fast, block PHP where it does not belong, and lock the file system so the payload has nowhere to write.
- Update plugins within days of a security release, not months. With a five-hour median exploitation window, monthly update cycles leave the door open for weeks. A pre-update snapshot means a bad update rolls back in under 60 seconds instead of becoming an excuse to delay.
- Deny PHP execution in uploads. Apache:
RewriteRule ^wp-content/uploads/.*\.php$ - [F,L]in the root .htaccess. Nginx: alocation ~* /uploads/.*\.php$ { deny all; }block. - Disable the dashboard file editor, and file modifications where practical.
define('DISALLOW_FILE_EDIT', true);at minimum. Sites that never install plugins from the dashboard can addDISALLOW_FILE_MODSso an attacker with an admin session still cannot upload a fake plugin. - Run a firewall with file integrity monitoring. Wordfence’s scanner compares core, plugin, and theme files against known-good copies and flags new files in uploads, covering most of the file-based hiding places listed earlier.
- Turn on two-factor for every administrator and audit the user list monthly. A quiet extra admin is the single most common thing I find on repeat-infection sites.
After the site is clean, request a review in Google Search Console under Security Issues so the browser warning lifts. Retest from a phone with a search referrer first; if Google can still trigger the redirect, the review fails and the clock resets.
Two Commands to Run Today
Run wp option get siteurl and wp plugin verify-checksums --all on your site now, before you have a problem. Both take seconds and together cover the fastest-to-fix and most common carriers of a WordPress redirect hack. If either returns something unexpected, you caught it early, while it is one option and one plugin rather than every plugin and a cron job that rewrites them.
If the results come back messy, or the redirect has already returned once, the job has shifted from cleanup to investigation, and that is what our WordPress malware removal service is built for: tracing the entry point and every file the attacker touched so the second cleanup is the last one.
Frequently Asked Questions
Modern redirect malware checks the referrer, user agent, cookies, and login state before firing. It stays quiet for logged-in admins and direct visits so you cannot reproduce it, and only redirects visitors arriving from search engines or social networks on phones. Test in a private window on a mobile device after clicking through from a Google result.
Only if the backup predates the infection and you patch the entry point before bringing the site back online. Most redirect infections sit for weeks before anyone notices, so recent backups usually contain the same malware. Restore, update every plugin, rotate every password, then verify checksums before you trust the site.
Yes, once you request a review in Search Console under Security Issues and Google recrawls clean pages. That review typically clears in a few days if no malware remains. If the redirect only fires for certain visitors, Google may still see it, so test with a mobile user agent and search referrer before you submit.
Redirect keeps coming back after every cleanup?
We trace the redirect to every file, database row, and backdoor it touched, close the entry point, and monitor for reinfection with Wordfence.