img

The Hidden Admin

A Case Study in Backdoor Persistence, Ad Fraud, and the Art of Hiding in Plain Sight


Introduction: Ads That Shouldn’t Exist

The client noticed something strange on their website: Google ads had appeared. This wouldn’t be unusual for most sites, except for one thing—they had never set up Google AdSense. They didn’t have an AdSense account. They had no idea where these ads were coming from.

The ads were real. They were being served through Google’s legitimate advertising infrastructure. But the revenue wasn’t going to the client. It was going to someone else entirely.

What followed was an investigation that uncovered one of the more elegant WordPress compromises I’ve encountered. Not because it was technically sophisticated—it wasn’t—but because it was designed to be invisible. A fake plugin with a believable name. An admin account engineered to blend in. A backdoor that could regenerate itself on command. And database-level injections that wouldn’t show up in any file scan.

The attackers had been inside this site for approximately sixteen months before anyone noticed.

This case study documents how that access was established, how it was hidden, and how we found it anyway. For site owners, it’s a reminder that not all compromises announce themselves—and that the absence of obvious symptoms doesn’t mean everything is fine.


Part One: The Initial Assessment

The Client’s Situation

The client operated a religious educational media organization, running a WordPress site that hosted audio content for their community. Like many small organizations, their website was a tool that needed to work, not a technology project that demanded constant attention. It had been built several years earlier, maintained as needed, and generally left alone when nothing seemed broken.

Their environment reflected this history:

  • WordPress 6.0.11 (significantly outdated; current versions were in the 6.7.x range)
  • PHP 8.0.30 (end-of-life since November 2023, no longer receiving security updates)
  • The Divi theme (a popular premium WordPress theme)
  • A collection of plugins accumulated over time, including one—Sticky Menu or Anything on Scroll—with a known critical vulnerability

This configuration isn’t unusual. It describes a substantial percentage of small organization WordPress sites. The software works, updates get deferred, and the gap between “current” and “installed” gradually widens until something forces attention.

First Look: The User Count Discrepancy

Before gaining SSH access to the server, I started with what was available: the WordPress admin dashboard.

The Users panel in WordPress displays a summary at the top showing how many users exist and how many have each role. On this site, the numbers didn’t add up. The summary showed three total users with two administrators. But the list below only displayed two users, with one administrator visible.

One user was missing.

This kind of discrepancy is a red flag. WordPress doesn’t miscount its own users. When the displayed count doesn’t match the visible list, something is actively hiding a user account from the admin interface.

I navigated directly to user ID 16 by manually editing the URL: /wp-admin/user-edit.php?user_id=16. The hidden account appeared:

Field Value
Username themeadmin
Email [email protected]
Role Administrator

The naming was deliberate. “themeadmin” sounds like a service account—the kind of user that might have been created during theme installation and forgotten about. [email protected] sounds like it could belong to a legitimate WordPress developer. At a glance, neither would trigger suspicion.

This wasn’t an oversight. It was social engineering applied to user account creation.


Part Two: The Fake Plugin

Finding the Hiding Mechanism

With SSH access to the server, I could search the filesystem directly. The first question: what was hiding the admin account from the user list?

WordPress allows plugins to modify database queries before they execute using hooks like pre_user_query. A search for this hook across all PHP files found the answer:

./wp-content/plugins/Divi Menu/Divi Menu.php

A plugin called “Divi Menu” was intercepting user queries and filtering out the hidden admin account.

Anatomy of the Backdoor

The fake plugin was a masterpiece of misdirection. Here’s what made it effective:

The name: “Divi Menu” sounds like it belongs. The client’s site used the Divi theme from Elegant Themes. A plugin that extends Divi’s menu functionality? Completely plausible. A site administrator scrolling through their plugin list would have no reason to question it.

The metadata: The plugin file included a header block identifying “Elegant Themes” as the author with links to their legitimate website. Anyone who checked the plugin details would see what appeared to be an official extension.

