← AEU Hosting Blog

WordPress XSS Campaign Installs Hidden Admin Accounts

2026-10-08

WordPress XSS Campaign Installs Hidden Admin Accounts

A WordPress XSS campaign first observed by Patchstack on October 4, 2026 is using stored cross-site scripting flaws in two unrelated plugins to install an administrator account that site owners cannot see in wp-admin. The same JavaScript payload, delivered through both vulnerabilities, pulls from imgcdn1.com and was retrieved and analysed by Patchstack. It is a post-exploitation and persistence implant designed to run in the browser of a logged-in administrator, abuse that administrator session to install a malicious plugin, create a hidden administrator, add a backdoor login URL, and report results to attacker-controlled infrastructure.

The first exploited vulnerability is CVE-2026-93836, an unauthenticated stored XSS in WPC Product Bundles for WooCommerce through version 8.6.6. The flaw lets a quantity value that starts with a valid number pass validation while also carrying attacker-controlled markup. The value is stored in WooCommerce order metadata and later rendered unsafely. Patchstack saw requests on October 4 supply the payload through the woosb_ids[…][qty] parameter, with a numeric prefix followed by a script tag that loads x.js from imgcdn1.com. The next day, the same payload appeared through CVE-2026-94504 in Ninja Forms through version 3.15.3. Ninja Forms stored textarea submissions and could render them without safe HTML encoding in the legacy administrative submission editor. Version 3.15.4 strengthened output escaping. The attacker submitted the payload through the normal Ninja Forms AJAX endpoint at /wp-admin/admin-ajax.php with action=nf_ajax_submit, inserting an image tag with an onerror handler that creates a script element and loads the same x.js. The two loaders differ because the attackers adapt to each vulnerable context, but the centralised second stage ties both attempts to one campaign.

The retrieved x.js is a purpose-built WordPress post-exploitation and persistence script, lightly obfuscated with encoded string tables and indirect function lookups. Its flow is: stored XSS in wp-admin, resolve origin, check in with the command-and-control server (C2), install plugin, create administrator, fetch emer-run.php, and report back. The JavaScript does not need a second server-side vulnerability; it uses legitimate WordPress administrative functionality through the administrator's existing authenticated 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, which are one-time security tokens that legitimate administrative actions require, and replays them to perform those actions itself. HttpOnly cookies, a browser protection that stops JavaScript from reading cookies, do not help because the script never reads the cookie; it simply rides the session.

Before any persistence action, the malware contacts https://imgcdn1.com/fz/c.php?xa=check&site=. The response contains state fields including plugin, admin, emer and skip, which let the C2 decide which stages have already succeeded for a victim. The malware also stores local execution state in the browser under a Local Storage key beginning with __xp_v9_, tracking successful stages as plugin, admin and emer, and eventually marking a completed compromise as all. Status data is sent using fetch(), navigator.sendBeacon() and an image request fallback, so the C2 receives results even if navigation interrupts execution.

Using the victim's session, the script loads /wp-admin/plugin-install.php?tab=upload, scrapes the upload nonce, pulls a ZIP from https://imgcdn1.com/fz/c.php?xa=pkg and submits it through WordPress's own plugin installer. Patchstack retrieved and analysed the package. It installs to the slug wp-smart-thumbnails and holds four files: wp-smart-thumbnails.php (20,728 bytes, packed dropper and file manager), emer-run.php (14,330 bytes, persistence installer), readme.txt (46 bytes, decoy) and includes/class-thumb-cache.php (41 bytes, decoy). The plugin poses as WP Smart Thumbnails 1.2.4 by "MediaPress Labs", a plugin for caching responsive thumbnails, but the functionality file contains only 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 opens with an inverted guard: if ABSPATH is defined, it returns, so the payload never runs while WordPress loads the plugin. Installed and active, it does nothing; it only detonates on a direct HTTP request, as the attacker's own comment says.

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 Patchstack's unpacked stage matches it. Instead of eval(), the dropper registers a custom stream wrapper and includes the decoded source through a dyiv:// URL, evading rules that watch for eval() on decoded input. 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; the only equality test on a key is a JavaScript handler for the Escape key. There are no command-execution primitives, but upload and save can write attacker-supplied bytes to any writable path, enough to drop a second shell.

emer-run.php is where 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 command line or WP-CLI it skips the capability check entirely. Its header comment says "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 account built to look routine: a username randomly drawn from a dictionary of about 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 site owner. The script writes a must-use plugin to wp-content/mu-plugins/class-wp-query-.php, where the hex is derived from the username. That file installs three hooks: pre_user_query appends AND user_login != '' to the user query SQL; users_list_table_query_args adds the account to login__not_in; views_users decrements the role counts shown above the list. 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.