Threat Overview
Patchstack reported a confirmed, active campaign exploiting stored cross-site scripting vulnerabilities in two unrelated WordPress plugins: CVE-2026-93836 in WPC Product Bundles for WooCommerce and CVE-2026-94504 in Ninja Forms. Exploitation was first observed on October 4 and October 5, 2026, respectively. Both paths inject JavaScript that loads https://imgcdn1.com/fz/x.js. When an authenticated WordPress administrator views the poisoned content, the script abuses the administrator’s existing session and WordPress nonces to install a malicious plugin and establish multiple persistence paths.
The installed plugin uses the slug wp-smart-thumbnails and impersonates a benign-sounding “WP Smart Thumbnails” plugin. Reported components include a packed file manager, an emergency runner, hidden or visible administrator creation, two must-use plugin paths, and a token-based login route using /?_wplogin=<token>. The campaign also backdates files to the oldest WordPress-root timestamp, which makes filesystem modification-time review unreliable. Sentinel hunting should therefore combine request, DNS/network, Defender file/process, hash, and Syslog evidence rather than rely on recent-file timestamps alone.
References
- Patchstack, “Four ways back in: the WordPress XSS campaign that hides its own admin account,” published October 6, 2026: https://patchstack.com/articles/four-ways-back-in-the-wordpress-xss-campaign-that-hides-its-own-admin-account/
- BleepingComputer, “Hackers exploit WordPress plugin flaws to create admin accounts,” published October 6, 2026: https://www.bleepingcomputer.com/news/security/hackers-exploit-wordpress-plugin-flaws-to-create-admin-accounts/
- WordPress.org, WPC Product Bundles for WooCommerce plugin page/changelog, accessed October 7, 2026: https://wordpress.org/plugins/woo-product-bundle/
- WordPress.org, Ninja Forms plugin page/changelog, accessed October 7, 2026: https://wordpress.org/plugins/ninja-forms/
Impacted Systems
- WPClever WPC Product Bundles for WooCommerce: versions 8.6.6 and earlier; fixed in 8.6.7. WordPress/WooCommerce, self-managed or hosted deployments where the plugin is installed and enabled. Exploitation uses a quantity field in bundle data and requires an administrator to view the stored payload for session abuse.
- Saturday Drive Ninja Forms: versions 3.15.3 and earlier; fixed in 3.15.4. WordPress, self-managed or hosted deployments where the plugin is installed and enabled. The observed injection is submitted through
POST /wp-admin/admin-ajax.phpwithaction=nf_ajax_submit; an administrator must view the malicious submission. - Platform: PHP-based WordPress on Linux or Windows; physical, virtual, containerized, or managed hosting. File and process hunts require host telemetry, while request hunts require WAF, reverse-proxy, CDN, or web-server logs.
- Exposure: public forms or bundle endpoints reachable from the Internet, plus an administrator who views the poisoned content while authenticated.
- Fixed versions: WPC Product Bundles 8.6.7 or later and Ninja Forms 3.15.4 or later. Other WordPress plugins are not implicated by these two CVEs merely because they use forms or WooCommerce.
Why this matters
The campaign is confirmed active and converts a stored-XSS foothold into durable server-side persistence. A successful compromise can survive password changes through must-use plugins and a magic-login token, and the attacker can manipulate arbitrary files through the installed file manager. WordPress is broadly deployed and frequently Internet-facing, making the published infrastructure, paths, hashes, and behavioral sequence immediately useful to MSSP customers.
Exploitation Status
Confirmed active exploitation. Patchstack observed exploitation beginning October 4, 2026 for CVE-2026-93836 and October 5 for CVE-2026-94504. The same JavaScript loader, command infrastructure, plugin slug, and persistence design linked both paths. The five published source IPs include Tor exit nodes and should be treated as supporting indicators rather than durable blockers. The campaign domain, file paths, hashes, and magic-login parameter are higher-value corroboration. No claim is made that every request to the vulnerable form endpoints is malicious.
What this hunt looks for
Published loader and command infrastructure, vulnerable form-submission patterns, malicious plugin and must-use-plugin paths, published hashes, web-service file writes and child processes, direct file-manager access, magic-login requests, and Syslog fallback evidence.
Required logs
WAF, reverse-proxy, CDN, or web-server request telemetry in CommonSecurityLog, AzureDiagnostics, or Syslog; Microsoft Defender for Endpoint DeviceFileEvents, DeviceProcessEvents, and DeviceNetworkEvents on WordPress hosts; and DNS or firewall telemetry for published infrastructure.
Hunt 1 — High-confidence request and infrastructure indicators
let lookback = 30d;
let ips = dynamic(["204.8.96.109","147.90.235.22","90.184.10.74","94.16.115.121","43.250.53.42"]);
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", ""))
| where Raw has "imgcdn1.com" or Raw has_any ("/fz/x.js", "/fz/c.php", "wp-smart-thumbnails", "_wplogin=") or Src in (ips)
| project TimeGenerated, Src, Evidence=substring(Raw, 0, 1800)
| order by TimeGenerated desc
Hunt 2 — Vulnerable submission patterns in web telemetry
let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", ""))
| extend Decoded=url_decode_component(url_decode_component(Raw))
| where (Decoded has "/wp-admin/admin-ajax.php" and Decoded has "action=nf_ajax_submit" and Decoded has_any ("<script", "imgcdn1.com", "/fz/x.js"))
or (Decoded has "woosb_ids" and Decoded has "qty" and Decoded has_any ("<script", "imgcdn1.com", "/fz/x.js"))
| project TimeGenerated, Src, Request=substring(Decoded, 0, 1800)
| order by TimeGenerated desc
Hunt 3 — Endpoint connections to the loader or command service
let lookback = 30d;
DeviceNetworkEvents
| where Timestamp >= ago(lookback)
| where RemoteUrl =~ "imgcdn1.com" or RemoteUrl endswith ".imgcdn1.com"
| project Timestamp, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteUrl, RemoteIP, RemotePort
| order by Timestamp desc
Hunt 4 — Published malicious hashes in Defender telemetry
let lookback = 30d;
let sha256 = dynamic([
"af803bc822cf9596b1f1785e1d59642f91f6fbc21f8e9943feefa19a6df8c2a4",
"ccc95113334357c2a3671aae12043bff00020858cec115f7e724fe43e9f1fc24",
"ed00234ad2b67dbbc9073e8c711b18b7d39e0a5255b6159189f75cf2ed99cfb9"
]);
union isfuzzy=true
(DeviceFileEvents | where Timestamp >= ago(lookback) | project Timestamp, DeviceName, ActionType, FileName, FolderPath, SHA256, InitiatingProcessFileName, InitiatingProcessCommandLine),
(DeviceProcessEvents | where Timestamp >= ago(lookback) | project Timestamp, DeviceName, ActionType, FileName, FolderPath, SHA256, InitiatingProcessFileName, InitiatingProcessCommandLine)
| where SHA256 in (sha256)
| order by Timestamp desc
Hunt 5 — Malicious plugin and must-use-plugin file creation
let lookback = 30d;
DeviceFileEvents
| where Timestamp >= ago(lookback)
| where FolderPath has_any ("/wp-content/", @"\wp-content\")
| where FolderPath has_any ("wp-smart-thumbnails", "mu-plugins")
or FileName in~ ("wp-smart-thumbnails.php", "emer-run.php", "class-wp-token-validate.php")
or FileName matches regex @"(?i)^class-wp-query-[0-9a-f]{8}\.php$"
| project Timestamp, DeviceName, ActionType, FileName, FolderPath, SHA256, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc
Hunt 6 — Web-service processes writing PHP into plugin directories
let lookback = 30d;
DeviceFileEvents
| where Timestamp >= ago(lookback)
| where FileName endswith ".php" and FolderPath has_any ("/wp-content/plugins/", "/wp-content/mu-plugins/", @"\wp-content\plugins\", @"\wp-content\mu-plugins\")
| where InitiatingProcessFileName in~ ("php", "php-fpm", "apache2", "httpd", "nginx", "w3wp.exe")
| summarize FirstSeen=min(Timestamp), LastSeen=max(Timestamp), Files=make_set(strcat(FolderPath, "/", FileName), 50), Hashes=make_set(SHA256, 20), Writes=count() by DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Writes desc
Hunt 7 — Suspicious child processes from PHP or web-server parents
let lookback = 30d;
DeviceProcessEvents
| where Timestamp >= ago(lookback)
| where InitiatingProcessFileName in~ ("php", "php-fpm", "apache2", "httpd", "nginx", "w3wp.exe")
| where FileName in~ ("sh", "bash", "dash", "curl", "wget", "python", "python3", "perl", "nc", "socat", "cmd.exe", "powershell.exe", "certutil.exe")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA256
| order by Timestamp desc
Hunt 8 — Magic-login and direct file-manager access
let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", ""), column_ifexists("CallerIPAddress", "")), Status=tostring(coalesce(column_ifexists("HttpStatusCode", ""), column_ifexists("scStatus_s", "")))
| extend Decoded=url_decode_component(Raw)
| where Decoded has "_wplogin=" or Decoded has_any ("/wp-content/plugins/wp-smart-thumbnails/wp-smart-thumbnails.php", "/wp-content/plugins/wp-smart-thumbnails/emer-run.php")
| project TimeGenerated, Src, Status, Request=substring(Decoded, 0, 1800)
| order by TimeGenerated desc
Hunt 9 — Syslog/web-log fallback for campaign artifacts
let lookback = 30d;
Syslog
| where TimeGenerated >= ago(lookback)
| where SyslogMessage has_any ("imgcdn1.com", "/fz/x.js", "/fz/c.php", "wp-smart-thumbnails", "emer-run.php", "class-wp-token-validate.php", "_wplogin=")
or (SyslogMessage has "/wp-admin/admin-ajax.php" and SyslogMessage has "nf_ajax_submit")
| project TimeGenerated, Computer, HostName, ProcessName, SeverityLevel, SyslogMessage
| order by TimeGenerated desc
Detection Notes
- Hunts 4, 5, and 8 are highest signal. A published hash, malicious plugin path, must-use token plugin, or
_wploginrequest is much stronger than a source-IP match. - Four published source IPs were identified as Tor exits. Treat them as time-bounded correlation points; blocking them does not prevent exploitation from new exits.
- The malware backdates filesystem timestamps.
DeviceFileEvents.Timestamprecords telemetry observation time and remains useful, but disk-only “modified in the last N days” scripts can miss the files. wp-smart-thumbnailscould collide with an unrelated local name. Validate file hashes, publisher provenance, directory contents, direct-request behavior, and the reported impersonated version/author before declaring compromise.- Web-request bodies are frequently omitted, truncated, or redacted. Without body capture, Hunt 2 may see only the endpoint and action, which is not sufficient by itself.
- Managed WordPress hosting may not expose EDR or raw web logs. In that case, DNS/network and provider-supplied access logs are the only Sentinel-visible layers.
pack_all()is schema-tolerant but expensive. Replace it with validated URI, query, body, source-IP, response-code, and hostname fields in production workspaces.