The functionality: The plugin did exactly two things. First, it hooked into WordPress’s user query system to filter the “themeadmin” account from all user listings. Second—and more concerning—it included a URL-triggered backdoor.

The backdoor worked like this: visiting the site with a specific secret parameter in the URL (?admin_key=[secret-value]) would cause the plugin to create a new administrator account with hardcoded credentials. If the hidden account was ever discovered and deleted, the attacker could simply visit the URL to recreate it.

This is persistence engineering. The attackers weren’t just creating access; they were creating the ability to recreate access if their initial foothold was removed.

The Email Discrepancy

One detail caught my attention. The backdoor code was configured to create accounts with the email address [email protected]—another legitimate-sounding address designed to blend in. But the actual hidden account used [email protected].

This mismatch suggests the attackers manually modified the account after creation, or that this represented multiple compromise attempts over time. Either way, it indicated ongoing access and management of the backdoor, not just a one-time automated attack.


Part Three: Following the Money

The AdSense Trail

With the backdoor mechanism understood, the next question was how the fraudulent ads were being served. A search of the filesystem for the AdSense publisher ID came up empty. The code wasn’t in any PHP file, JavaScript file, or template.

This meant the injection was in the database.

WordPress stores many site-wide settings in a database table called wp_options. Themes and plugins use this table to persist their configurations. The Divi theme stores its settings in an option called et_divi, which includes fields for custom code injection into page headers and footers—a legitimate feature that lets site owners add tracking scripts and other customizations.

The attackers had used this feature to inject their ad code:

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXX"></script>

This script loaded on every page of the site, serving ads that generated revenue for the attacker’s AdSense account. The injection point was clever: it used the theme’s own settings system, which meant the malicious code lived in serialized data that wouldn’t appear in a simple file search.

Content Injection

Beyond the site-wide header injection, the attackers had also modified individual post content. Two posts—an original and its revision—contained embedded ad containers with full AdSense markup. This belt-and-suspenders approach meant that even if one injection method was discovered and removed, the other would continue operating.

The ads.txt File

One detail elevated this from opportunistic injection to deliberate fraud infrastructure: the attackers had created an ads.txt file in the site’s root directory.

For readers unfamiliar with digital advertising: ads.txt is an industry-standard file that publishers use to declare which advertising networks are authorized to sell their inventory. It’s a fraud prevention mechanism—ad buyers can check a site’s ads.txt to verify that the ads being sold are legitimate.

The attackers had added their own declaration:

google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, XXXXXXXXXXXXXXXX
This line told the advertising ecosystem that the fraudulent publisher ID was an authorized direct seller for this website. It wasn't just ad injection—it was ad injection with forged authorization documents.

This level of preparation suggests the attackers understood the digital advertising ecosystem well enough to know that without an ads.txt entry, their fraudulent ads might be flagged or devalued by ad verification systems. They weren’t just stealing ad impressions; they were building infrastructure to make that theft sustainable.

The Mobile-Only Mystery

One detail from the initial report stood out: the client first noticed the ads on mobile, not on desktop. As the site administrator, they regularly accessed the site from their desktop browser while logged in. The ads had apparently been running for months without them ever seeing one.

This pattern is consistent with a common evasion technique. Attackers who inject ads often implement display conditions that hide their work from the people most likely to notice—typically logged-in administrators viewing the site on desktop. WordPress adds a logged-in class to the page body for authenticated users, making this trivially easy to implement with a single CSS rule or a few lines of JavaScript.

We searched for evidence of this conditional logic in the Divi theme settings, the WordPress Customizer, and generated CSS files. Nothing definitive turned up. The problem is timing: by the time we thought to look for this specific behavior, we had already removed the malicious injection and purged all caches. If the conditional display logic was embedded in the injected AdSense code itself—rather than stored separately—that evidence no longer exists.

We can’t confirm exactly how the attackers avoided detection for sixteen months. But the pattern fits: ads visible to anonymous mobile visitors, invisible to the logged-in admin on desktop. Whether by design or coincidence, it worked.

