Skip to content
Four ways back in: the WordPress XSS campaign that hides its own admin account

Four ways back in: the WordPress XSS campaign that hides its own admin account

Patchstack •Edouard • October 6, 2026

One JavaScript payload, delivered through multiple WordPress stored XSS flaws, installs an admin account you cannot see in wp-admin. Analysis and IoCs.

Two unrelated WordPress plugins, two separate stored Cross-Site Scripting vulnerabilities, one payload. Over the past several days our telemetry has recorded exploitation attempts against both, and every attempt pulls the same JavaScript from imgcdn1.com. Two is what we have confirmed, not what we expect the final count to be.

We first observed the payload on October 4, 2026 , in an exploitation attempt targeting CVE-2026-93836 in WPC Product Bundles for WooCommerce. The following day, we observed the same payload being delivered through CVE-2026-94504 in Ninja Forms.

The JavaScript is not a vulnerability scanner or a proof-of-concept. It is a WordPress post-exploitation and persistence implant designed to execute inside the browser of a logged-in administrator.

Once triggered, it abuses the victim’s authenticated wp-admin session to install a malicious WordPress plugin, create a new administrator account, invoke an additional hidden persistence mechanism, and report the results to attacker-controlled infrastructure.

We retrieved and analysed that plugin. It carries an unauthenticated file manager behind seven layers of packing, and a second script that creates a WordPress administrator hidden from the Users screen, installs a backdoor login URL that authenticates as the site’s original owner, and backdates its own files so they look older than the WordPress installation itself.

Two Vulnerabilities, One Payload

WPC Product Bundles for WooCommerce

Unauthenticated Stored Cross-Site Scripting

Unauthenticated Stored Cross-Site Scripting

The first exploitation attempt we identified targeted CVE-2026-93836 , an unauthenticated stored XSS vulnerability affecting WPC Product Bundles for WooCommerce through version 8.6.6.

The vulnerability allows a quantity beginning with a valid numeric value to pass validation while retaining additional attacker-controlled markup. The original value can then be stored in WooCommerce order metadata and later rendered unsafely.

The requests observed on October 4 supplied the payload through the woosb_ids[…][qty] parameter, carrying a numeric prefix followed by a script tag:

Then on October 5, we identified the same JavaScript payload being distributed through a second vulnerability: CVE-2026-94504 in Ninja Forms.

Ninja Forms through version 3.15.3 stores attacker-controlled non-rich-text textarea submissions and can render them without safe HTML encoding in the legacy administrative submission editor. When the affected submission is viewed, injected JavaScript can execute in the WordPress administrative origin. Version 3.15.4 strengthened output escaping on the affected administrative screen.

This time, the attacker submitted the payload through the normal Ninja Forms AJAX endpoint, /wp-admin/admin-ajax.php with action=nf_ajax_submit , inserting the malicious value into multiple form fields:

The loaders differ because the attackers adapt the injection to each vulnerable context, but the post-exploitation code stays centralized on their own infrastructure. That shared second stage is what ties both attempts to one campaign.

The retrieved x.js is a purpose-built WordPress post-exploitation and persistence script. It is lightly obfuscated, primarily through encoded string tables and indirect function lookups.

Its overall execution flow is:

The important distinction is that the JavaScript itself does not need a second server-side vulnerability, but uses legitimate WordPress administrative functionality through the administrator’s existing authenticated session.

✌️ Our users are protected from these vulnerabilities. Are yours?

Mitigate vulnerabilities in real-time without changing code.

Identify vulnerabilities in your plugins and get recommendations for fixes.

Protect your users, improve server health and earn additional revenue.

Riding the Administrator’s Session

The malware never steals the administrator’s WordPress cookie. Because the JavaScript runs in the security context of the compromised site, same-origin requests to /wp-admin/ automatically carry the logged-in user’s session. The script loads privileged WordPress pages, scrapes the CSRF nonces that legitimate administrative actions require, and replays them to perform those actions itself.

HttpOnly cookies do not help here, because the JavaScript never reads the cookie. It simply rides the administrator’s authenticated session , inheriting every action that administrator can perform.

Checking In With the C2

Before carrying out any of its persistence actions, the malware contacts . The response contains state fields including plugin , admin , emer and skip , which let the C2 determine which stages have already succeeded for a particular victim.

The malware also stores local execution state in the browser, under a Local Storage key beginning with __xp_v9_ . Successful stages are tracked as plugin , admin and emer , with a completed compromise eventually marked as all . This prevents unnecessary repetition if the stored XSS executes multiple times.

Status data is sent back using several browser mechanisms, including fetch() , navigator.sendBeacon() and an image-request fallback, increasing the likelihood that the C2 receives the result even if interrupts execution.

Malicious Plugin Installation

Using the victim’s session, the script loads /wp-admin/plugin-install.php?tab=upload , scrapes the upload nonce, pulls a ZIP from and submits it through WordPress’ own plugin installer.