What This Attack Cost

Ad injection might seem less severe than payment card theft, but it’s still fraud with real consequences.

The site was serving ads that visitors might reasonably assume were legitimate. The revenue was going to criminals. And the site’s reputation—particularly important for a religious educational organization—was being used to generate money for unknown third parties.

For Google, ad fraud is a serious policy violation. Sites caught serving ads through hijacked accounts can be permanently blacklisted from AdSense. For the site owner, unwitting participation in ad fraud creates liability and reputational risk.


Part Four: The Deeper Exposure

Sensitive Files in the Webroot

During the filesystem review, I routinely check for common misconfigurations that extend beyond the immediate compromise. What I found significantly expanded the scope of this incident.

Two files were sitting in the site’s public web directory, accessible to anyone who knew—or guessed—their names:

File Contents Status
wp-config.php.bk Complete WordPress configuration including database credentials and security salts Publicly downloadable
[database-name]-2022-03-15-[hash].sql Full database dump Publicly downloadable

Both files returned HTTP 200 responses. Anyone who requested them received the complete contents.

The Impact

The wp-config.php.bk file—likely created as a backup during some past maintenance—contained everything an attacker needs to access a WordPress site’s database directly: hostname, database name, username, password, and the secret keys WordPress uses to secure cookies and authentication tokens.

The SQL dump was worse. Created in March 2022, it contained a complete snapshot of the database at that time: user accounts, password hashes, post content, configuration options, and any other data stored in WordPress.

These files had been publicly accessible for years. The SQL dump dated back over three and a half years. The config backup was undated but likely from a similar timeframe.

What This Means

The ad injection attack we investigated began in August 2024. But the exposure of these credential files meant the site had been vulnerable for far longer—and in a more fundamental way.

Anyone who discovered these files could have:

  • Connected directly to the database server from anywhere
  • Extracted all user password hashes for offline cracking
  • Modified any data in the database without leaving WordPress logs
  • Obtained the security salts needed to forge authentication cookies

We can’t know whether the ad injection attackers found these files, or whether they used an entirely different entry point. But we also can’t assume they didn’t. The credential exposure meant that the database password and all WordPress security salts needed to be rotated as part of remediation—not just the admin password.

The Lesson

Backup files in web-accessible directories are one of the most common and most dangerous misconfigurations. A file named wp-config.php won’t be served by a properly configured web server—PHP files get executed, not downloaded. But rename it to wp-config.php.bk or wp-config.php.old or wp-config.php.save, and suddenly it’s just a text file that the server will happily send to anyone who asks.

The same applies to database dumps. They should never exist in the webroot. They should be created in a non-web-accessible directory, transferred off the server, and deleted. A SQL file left behind from a backup three years ago is a time capsule of credentials and data waiting to be discovered.


Part Five: Remediation

The Cleanup

Before making any changes, I created full backups of both the filesystem and database. This isn’t optional—it’s the foundation that makes confident remediation possible. If something goes wrong during cleanup, you need the ability to restore. And forensically, you want a preserved snapshot of the compromised state before you start modifying files.

With backups secured, removing the compromise required addressing every layer:

The fake plugin: Deleted the Divi Menu directory entirely. With it went the user-hiding code and the URL-triggered backdoor.

The hidden admin account: Removed from both the users table and all associated metadata. The account and all its capabilities were eliminated.

The AdSense injection in theme settings: Cleared the malicious script from the Divi integration settings. This required parsing the serialized PHP array, removing the injected content, and re-serializing the data.

(The theme stores all of its settings bundled together in a single database entry; you can’t just delete one line without carefully extracting it and rebuilding the bundle, or the entire theme configuration breaks.)

The infected post content: Cleaned the AdSense containers from the two affected posts, matching the specific markup pattern used by the attackers.

The fraudulent ads.txt: Deleted the file declaring the attacker’s publisher ID as an authorized seller.

The exposed credential files: Removed the wp-config.php.bk backup and the orphaned SQL dump from the webroot.

Credential rotation: Reset the admin password, rotated the database password (updating wp-config.php to match), and regenerated all WordPress security salts. Given the credential exposure, we had to assume every secret on the site was compromised.

The Cache Problem

After completing the database cleanup, I loaded the site to verify the fix. The ads were still there.

This is a common gotcha in WordPress remediation. The database was clean, but the site was running LiteSpeed Cache, which had stored copies of the infected pages. Every visitor was still receiving the cached, compromised version.

Verification confirmed the issue: adding ?nocache=1 to the URL showed a clean page. The database was fixed, but the cache was still serving the old content.

Purging required clearing both the filesystem cache and the database-stored cache metadata. After a full purge through the LiteSpeed Cache admin interface, the site finally rendered clean.

The Lesson

Cache-related persistence is an easy mistake to make. After hours of careful forensic work, it’s tempting to verify once and move on. But modern WordPress sites often have multiple caching layers—plugin caches, server caches, CDN caches—and each one needs to be cleared after remediation. The attack isn’t really gone until visitors stop receiving compromised content.


Part Six: Hardening

Addressing the Root Cause

Cleanup removes the immediate threat, but it doesn’t prevent recompromise. The vulnerabilities that allowed initial access were still present.

The likely entry point was the Sticky Menu or Anything on Scroll plugin, which had a critical vulnerability allowing unauthenticated attackers to modify WordPress options. This vulnerability was actively exploited in the wild and could allow attackers to enable user registration with administrator privileges—a direct path to the kind of access we observed.

The hardening phase addressed the environment:

WordPress core: Updated from 6.0.11 to 6.9—closing nearly three years of accumulated security patches.

PHP version: Upgraded from 8.0 (end-of-life) to 8.2, restoring active security support through December 2026.

Vulnerable plugins: The Sticky Menu plugin was removed. Other plugins were updated to current versions.

Security plugin: Installed Wordfence with extended protection, enabling the web application firewall to block attacks at the server level before they reach WordPress.

Server hardening: Added rules to block PHP execution in the uploads directory (preventing uploaded malware from running), blocked direct access to sensitive files, and disabled the XML-RPC interface (a common brute-force target).

WordPress hardening: Disabled the built-in file editor (preventing attackers with admin access from injecting code through the dashboard) and enabled automatic core updates.

The Scan Limitation

Wordfence’s malware scanner repeatedly failed to complete on this site. This happens on shared hosting where server resource limits terminate long-running processes. The scans would start, run for several minutes, then get killed before finishing.

This wasn’t a crisis—we had already manually verified the site was clean during the forensic phase. The firewall component was fully operational, and scans would eventually complete during off-peak hours. But it illustrates a limitation of shared hosting for sites that need robust security scanning.


Part Seven: Timeline and Dwell Time

Reconstructed Timeline

Date Event
August 2024 Malicious plugin file created (initial compromise)
October 2024 Malicious plugin file modified (maintenance or update)
December 2025 Client notices fraudulent ads
December 2025 Incident response and remediation completed

Sixteen Months Undetected

The attackers maintained access to this site for approximately sixteen months. During that time:

  • They had full administrative access to WordPress
  • They could read and modify any content on the site
  • They had access to the database, including any stored credentials or sensitive data
  • They were actively generating revenue through ad fraud
  • The backdoor allowed them to regenerate access if their account was discovered

And no one knew.

This extended dwell time isn’t unusual. The attackers weren’t doing anything that triggered obvious alerts. The ads looked like legitimate ads. The hidden admin account was invisible to casual inspection. The fake plugin had a plausible name. There were no error messages, no site crashes, no customer complaints.

The compromise was designed to be profitable without being noticeable. It succeeded at both.


Lessons for Site Owners

What This Case Demonstrates

Attackers design for stealth. The fake plugin name, the legitimate-sounding email addresses, the user-hiding mechanism, the database-level injection—every element was chosen to avoid detection. These weren’t the actions of automated malware. Someone designed this attack to blend in.