We retrieved and analysed the package. It installs to the slug wp-smart-thumbnails and holds four files:

It poses as WP Smart Thumbnails 1.2.4 by “MediaPress Labs”, a plugin for caching responsive thumbnails. The file that would hold that functionality contains nothing but an ABSPATH guard, and readme.txt is two lines. Every file is timestamped July 2026, months before the campaign, which fits deliberate backdating.

The main plugin file then opens with a guard that inverts the usual WordPress convention:

A legitimate plugin exits when ABSPATH is not defined, to block direct access. This one exits when it is , so the payload never runs while WordPress loads the plugin. Installed and active, it does nothing. It only detonates on a direct HTTP request, and the attacker’s own says so.

The Dropper and the Webshell

Below that guard sits a single base64 blob. It is base64-decoded, XORed against a 26-byte key, reversed, put through ROT13, base64-decoded twice more, rotated by two bytes and decompressed with gzuncompress , then checked against a hardcoded MD5 before it runs. That last check is the author’s own integrity guard, and it makes verification easy: our unpacked stage matches it.

The execution step is worth noting. Instead of eval() , the dropper registers a custom stream wrapper and include s the decoded source through a dyiv:// URL. Same effect, but rules watching for eval() on decoded input never fire.

The unpacked stage is roughly 37 KB: a graphical file manager titled “Moon, tell me if I could” , offering list, upload, delete, rename, mkdir, read and save over a JSON API. It has no authentication of any kind. No password, no token, no capability check anywhere in the file; the only equality test on a key is a JavaScript handler for the Escape key. Anyone who finds the path gets what the attacker has.

There are no command-execution primitives, no system() or exec() . That is a thin distinction, because upload and save write attacker-supplied bytes to any writable path, which is enough to drop a second shell.

What emer-run.php Actually Installs

emer-run.php is where the durable persistence gets built. Over HTTP it bootstraps WordPress, demands current_user_can('administrator') and returns JSON, which is why x.js calls it from inside the administrator’s session. From CLI or WP-CLI it skips the capability check entirely. Its header is candid the job: “hidden admin + magic login (invoked via XSS admin session). Returns JSON only; drops obfuscated MU-plugins.” It does three things.

First, it creates an administrator built to look routine. The username is drawn at random from a dictionary of around fifty innocuous operational words such as support , updater , maintenance and backup , with an @wordpress.org email address and a 16-character random password.

Second, it hides that account from the administrator who owns the site. This is the part that matters most for detection. The script writes a must-use plugin to wp-content/mu-plugins/class-wp-query- .php , where the hex is derived from the username, and that file installs three hooks:

The account does not appear in Users → All Users , does not appear in the Administrator filter, and is not counted in the totals above the list. It is a fully privileged administrator the site owner cannot see. Must-use plugins make that stick: they load on every request, cannot be deactivated from wp-admin, and are never listed on the Plugins screen. Removing wp-smart-thumbnails does not remove them.

Third, it creates a magic login URL at /wp-login.php?_wplogin= , backed by a second must-use plugin, class-wp-token-validate.php . The token lives in the fz_emer_login_tokens option and the plugin hooks init at priority 1 to trade it for a session cookie. That token is not bound to the hidden account. It is bound to the oldest existing administrator on the site , so anyone holding the URL is silently logged in as the original owner, in a session no log that records only a user ID can tell from a real one.

Both must-use plugins are written obfuscated, as XOR-plus- gzdeflate blobs with random variable names and a fresh key per site, so no two infected sites a hash. Each one is then backdated: the script scans the WordPress root, takes the oldest modification time it finds, and stamps it onto the file it just dropped. The implants end up looking like the oldest files on the system, which defeats the usual hunt for recently changed files in wp-content .

Taken together, a single successful execution of the stored XSS leaves the attacker with four independent routes back into the site, only one of which is visible from the WordPress dashboard:

The visible administrator created through user-new.php , with credentials supplied by the C2

The hidden administrator created by emer-run.php and concealed by a must-use plugin

The magic login URL , which authenticates the holder as the site’s oldest administrator

The unauthenticated file manager in the plugin itself, reachable by direct request

Removing the vulnerable plugin, or even the malicious one, closes none of the last three.

Why Stored XSS Works So Well for This Campaign

Stored XSS suits this campaign because it removes the hardest part of the attack: getting a privileged user to cooperate.

A reflected XSS needs the victim to visit an attacker-controlled URL. A stored XSS is already inside the application, sitting in data administrators open as part of their job: a WooCommerce order in the first case, a Ninja Forms submission in the second. It fires the moment they look.

That distinction has mattered to WordPress Core itself recently. In September 2026, WordPress 7.1.1 fixed an unauthenticated stored XSS in wpautop() that let attacker-controlled script persist through content, subject to approval.

The attack model is therefore:

Which plugin provides the initial XSS barely matters. Once JavaScript runs inside an authenticated administrative origin, the post-exploitation stage is identical, which is why these attackers collect stored XSS vulnerabilities instead of building a separate malware chain for each plugin.

Infrastructure: imgcdn1.com

A single domain carries every stage of the campaign:

The domain was registered on October 1, 2026 , and a public DNS indexing service lists it among domains delegated to the Cloudflare nameserver hazel.ns.cloudflare.com on October 2. We saw it used in exploitation two days later.

Both vulnerabilities had been publicly disclosed on September 22, roughly ten days before the domain appeared. The timing fits infrastructure stood up specifically for this campaign.

As of publication we have confirmed this delivery infrastructure across at least two separate stored XSS vulnerabilities, and exploitation volume remains limited in our telemetry. Neither number is a ceiling. The post-exploitation stage is plugin-agnostic, so any stored XSS that puts JavaScript in front of a WordPress administrator is a candidate vector for the same implant.

The earliest request in our telemetry was observed on October 4, 2026 at 10:39 UTC. It targeted a WooCommerce product URL and supplied the malicious loader through the woosb_ids parameter, in a URL-encoded value containing:

The source IPs differ across the attempts while the second-stage infrastructure and malware stay identical, which is consistent with exploitation spread across several scanning hosts behind one centralized payload and C2 server.

Four of the five addresses are Tor exit nodes that our firewall has blocked before in connection with other vulnerabilities. The fifth, 43.250.53.42, is flagged for spam and brute-force activity and appears in other recent campaigns. Because most of the sources are Tor exits, blocking these addresses is not a durable mitigation on its own.

The target pools differ sharply: WPC Product Bundles has more than 30,000 active installations , Ninja Forms more than 500,000 .

We continue to monitor exploitation activity and payload evolution for changes in infrastructure, obfuscation and post-exploitation techniques, and will update this post as additional affected plugins are confirmed.

The analyzed x.js sample is 19,378 bytes. The must-use plugins are obfuscated with a fresh key per site and will not match a fixed hash, so they are listed by path.

These indicators describe the campaign observed in our telemetry and should not be interpreted as exhaustive indicators for exploitation of either CVE.

Detection and Mitigation

Update WPC Product Bundles for WooCommerce past the affected 8.6.6 release. The upstream changelog identifies 8.6.7 as carrying the security fix.

Update Ninja Forms to 3.15.4 or later . Its upstream changelog notes strengthened output escaping in the administrative submission edit screen in that release.

historical HTTP and application logs for references to the delivery infrastructure:

and inspect WordPress installations for the dropped plugin:

Because the hidden administrator is filtered out of the Users screen, it cannot be found from the dashboard. Enumerate administrators directly in the database instead:

Compare that result against what wp-admin shows. Any account that appears in the query but not in the interface is hidden by one of the dropped must-use plugins. An @wordpress.org email address on a local account is a further signal, as is an account named after a routine operational word such as support , updater or maintenance .

Then inspect the must-use plugin directory, which the Plugins screen never shows:

Note that file modification times are not reliable here. The installer deliberately backdates everything it writes to the oldest timestamp found in the WordPress root, so these files will not surface in a for recently changed files. Sort by content, not by date.

Check the options table for the campaign’s own state:

The presence of fz_emer_login_tokens means at least one backdoor login token is live. Deleting both options invalidates the magic URL, but the must-use plugins must be removed as well or a fresh token can simply be issued.

In web server logs, look for requests carrying the _wplogin query parameter to wp-login.php , and for direct requests to the plugin’s own PHP files. The file manager only runs when reached directly, so any hit on wp-smart-thumbnails.php outside the plugin screens is a usable signal.

Finally, investigate any administrator account created around the time an administrator viewed WooCommerce orders or Ninja Forms submissions.

A site where the XSS successfully executed in an administrator’s browser should be treated as potentially compromised. Removing the injected JavaScript alone is not sufficient because the malware is explicitly designed to move persistence outside the vulnerable plugin.

Remove the unauthorized accounts and plugins, clear the dropped files from mu-plugins , delete the fz_emer_done_v1 and fz_emer_login_tokens options, rotate privileged credentials and WordPress authentication salts, and review the filesystem and database for further modifications. Because the backdoor login URL authenticates as the site’s oldest administrator, that account’s password should be treated as compromised even though it was never directly stolen.

What looked like exploitation of a single WordPress vulnerability is a broader campaign. These attackers are not tied to a plugin. They are collecting stored XSS vulnerabilities that put JavaScript in front of WordPress administrators and treating them as interchangeable delivery mechanisms for one post-exploitation implant.

That is what makes it worth watching. The XSS is not the payoff here, it is the front door, and what comes through it is built to outlive the vulnerability that let it in.

🤝 You can help us make the Internet a safer place

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Protect your users too! Improve server health and earn added revenue with proactive security.

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.