File scans miss database compromises. The AdSense injection wasn’t in any file. Traditional malware scanners that examine PHP and JavaScript files would have found nothing. Database auditing is a separate skill that requires different tools.

Dwell time can be measured in years. The sixteen months between initial compromise and detection represents an extraordinary amount of potential damage. Credentials could have been harvested. Content could have been modified. The attackers could have pivoted to more harmful activities at any time.

Credential exposure compounds every other risk. The publicly accessible config backup and database dump meant that even if the ad injection was the only active attack, we had to treat every credential as compromised. Backup files in web directories are silent vulnerabilities that can persist for years.

Caching extends compromise impact. Even after the database is cleaned, visitors may continue receiving compromised content until all cache layers are purged. Verification isn’t complete until real users see clean pages.

Outdated software is the common thread. An end-of-life PHP version, a significantly outdated WordPress installation, and a plugin with a known critical vulnerability created the opening. None of these updates would have been difficult. All of them were deferred.

Warning Signs to Watch For

  • User counts that don’t match visible user lists

  • Plugins you don’t remember installing

  • Plugins with names that sound almost-but-not-quite right

  • Ads or content appearing that you didn’t create

  • Admin accounts with generic or service-like names you can’t explain

  • Modified file timestamps on plugin files that shouldn’t be changing

  • Backup files (.bak, .old, .save, .bk) in your web directories

  • SQL dumps or other database exports left in accessible locations

  • An ads.txt file you didn’t create

Minimum Security Baseline

  • Keep everything updated. WordPress, themes, plugins, and PHP. Automate where possible.

  • Audit your plugins. Remove anything you’re not actively using. Verify that installed plugins match what you expect.

  • Review user accounts regularly. Know every account that has admin access. Question anything unfamiliar.

  • Use a security plugin with both firewall and scanning capabilities.

  • Check your database. Options, post content, and widget areas can all harbor injections that file scans miss.

  • Purge caches after any cleanup. Every layer, every time.

  • Monitor for anomalies. Google Search Console, uptime monitoring, and regular manual reviews catch what automated tools miss.

  • Never leave backup files in web directories. No .bak, .old, .save copies of sensitive files. Ever.

  • Create backups properly. Store them outside the webroot, transfer them off-server, and delete local copies when done.

  • Rotate credentials periodically. Database passwords and WordPress salts should change on a regular schedule, not just after incidents.


Conclusion

This investigation revealed a compromise built for longevity—and an environment that had been leaking secrets for years before the attackers ever arrived.

The ad injection itself was clever: a fake plugin with a believable name, a hidden admin account, database-level injections that file scanners wouldn’t find, and even a forged ads.txt to legitimize the fraud. For sixteen months, it operated undetected.

But the exposed credential files told a more troubling story. A config backup and database dump, sitting in the public webroot since 2022, meant the site’s secrets had been available to anyone who thought to look. The ad injection attackers may have found them. Others may have too. We’ll never know.

The site is now clean, updated, and hardened. The backdoor is gone, the hidden account removed, the ad injection cleared, the exposed files deleted, and all credentials rotated. The environmental issues that enabled the original compromise—the outdated WordPress, the end-of-life PHP, the vulnerable plugin—have been addressed.

But the real lesson isn’t about this specific attack. It’s about the assumption that everything is fine when nothing is visibly wrong.

Most compromises don’t announce themselves. There’s no ransomware popup, no defaced homepage, no obvious sign that something has changed. The successful attacks are the quiet ones—the persistent access, the subtle modifications, the ongoing theft that doesn’t trigger alerts.

The only defense against this kind of attack is attention. Regular audits. Timely updates. Understanding what should be on your site so you notice when something shouldn’t. It’s not glamorous work. But it’s the difference between catching a compromise in hours and discovering it sixteen months later.

By then, you can clean up the damage. But you can’t take back the access.


This case study documents a real client engagement. Technical details have been generalized and client-identifying information has been changed to protect confidentiality while preserving the educational value of the investigation.


Related Reading