WordPress Stored-XSS Admin Takeover: Fake "WP Smart Thumbnails" Plugin, Hidden Administrator and Backdoor Login
Executive Summary
Two unrelated WordPress plugins, WPC Product Bundles for WooCommerce (CVE-2026-93836, versions up to 8.6.6) and Ninja Forms (CVE-2026-94504, versions up to 3.15.3), carry stored cross-site scripting flaws that are being exploited to deliver one shared JavaScript implant from imgcdn1[.]com. Patchstack first saw it on 2026-10-04 against WPC Product Bundles and on 2026-10-05 through Ninja Forms, and reported it on 2026-10-06. The XSS is only the front door: the payload runs inside a logged-in administrator's browser, rides that authenticated session, scrapes the nonces WordPress requires, and then uses WordPress's own plugin installer to drop a fake plugin.
One successful execution leaves the attacker with four independent ways back in: a visible administrator account, an administrator hidden from the Users screen by a must-use plugin, a magic login URL that authenticates as the site's oldest administrator, and an unauthenticated file manager inside the fake plugin. Removing the vulnerable plugin, or even the fake one, closes none of the last three. Patchstack also notes that the installer backdates the files it writes, so a search for recently changed files misses them.
Honest endpoint scope. The exploit, nonce theft, plugin upload, administrator creation and database option writes all happen inside the browser, WordPress and PHP. Falcon endpoint telemetry cannot see them. What Falcon can see is narrower: the administrator workstation looking up the C2 domain, file-integrity events for the dropped PHP files if a FIM policy covers wp-content, shells spawned by the web or PHP worker, and command lines that name the campaign's artifacts. Everything else needs WordPress audit logs, database queries, web server logs, a WAF or a file-integrity tool, and Section 7 and Section 15 say so plainly.
Defender priority: update both plugins, then treat any site where an administrator opened a WooCommerce order or Ninja Forms submission since 2026-10-04 as potentially compromised. Updating stops new injections but does not clean an existing infection. Enumerate administrators directly in the database, inspect the mu-plugins directory by content rather than date, and rotate credentials and salts.
Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry Forward |
|---|---|---|---|
| 1 · Primary research | Patchstack, "Four ways back in", 2026-10-06 (snapshot 01) | Full campaign analysis: both CVEs, x.js behavior, plugin and installer analysis, four persistence routes, infrastructure, IOC table, detection and mitigation steps | Yes: primary source for every indicator |
| 1 · Vendor changelog | Ninja Forms readme changelog (snapshot 04) | Version 3.15.4 (21 September 2026) lists strengthened output escaping in the admin submission edit screen; confirms 3.15.3 as the preceding release | Yes: fix version |
| 1 · Vendor changelog | WPC Product Bundles readme changelog (snapshot 05) | Version 8.6.7 lists a fixed vulnerability; version 8.7.3 separately lists sanitizing bundle ids from request, cart session and order item meta to prevent stored XSS; current listed release is 8.7.4 | Yes: fix version, with the 8.7.3 caveat |
| 2 · Security journalism | BleepingComputer, 2026-10-06 (snapshot 02) | Independent write-up of Patchstack findings: same two CVEs, 500,000 and 30,000 install counts, four access mechanisms, backdated auxiliary plugins | Yes: corroborates CVEs and fix versions (derivative of Patchstack, not independent research) |
| 2 · Security journalism | ReconBee, 2026-10-07 (snapshot 06) | Repeats the CVE pairing and the imgcdn1[.]com domain | Partial: aggregator |
| 3 · CVE listing | SecurityOnline.info homepage "Active Exploits in the Wild" panel (snapshot 03) | Lists CVE-2026-94504 and CVE-2026-93836 as active exploits with CVSS 7.2 and short descriptions matching the two plugins | Partial: confirms CVE numbers and flags a CVSS discrepancy |
| 2 · Hardening authority | WordPress.org developer documentation: Hardening WordPress and wp-config (snapshots 07, 08) | DISALLOW_FILE_EDIT, DISALLOW_FILE_MODS, file permission guidance, Must-Use Plugins reference | Yes: hardening controls |
| 2 · ATT&CK | MITRE ATT&CK T1505.003 (snapshot 09) | Web Shell technique, mitigations M1042 and M1018, detection strategy for file creation in web directories followed by web server child processes | Yes: mapping and detection logic |
Decision log
- CVE numbers. CVE-2026-93836 and CVE-2026-94504 are the only CVE identifiers shipped. Both appear in the Patchstack article, BleepingComputer, ReconBee and the SecurityOnline panel. Other Ninja Forms CVE identifiers circulate in low-quality aggregator posts and search snippets; none of them appears in any saved snapshot, and one that surfaced in a search snippet described a different, earlier Ninja Forms version range, so none is shipped.
- CVSS discrepancy. Patchstack lists CVSS 7.1 for both flaws; SecurityOnline lists 7.2. The pack reports "7.1 to 7.2, high" rather than choosing one.
- "Authenticated" wording. BleepingComputer and ReconBee say the flaws "require an authenticated session to exploit". Patchstack calls both unauthenticated stored XSS. These reconcile: the attacker injects the payload without logging in, but the script only does damage when an authenticated administrator views the stored content. The pack follows Patchstack.
- WPC Product Bundles fix version. Patchstack and BleepingComputer name 8.6.7. The vendor changelog lists a vulnerability fix in 8.6.7 and a separate stored-XSS sanitization fix in 8.7.3. The pack recommends moving to the current release, not stopping at 8.6.7.
- Single-source indicators ship as detect. Every hash, the domain and the plugin artifacts originate in the Patchstack analysis. BleepingComputer and ReconBee repeat the domain but do not independently verify it. Under the two-source rule nothing ships at prevent, and no row in the Falcon import CSV is prevent.
- Hashes that cannot exist. Patchstack states the two must-use plugins are obfuscated with a fresh key per site, so no fixed hash is published and none is invented here. They are listed by path and detected by behavior.
- No coined name. The sources give the campaign no label. The pack uses descriptive wording and the fake plugin identity "WP Smart Thumbnails" exactly as the sources name it.
- Fetched-content check. No fetched page contained instructions directed at an automated reader. Pages carried only ordinary site navigation, ads and unrelated headlines, which were ignored.
Hunt Brief & Attack Chain
Hypotheses
| ID | Hypothesis | MITRE | Falcon event | Query | Fidelity |
|---|---|---|---|---|---|
| H1 | An administrator workstation resolved imgcdn1[.]com after opening an order or submission | T1071.001, T1059.007 | DnsRequest | Q1 | High |
| H2 | Campaign PHP files were written into the plugins or mu-plugins directory | T1505.003, T1564 | CriticalFileModified (FIM) | Q5, Q6 | High (needs FIM) |
| H3 | A shell or downloader ran under the web or PHP worker after the file manager was reached | T1505.003, T1059 | ProcessRollup2 | Q3 | Medium |
| H4 | Someone ran commands naming the implant files, options or login token | T1505.003, T1136.001, T1078 | ProcessRollup2 | Q4 | High |
| H5 | The web server fetched a second-stage payload from a rare address | T1105 | NetworkConnectIP4 | Q2 | Low |
| H6 | The delivery IPs reached the web ports | T1190 | NetworkReceiveAcceptIP4 | Q7 | Low |
| H7 | Stored payload, hidden administrator, backdoor login and file manager use (WordPress, DB, web log layer) | T1190, T1136.001, T1078, T1505.003 | None (non-Falcon) | Section 7 native hunts | High (off-endpoint) |
Attack chain
| Step | Behavior | Telemetry | Hunt angle |
|---|---|---|---|
| 1 | Attacker submits a payload through woosb_ids[...][qty] (WPC, from 2026-10-04) or admin-ajax.php with action=nf_ajax_submit (Ninja Forms, from 2026-10-05) | Web server and WAF logs, WordPress database | GAP on endpoint. Web logs, WAF, database |
| 2 | Payload sits in WooCommerce order metadata or a Ninja Forms submission until an administrator views it | WordPress database | GAP on endpoint. DB search for the domain string |
| 3 | Administrator opens the content; browser loads /fz/x.js from imgcdn1[.]com | Admin workstation DNS and proxy | Q1 |
| 4 | x.js checks in at /fz/c.php?xa=check&site=, tracks stages in browser Local Storage key prefix __xp_v9_ | Admin workstation DNS and proxy; browser storage | Q1; browser forensics |
| 5 | Using the admin session, x.js scrapes the plugin-upload nonce, pulls a ZIP from c.php?xa=pkg, installs wp-smart-thumbnails | Web server file writes, WordPress audit log | Q5 (FIM); WordPress audit log |
| 6 | Visible administrator created via user-new.php with credentials from xa=admin_creds | WordPress audit log, database | GAP on endpoint. WordPress audit log, DB |
| 7 | emer-run.php fetched and run in the admin session: hidden administrator, two must-use plugins, magic login token | File writes in mu-plugins, database options | Q5, Q6 (FIM); DB options check |
| 8 | Installer backdates every file it writes to the oldest timestamp in the WordPress root | File timestamps | Q5 and Q6 use event time, not mtime |
| 9 | Result reported to the C2 via fetch, sendBeacon and an image-request fallback | Admin workstation DNS and proxy | Q1 |
| 10 | Later: attacker uses /wp-login.php?_wplogin=<token> or hits the file manager directly, uploads a second shell | Web access logs; web-worker child processes | Q3, Q4; web logs for the direct requests |
Consolidated IOC Table
| Type | Value | Confidence | Action | Context | Expiry |
|---|---|---|---|---|---|
| domain | imgcdn1[.]com | High | detect | Serves x.js, c.php, plugin ZIP. Registered 2026-10-01 (Patchstack). Single original source | +6 months |
| url | https://imgcdn1[.]com/fz/x.js | High | detect | Browser implant, 19,378 bytes (Patchstack) | +6 months |
| url | https://imgcdn1[.]com/fz/c.php | High | detect | C2 and payload server; ?xa=check, ?xa=pkg, ?xa=admin_creds endpoints | +6 months |
| sha256 | af803bc822cf9596b1f1785e1d59642f91f6fbc21f8e9943feefa19a6df8c2a4 | Medium | detect | x.js (single source) | +6 months |
| md5 | 8a4413c1aa058179ff54b95571bcf2a3 | Medium | detect | x.js (single source) | +6 months |
| sha256 | ccc95113334357c2a3671aae12043bff00020858cec115f7e724fe43e9f1fc24 | Medium | detect | wp-smart-thumbnails.php, packed dropper and file manager | +6 months |
| md5 | da651d4f191a7c363806fe5357752015 | Medium | detect | wp-smart-thumbnails.php (main plugin file) | +6 months |
| sha256 | ed00234ad2b67dbbc9073e8c711b18b7d39e0a5255b6159189f75cf2ed99cfb9 | Medium | detect | emer-run.php, persistence installer | +6 months |
| md5 | 1024732009983dd5e54b4cf5593f04d4 | Medium | detect | emer-run.php | +6 months |
| path | /wp-content/plugins/wp-smart-thumbnails/ (wp-smart-thumbnails.php, emer-run.php, readme.txt, includes/class-thumb-cache.php) | High | hunt | Fake plugin "WP Smart Thumbnails" 1.2.4 by "MediaPress Labs". Slug is campaign-specific | n/a |
| path | /wp-content/mu-plugins/class-wp-query-<8 hex>.php | High | hunt | Hides the hidden administrator (hooks pre_user_query, users_list_table_query_args, views_users). Obfuscated, per-site key, no fixed hash | n/a |
| path | /wp-content/mu-plugins/class-wp-token-validate.php | High | hunt | Trades the login token for a session cookie (hooks init at priority 1) | n/a |
| wp option | fz_emer_done_v1, fz_emer_login_tokens | High | hunt | Campaign state. Presence of fz_emer_login_tokens means a backdoor login token is live | n/a |
| url pattern | /wp-login.php?_wplogin=<token> | High | hunt | Backdoor login, authenticates as the oldest administrator | n/a |
| string | dyiv:// stream wrapper; title string "Moon, tell me if I could"; Local Storage prefix __xp_v9_ | Medium | hunt | Dropper and file manager internals; browser-side state (Patchstack) | n/a |
| account pattern | usernames drawn from words such as support, updater, maintenance, backup; @wordpress.org email; 16-character random password | Medium | enrich | Hidden administrator profile. Dictionary of about fifty words, so names vary | +1 month |
| ipv4 | 204.8.96.109, 147.90.235.22, 90.184.10.74, 94.16.115.121 (WPC, 2026-10-04); 43.250.53.42 (Ninja Forms, 2026-10-05) | Low | hunt | Observed delivery sources. Four are Tor exit nodes; the fifth is flagged for spam and brute force (Patchstack). Not durable, not exhaustive | +3 months |
Patchstack states these indicators describe the campaign observed in its telemetry and should not be read as exhaustive indicators for exploitation of either CVE.
Affected Surface & Telemetry Matrix
| Surface | Required telemetry | Priority | Gap risk |
|---|---|---|---|
| WordPress administrator browser (Windows, macOS, Linux workstations) | DnsRequest, proxy and DNS logs | High | Partial DNS visible; script execution, nonce theft and session riding are NOT visible to Falcon |
| Linux web server (Apache, NGINX, PHP-FPM) | ProcessRollup2, CriticalFileModified (FIM), NetworkConnectIP4 | High | Partial file events need a FIM policy covering wp-content; PHP file writes are not a native Falcon file-write type |
| Windows web server (IIS with w3wp.exe, or Apache/PHP on Windows) | ProcessRollup2, CriticalFileModified (FIM), NetworkConnectIP4 | Medium | Partial same FIM dependency |
| WordPress application layer (users, options, plugin install) | WordPress audit log plugin, database queries | High | GAP not endpoint-visible. Hidden administrator and fz_emer_* option rows exist only in the database |
| Web access and error logs | Apache or NGINX logs, WAF, CDN logs | High | GAP injection requests, _wplogin logins and direct file-manager hits live here |
| Database | wp_users, wp_usermeta, wp_options, order and submission tables | High | GAP only direct SQL reveals the hidden administrator, because the mu-plugin filters WordPress's own user queries |
ATT&CK Mapping
| Tactic | Technique | Observed behavior | Query / control |
|---|---|---|---|
| Initial Access | T1190 Exploit Public-Facing Application | Stored XSS injected through WooCommerce order data or Ninja Forms submissions | GAP (web/WAF/DB); Q7 context |
| Execution | T1059.007 JavaScript | x.js runs in the administrator's authenticated browser origin | GAP on endpoint; Q1 sees the fetch |
| Command and Control | T1071.001 Web Protocols | HTTPS check-in and reporting to imgcdn1[.]com/fz/c.php | Q1 |
| Command and Control | T1105 Ingress Tool Transfer | Plugin ZIP pulled from c.php?xa=pkg; emer-run.php fetched | Q1, Q2 |
| Persistence | T1505.003 Web Shell | Unauthenticated file manager in wp-smart-thumbnails.php; second shells via its upload and save | Q3, Q4, Q5 |
| Persistence | T1136.001 Create Account: Local Account | Visible administrator via user-new.php and hidden administrator via emer-run.php | GAP (WP audit log, DB); Q4 for CLI use |
| Persistence / Defense Evasion | T1564 Hide Artifacts | Must-use plugin filters the account out of Users screen, counts and queries | Q5, Q6; DB cross-check |
| Persistence / Initial Access | T1078 Valid Accounts | Magic login URL authenticates as the oldest administrator | GAP (web logs); Q4 |
| Defense Evasion | T1070.006 Timestomp | Installed files stamped with the oldest timestamp in the WordPress root | Q5, Q6 (event time) |
| Defense Evasion | T1027 Obfuscated Files or Information | Seven-layer packed dropper, XOR plus gzdeflate must-use plugins, stream wrapper instead of eval() | Partial: name and path based only |
| Defense Evasion | T1036 Masquerading | Fake plugin identity "WP Smart Thumbnails" 1.2.4 by "MediaPress Labs"; decoy readme and class files | Q5 |
Native Audit-Log Hunts
Most of this campaign is invisible to the Falcon sensor. These checks run on the WordPress host, the database and the web server, and are the primary hunt for the application-layer steps. Commands come from the Patchstack detection section unless marked as derived guidance.
1. Enumerate administrators in the database, not in wp-admin
SELECT u.user_login, u.user_email, u.user_registered FROM wp_users u JOIN wp_usermeta m ON m.user_id = u.ID WHERE m.meta_key = 'wp_capabilities' AND m.meta_value LIKE '%administrator%';
Compare to Users, All Users. Any account present in SQL but absent in the interface is hidden by a dropped must-use plugin. An @wordpress.org address on a local account, or a routine word such as support, updater, maintenance or backup, is a further signal. Adjust the wp_ prefix to your table prefix. Use raw SQL: WP-CLI loads must-use plugins, so wp user list can be filtered by the same hooks.
2. Check the campaign's own options
SELECT option_name FROM wp_options
WHERE option_name IN ('fz_emer_done_v1', 'fz_emer_login_tokens');
fz_emer_login_tokens present means at least one backdoor login token is live. Deleting the options alone is not enough while the must-use plugins remain.
3. Inspect the mu-plugins directory and the fake plugin
ls -la wp-content/mu-plugins/ ls -la wp-content/plugins/wp-smart-thumbnails/ 2>/dev/null find wp-content/mu-plugins -type f -name 'class-wp-query-*.php' -o -name 'class-wp-token-validate.php' grep -rl "dyiv://" wp-content/plugins 2>/dev/null
File modification times are not reliable here, because the installer backdates what it writes. Sort by content and by whether the file belongs to your known-good baseline. The mu-plugins directory should normally be empty or hold only files your host or team placed there. Both campaign must-use plugins are obfuscated, so a plain-text grep for hook names will not find them.
4. Search web server logs
grep -E "imgcdn1|/fz/x\.js|/fz/c\.php" /var/log/apache2/*access* /var/log/nginx/*access* grep -E "_wplogin=" /var/log/apache2/*access* /var/log/nginx/*access* grep -E "wp-smart-thumbnails" /var/log/apache2/*access* /var/log/nginx/*access* grep -E "woosb_ids|nf_ajax_submit" /var/log/apache2/*access* /var/log/nginx/*access*
Requests carrying _wplogin to wp-login.php and direct requests to wp-smart-thumbnails.php outside the plugin screens are usable signals (Patchstack). The last pattern is broad: nf_ajax_submit is a normal Ninja Forms action, so triage hits by looking for markup in the submitted values. POST bodies are usually not in access logs, so a WAF or audit log is needed to see the payload itself.
5. Find stored payloads before an administrator opens them (derived guidance)
wp db query "SELECT meta_id, post_id FROM wp_postmeta WHERE meta_value LIKE '%imgcdn1%'" wp db query "SELECT meta_id, order_item_id FROM wp_woocommerce_order_itemmeta WHERE meta_value LIKE '%imgcdn1%'"
This searches the places the sources say the payload is stored (order metadata and form submissions). Table names vary with prefix and WooCommerce storage mode; treat it as a starting point. Remove matching rows without opening them in wp-admin.
6. Review administrator-created accounts and plugin installs around order or submission views
Patchstack advises investigating any administrator account created around the time an administrator viewed WooCommerce orders or Ninja Forms submissions. This needs a WordPress audit-log plugin or equivalent; the stock log does not record it.
7. Administrator workstation (browser)
Search proxy and DNS logs for imgcdn1[.]com. On a workstation known to have opened affected content, check browser Local Storage for keys beginning __xp_v9_ (Patchstack). Treat the administrator's WordPress session as compromised.
CrowdStrike LogScale CQL Hunt Queries
Seven endpoint queries cover only the endpoint-visible slice. Q5 and Q6 depend on a Falcon FIM or critical-file policy that includes wp-content, which is not on by default (marked "validate in tenant"). Q7 depends on inbound-accept telemetry. XSS execution, nonce theft, hidden-administrator creation and the database option rows have no Falcon query; see Section 7 and Section 15.
Looks for: any DNS lookup of the single domain that serves every stage: the browser implant, the C2 check-in and the plugin ZIP. The lookup is made by the administrator's browser, so expect hits on admin workstations first. Works on Windows, macOS and Linux sensors.
// HUNT: DNS lookup of imgcdn1[.]com, the host serving the stored-XSS implant and its C2 // MITRE: T1071.001, T1105, T1059.007 // CONF: high // FP: low // COST: low // REQUIRES: DnsRequest on admin workstations (Windows, macOS) and on web servers (Windows, Linux) // FALSE POSITIVES: threat-intel sandboxes or DNS-sinkhole testing that deliberately resolves the domain // NOTE: the implant runs in the ADMINISTRATOR'S BROWSER, so the lookup normally comes from an admin workstation, not the web server #event_simpleName=DnsRequest | DomainName=/(^|\.)imgcdn1\.com$/i | groupBy([aid, ComputerName, ContextBaseFileName, DomainName], function=[count(as=Lookups), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen)], limit=20000) | sort(FirstSeen, order=asc) | table([aid, ComputerName, ContextBaseFileName, DomainName, Lookups, FirstSeen, LastSeen])
Looks for: low-count outbound web connections from the web or PHP worker, a follow-on indicator if a second-stage fetch happens server-side. FP: plugin and core update checks, payment and shipping APIs and CDNs are normal; baseline before alerting (see the TUNING line in the card).
// HUNT: Web-server or PHP worker making outbound web connections to rare external addresses // MITRE: T1105, T1071.001 // CONF: low // FP: high // COST: medium // REQUIRES: NetworkConnectIP4 from Linux (httpd, apache2, nginx, php-fpm) and Windows (w3wp.exe) web servers // FALSE POSITIVES: WordPress core and plugin update checks, payment gateway and shipping APIs, license servers, CDN and font fetches, outbound mail APIs // TUNING: baseline for 14 days in investigate mode, then exclude every destination the site legitimately calls, for example by adding: | !in(RemoteAddressIP4, values=["REPLACE_WITH_BASELINED_API_IP"]) and keep only the rare tail (Conns <= 5) // NOTE: Patchstack describes the fetches as browser-side, so this is a second-stage and follow-on hunt, not a primary detection #event_simpleName=NetworkConnectIP4 | ContextBaseFileName=/^(httpd|apache2|nginx|php-fpm[0-9.]*|php-cgi[0-9.]*|lsphp[0-9]*|w3wp\.exe)$/i | in(RemotePort, values=[80, 443]) | !cidr(RemoteAddressIP4, subnet="10.0.0.0/8") | !cidr(RemoteAddressIP4, subnet="172.16.0.0/12") | !cidr(RemoteAddressIP4, subnet="192.168.0.0/16") | !cidr(RemoteAddressIP4, subnet="127.0.0.0/8") | groupBy([aid, ComputerName, ContextBaseFileName, RemoteAddressIP4], function=[count(as=Conns), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen)], limit=20000) | Conns <= 5 | sort(Conns, order=asc) | table([aid, ComputerName, ContextBaseFileName, RemoteAddressIP4, Conns, FirstSeen, LastSeen])
Looks for: the classic web-shell follow-on. The fake plugin's file manager cannot execute commands itself but can write a second shell. FP: image and PDF plugins that shell out; exclude their helper commands (see TUNING).
// HUNT: Shell, downloader or recon utility spawned by a web-server or PHP worker process // MITRE: T1505.003, T1059 // CONF: medium // FP: medium // COST: low // REQUIRES: ProcessRollup2 on Linux (apache2, httpd, nginx, php-fpm) and Windows (w3wp.exe) web servers // FALSE POSITIVES: image and PDF plugins that shell out to convert, gs or wkhtmltopdf; backup and cache plugins; hosting-panel helpers // TUNING: after a baseline, exclude known plugin helper commands, for example: | !(FileName=/^(sh|bash)$/i CommandLine=/convert|gs |wkhtmltopdf|ffmpeg/i) ; escalate when the parent is a PHP worker and the child is a downloader (curl, wget) or an interactive shell with no helper pattern // NOTE: the fake plugin's file manager has no command-execution primitive (Patchstack), so this catches the SECOND shell an attacker drops through it, not the file manager itself #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(httpd|apache2|nginx|php-fpm[0-9.]*|php-cgi[0-9.]*|lsphp[0-9]*|w3wp\.exe)$/i | FileName=/^(sh|bash|dash|zsh|ash|nc|ncat|netcat|socat|curl|wget|python[0-9.]*|perl|ruby|whoami|id|uname|cmd\.exe|powershell\.exe|pwsh\.exe|certutil\.exe|bitsadmin\.exe|whoami\.exe|curl\.exe|mshta\.exe)$/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine])
Looks for: shell or WP-CLI activity touching the plugin slug, the installer, the two must-use plugin names, the two option names or the _wplogin token. Also fires on your own responders, so tune by parent process. FP: IR triage and cleanup scripts (see TUNING).
// HUNT: Process command line referencing the campaign's plugin, installer, must-use plugin, option or login-token names // MITRE: T1505.003, T1136.001, T1078 // CONF: high // FP: medium // COST: low // REQUIRES: ProcessRollup2 on Linux and Windows web servers // FALSE POSITIVES: your own incident-response triage (grep, find, ls, rm, wp db query) and file-integrity or backup tooling that names these paths while cleaning up // TUNING: exclude your IR toolkit and approved admin sessions, for example: | !(ParentBaseFileName=/^(REPLACE_WITH_IR_WRAPPER)$/i) ; treat any hit whose parent is a web or PHP worker, or whose command line does not come from a known responder, as hostile #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | CommandLine=/(wp-smart-thumbnails|emer-run\.php|class-wp-token-validate|class-wp-query-[0-9a-f]{8}\.php|fz_emer_(done_v1|login_tokens)|_wplogin=)/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine])
Looks for: the four filenames Patchstack lists. Depends on a Falcon FIM or critical-file policy that covers wp-content. This is the best endpoint-side proof the implant landed, and it is not on by default.
// HUNT: Campaign-named PHP files written to a WordPress host (dropper, installer, token-validate and query must-use plugins) // MITRE: T1505.003, T1564, T1070.006 // CONF: high // FP: low // COST: low // REQUIRES: CriticalFileModified (Falcon File Integrity Monitoring / critical-file policy covering wp-content) -- ⚠ validate in tenant: wp-content is NOT monitored by default and the policy must be scoped to plugins and mu-plugins // FALSE POSITIVES: none expected; these filenames are not part of WordPress core or any legitimate plugin Patchstack describes // NOTE: the event time is the write time as the sensor saw it, so the installer's file-timestamp backdating does not hide it here #event_simpleName=CriticalFileModified | FileName=/^(class-wp-token-validate\.php|class-wp-query-[0-9a-f]{8}\.php|emer-run\.php|wp-smart-thumbnails\.php)$/i | table([@timestamp, aid, ComputerName, FileName, FilePath, ContextBaseFileName, ContextProcessId, UserName])
Looks for: new files in the must-use directory, which the Plugins screen never lists and which survive removal of the fake plugin. FP: managed hosts and security suites that ship their own mu-plugins (see TUNING).
// HUNT: Any PHP file created or changed in a WordPress mu-plugins directory // MITRE: T1505.003, T1564 // CONF: medium // FP: medium // COST: low // REQUIRES: CriticalFileModified scoped to wp-content/mu-plugins -- ⚠ validate in tenant: add the mu-plugins path to your FIM policy first // FALSE POSITIVES: managed WordPress hosts and caching or security suites that ship their own mu-plugins, and deliberate deploys from CI // TUNING: exclude the filenames your host or deploy pipeline legitimately writes, for example: | !FileName=/^(REPLACE_WITH_HOST_MU_PLUGIN)\.php$/i ; keep every other write, especially one made by a web or PHP worker process (ContextBaseFileName) with no matching change ticket // NOTE: both campaign must-use plugins are obfuscated per site with a fresh key, so no fixed hash exists; the directory itself is the signal #event_simpleName=CriticalFileModified | FilePath=/mu-plugins/i | FileName=/\.php$/i | table([@timestamp, aid, ComputerName, FileName, FilePath, ContextBaseFileName, ContextProcessId, UserName])
Looks for: contact from the five addresses in the source table. Mostly Tor exits, so this is context for scoping, never a verdict.
// HUNT: Inbound web connections from the five source IPs Patchstack observed delivering the payload // MITRE: T1190 // CONF: low // FP: low // COST: low // REQUIRES: NetworkReceiveAcceptIP4 on web servers -- ⚠ validate in tenant: inbound-accept telemetry coverage differs by sensor version and OS // FALSE POSITIVES: four of the five addresses are Tor exit nodes (Patchstack) shared with unrelated traffic, so a hit shows contact, not compromise // NOTE: hunt and enrich only, the addresses are not durable and the list is not exhaustive (Patchstack) #event_simpleName=NetworkReceiveAcceptIP4 | in(RemoteAddressIP4, values=["204.8.96.109", "147.90.235.22", "90.184.10.74", "94.16.115.121", "43.250.53.42"]) | in(LocalPort, values=[80, 443, 8080, 8443]) | table([@timestamp, aid, ComputerName, RemoteAddressIP4, LocalPort, ContextBaseFileName])
CrowdStrike Custom IOA Recommendations
| IOA name | Pattern | Exclusions | Deployment path |
|---|---|---|---|
| Web worker spawns shell or downloader (from Q3) | Process creation. Parent image matches httpd, apache2, nginx, php-fpm, php-cgi or w3wp.exe; child matches sh, bash, curl, wget, cmd.exe, powershell.exe | Exclude known image and PDF helper commands after baseline | Detect-only first; promote to Prevent only after a clean baseline window. Confirm Linux rule-type availability in your tenant |
| imgcdn1[.]com domain lookup (from Q1) | Domain name rule, imgcdn1.com and subdomains | None expected | Detect (single-source indicator, so not prevent). Prefer the IOC Management entry in Section 10 for the same domain |
| Campaign command-line artifacts (from Q4) | Process creation. Command line contains wp-smart-thumbnails, emer-run.php or class-wp-token-validate | Exclude your IR tooling by parent process | Detect. FP medium, so no automated action |
File-based Q5 and Q6 are FIM-dependent and not IOA candidates. IOA rule-type support differs between Windows and Linux sensors, so confirm each rule type exists for your sensor version before building.
Machine-Readable IOC Appendix
type,value,action,severity,expiration,description,tags domain,imgcdn1.com,detect,high,2027-04-07,Payload and C2 host for WordPress stored-XSS implant (Patchstack),wordpress;stored-xss;imgcdn1 sha256,af803bc822cf9596b1f1785e1d59642f91f6fbc21f8e9943feefa19a6df8c2a4,detect,high,2027-04-07,x.js browser implant 19378 bytes,wordpress;stored-xss;imgcdn1 md5,8a4413c1aa058179ff54b95571bcf2a3,detect,high,2027-04-07,x.js browser implant,wordpress;stored-xss;imgcdn1 sha256,ccc95113334357c2a3671aae12043bff00020858cec115f7e724fe43e9f1fc24,detect,high,2027-04-07,wp-smart-thumbnails.php packed dropper and file manager,wordpress;stored-xss;imgcdn1 md5,da651d4f191a7c363806fe5357752015,detect,high,2027-04-07,wp-smart-thumbnails.php main plugin file,wordpress;stored-xss;imgcdn1 sha256,ed00234ad2b67dbbc9073e8c711b18b7d39e0a5255b6159189f75cf2ed99cfb9,detect,high,2027-04-07,emer-run.php persistence installer,wordpress;stored-xss;imgcdn1 md5,1024732009983dd5e54b4cf5593f04d4,detect,high,2027-04-07,emer-run.php persistence installer,wordpress;stored-xss;imgcdn1 # No row is action=prevent: every indicator is single-sourced to the Patchstack analysis (two-source rule). # The two mu-plugins are obfuscated per site with a fresh key, so no hash exists. Detect them by path (Q5, Q6).
# Filesystem (hunt by path and content, not by mtime) wp-content/plugins/wp-smart-thumbnails/wp-smart-thumbnails.php wp-content/plugins/wp-smart-thumbnails/emer-run.php wp-content/plugins/wp-smart-thumbnails/readme.txt (46 bytes, decoy) wp-content/plugins/wp-smart-thumbnails/includes/class-thumb-cache.php (41 bytes, decoy) wp-content/mu-plugins/class-wp-query-<8 hex>.php wp-content/mu-plugins/class-wp-token-validate.php # Plugin identity (fake) "WP Smart Thumbnails" 1.2.4 by "MediaPress Labs" # WordPress options rows fz_emer_done_v1 fz_emer_login_tokens # Web request patterns GET /fz/x.js, /fz/c.php?xa=check|pkg|admin_creds (to imgcdn1[.]com, from admin browsers) GET /wp-login.php?_wplogin=<token> Direct request to wp-content/plugins/wp-smart-thumbnails/wp-smart-thumbnails.php POST /wp-admin/admin-ajax.php action=nf_ajax_submit (markup in submitted fields) Request parameter woosb_ids[...][qty] with a numeric prefix followed by markup # Code and browser artifacts stream wrapper scheme dyiv:// webshell title "Moon, tell me if I could" Local Storage key prefix __xp_v9_ mu-plugin hooks: pre_user_query, users_list_table_query_args, views_users, init (priority 1) # Hidden administrator profile username from: support, updater, maintenance, backup (and about fifty similar words) email domain: @wordpress.org password: 16 random characters
# Observed delivery sources (Patchstack). HUNT / ENRICH ONLY, not for blocking. # Four are Tor exit nodes; blocking addresses is not a durable mitigation (Patchstack). 2026-10-04 CVE-2026-93836 (WPC Product Bundles) 204.8.96.109 147.90.235.22 90.184.10.74 94.16.115.121 2026-10-05 CVE-2026-94504 (Ninja Forms) 43.250.53.42
# Version audit (WP-CLI). Fixed: Ninja Forms 3.15.4+, WPC Product Bundles 8.6.7+ (move to the current release)
wp plugin get ninja-forms --fields=name,version,status
wp plugin get woo-product-bundle --fields=name,version,status
wp plugin list --fields=name,version,status,update
# Fake plugin and must-use plugin presence
wp plugin is-installed wp-smart-thumbnails && echo "FAKE PLUGIN PRESENT"
ls -la wp-content/mu-plugins/
# Raw SQL checks (do not use wp user list, mu-plugins can filter it)
wp db query "SELECT u.user_login,u.user_email,u.user_registered FROM wp_users u JOIN wp_usermeta m ON m.user_id=u.ID WHERE m.meta_key='wp_capabilities' AND m.meta_value LIKE '%administrator%'"
wp db query "SELECT option_name FROM wp_options WHERE option_name IN ('fz_emer_done_v1','fz_emer_login_tokens')"
Detection Validation Gates
| Gate | Requirement | Status in this pack |
|---|---|---|
| Telemetry ready | Falcon sensor on every web server host and every WordPress administrator workstation; a FIM or critical-file policy covering wp-content/plugins and wp-content/mu-plugins before Q5 and Q6 are relied on | Verify per-tenant FIM coverage is the usual gap |
| Benign baseline | Run Q2, Q3, Q4 and Q6 in investigate mode for at least 14 days before any automated action. Record legitimate outbound APIs, plugin helper commands and host-managed mu-plugins | Required before go-live |
| Positive tests | Q1: resolve a harmless test hostname in a controlled lab and confirm the query shape; do not resolve the live domain from a production host. Q5: create a file named class-wp-token-validate.php under a monitored lab path and confirm the FIM event | Pending tenant validation |
| Promotion criteria | Zero unexplained hits over the baseline window before any Q3, Q4 or IOA is moved from audit to enforce. Q1 and Q5 may be promoted to alert earlier because their false-positive surface is small | Pending |
Hardening: Tiered & Deployable
Authorities: MITRE ATT&CK mitigations (the why), the WordPress.org Hardening WordPress and wp-config documentation (snapshots 07, 08), and the CIS Apache HTTP Server and NGINX Benchmarks for the web tier. No formal CIS WordPress benchmark was consulted for this pack, so WordPress controls cite the WordPress.org documentation and are marked where they are best practice only.
- Update Ninja Forms to 3.15.4 or later and WPC Product Bundles to the current release. Patchstack names 8.6.7; the vendor changelog lists a further stored-XSS sanitization fix in 8.7.3. MITRE M1051 (Update Software), T1190. Verify:
wp plugin list --fields=name,version,update. - Clean stored payloads and assume prior compromise. Search order and submission data for the domain string (Section 7, check 5), remove matches without opening them in wp-admin, then run the administrator, option and mu-plugins checks (checks 1 to 3). Updating does not clean an existing infection (BleepingComputer, Patchstack). MITRE M1047 (Audit).
- Sinkhole imgcdn1[.]com for administrator workstations and web servers at DNS or the web proxy. MITRE M1037 (Filter Network Traffic), M1031 (Network Intrusion Prevention). Verify: Q1 returns only the sinkhole lookups and a resolver query returns the sinkhole address.
- Block the backdoor login and the fake plugin at the web server. Reject any request whose query string contains
_wplogin=and deny thewp-smart-thumbnailsplugin path (playbook step 3). MITRE M1050 (Exploit Protection), T1078. Verify:curl -I "https://site.example.com/wp-login.php?_wplogin=x"returns 403. - Stop reviewing orders and form submissions as an administrator. Give order and submission reviewers a role without
install_plugins(for example Shop Manager). The implant needs administrator rights to install the plugin. MITRE M1026 (Privileged Account Management), M1018. Source: WordPress.org hardening guidance on least privilege (snapshot 07). Verify:wp user list --role=administrator --fields=user_logincontains only people who must administer.
- Set DISALLOW_FILE_MODS in wp-config.php. It blocks plugin and theme installation and update from the admin area, which removes the step the implant uses to land the fake plugin. Trade-off: updates must then come through WP-CLI, Composer or CI. MITRE M1054 (Software Configuration), M1042. Source: WordPress.org wp-config documentation (snapshot 08). Verify:
wp config get DISALLOW_FILE_MODSreturns 1. Pilot on one site first. - Make plugin and mu-plugins directories non-writable by the web user. Files owned by a deploy account, directories 755 and files 644, web user read-only (WordPress.org File Permissions guidance, snapshot 07). The installer cannot write its must-use plugins if PHP cannot write the directory. MITRE M1022 (Restrict File and Directory Permissions). Verify:
sudo -u www-data touch wp-content/mu-plugins/tfails. Pilot first, because dashboard auto-updates stop working. - Restrict wp-login.php and wp-admin to a VPN or office range. This does not stop the XSS, which fires from an allowed administrator's browser, but it stops attackers from using the magic login URL and a created account from their own network. MITRE M1030 (Network Segmentation). CIS Apache HTTP Server and NGINX Benchmarks cover access restriction. Verify: external request to wp-login.php returns 403.
- File-integrity monitoring on wp-content/plugins and wp-content/mu-plugins (Falcon FIM policy, AIDE or auditd) so Q5 and Q6 have data. MITRE M1047 (Audit). Verify: create a test file in a lab copy and confirm an event.
- Deploy a WAF rule or virtual patch for markup in
woosb_idsquantity values and in Ninja Forms AJAX submissions, in detection-only mode first. MITRE M1050. ⚠ best-practice: OWASP Core Rule Set covers generic XSS, tune for these fields. - Add a WordPress audit-log plugin that records user creation, role changes and plugin installs, and ship it to your SIEM. MITRE M1047. ⚠ best-practice, no formal benchmark.
- Be clear what MFA does not fix. The implant never reads the session cookie; it rides the live session, so MFA and HttpOnly cookies do not stop it (Patchstack). Keep MFA for the credential-theft cases it does address (M1032) but do not count it as a control for this attack.
- Immutable, CI-driven WordPress deployment. Read-only application filesystem, plugins and mu-plugins deployed from a reviewed repository, so an unreviewed PHP file cannot persist. MITRE M1022, M1054. ⚠ best-practice, no formal benchmark.
- Content-Security-Policy for wp-admin, Report-Only first. A policy that blocks third-party script origins would stop a script tag pointing at an attacker domain. WordPress admin uses inline scripts, so expect a long tuning phase. MITRE M1021 (Restrict Web-Based Content). ⚠ best-practice, no formal benchmark.
- Plugin vulnerability feed with a patch SLA. Subscribe to a WordPress vulnerability intelligence source and hold an SLA for exploited flaws measured in days. Patchstack notes the domain appeared roughly ten days after public disclosure of both flaws. MITRE M1051, M1016 (Vulnerability Scanning).
- Reduce the plugin estate and the number of administrators. Remove unused plugins (WordPress.org guidance) and keep administrator accounts to the minimum, reviewed quarterly against a database query, not the Users screen. MITRE M1018, M1026.
- Harden the web tier to CIS. Apply the CIS Apache HTTP Server Benchmark or CIS NGINX Benchmark and a CIS Linux benchmark for the host OS. Verify with the benchmark's own assessment tooling.
Deployable Playbooks
Linux examples use Apache or NGINX with a Debian-style layout. Replace /var/www/html, www-data, deploy and 192.0.2.0/24 with your own values. Test each change on a staging copy first.
Step 1: Compromise triage (read-only)
Prerequisites: Shell access and WP-CLI on the WordPress host; database credentials from wp-config.php
Reboot required: No
cd /var/www/html
# 1. administrators straight from the database (not filtered by mu-plugins)
wp db query "SELECT u.user_login,u.user_email,u.user_registered FROM wp_users u JOIN wp_usermeta m ON m.user_id=u.ID WHERE m.meta_key='wp_capabilities' AND m.meta_value LIKE '%administrator%'"
# 2. campaign options
wp db query "SELECT option_name FROM wp_options WHERE option_name IN ('fz_emer_done_v1','fz_emer_login_tokens')"
# 3. must-use and fake plugin
ls -la wp-content/mu-plugins/ wp-content/plugins/wp-smart-thumbnails/ 2>/dev/null
# 4. take evidence before changing anything
tar -czf /root/wp-evidence-$(date +%F).tgz wp-content/mu-plugins wp-content/plugins/wp-smart-thumbnails 2>/dev/null
Rollback:
none required: every command in this step only reads state, except the evidence archive, which can be deleted with rm /root/wp-evidence-*.tgz
Step 2: Lock down wp-config.php
Prerequisites: Staging validated; an alternative update path (WP-CLI, Composer or CI) exists because dashboard installs stop working
Reboot required: No (PHP-FPM reload not needed; the constant is read per request)
cd /var/www/html cp wp-config.php wp-config.php.bak-$(date +%F) # add above the "That's all, stop editing" line sed -i "/stop editing/i define( 'DISALLOW_FILE_MODS', true );\ndefine( 'DISALLOW_FILE_EDIT', true );\ndefine( 'FORCE_SSL_ADMIN', true );" wp-config.php wp config get DISALLOW_FILE_MODS
Rollback:
# restore the previous file cp wp-config.php.bak-YYYY-MM-DD wp-config.php
Step 3: Block the backdoor login, the fake plugin and (optionally) restrict admin access at the web server
Prerequisites: Apache 2.4 with mod_rewrite, or NGINX; an admin network range to allow
Reboot required: No (graceful reload of the web server)
# --- Apache 2.4 (vhost or .htaccess, outside the WordPress BEGIN/END block) ---
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)_wplogin= [NC]
RewriteRule ^ - [F,L]
RewriteRule ^wp-content/plugins/wp-smart-thumbnails/ - [F,L]
<Files "wp-login.php">
Require ip 192.0.2.0/24
</Files>
# --- NGINX ---
location ~* ^/wp-content/plugins/wp-smart-thumbnails/ { return 403; }
location = /wp-login.php {
if ($arg__wplogin) { return 403; }
allow 192.0.2.0/24;
deny all;
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
# check config, then reload
# apachectl configtest && systemctl reload apache2
# nginx -t && systemctl reload nginx
Rollback:
# remove the added lines (or restore your backup of the vhost/.htaccess/server block), then: # apachectl configtest && systemctl reload apache2 # nginx -t && systemctl reload nginx
Step 4: Make plugin and must-use directories read-only to PHP
Prerequisites: Root or deploy-account access; a deployment path that does not depend on PHP writing to these directories; staging pilot
Reboot required: No
cd /var/www/html
chown -R deploy:deploy wp-content/plugins wp-content/mu-plugins
find wp-content/plugins wp-content/mu-plugins -type d -exec chmod 755 {} \;
find wp-content/plugins wp-content/mu-plugins -type f -exec chmod 644 {} \;
# keep uploads writable, but never executable PHP
# verify: this must FAIL
sudo -u www-data touch wp-content/mu-plugins/.write-test
Rollback:
# restore the previous ownership recorded before the change (example) chown -R www-data:www-data wp-content/plugins wp-content/mu-plugins
Step 5: File-integrity watches and DNS sinkhole
Prerequisites: auditd installed; dnsmasq or your resolver supports local overrides; Falcon FIM policy configured separately in the console for Q5 and Q6
Reboot required: No
# /etc/audit/rules.d/wordpress-persistence.rules -w /var/www/html/wp-content/mu-plugins -p wa -k wp_mu_plugins -w /var/www/html/wp-content/plugins -p wa -k wp_plugins # load: augenrules --load verify: auditctl -l | grep wp_ # /etc/dnsmasq.d/sinkhole-imgcdn1.conf (the domain is the Patchstack-published payload and C2 host) address=/imgcdn1.com/0.0.0.0 # reload: systemctl restart dnsmasq verify: dig +short imgcdn1.com @127.0.0.1 returns 0.0.0.0
Rollback:
# remove the rule files, then reload: # rm /etc/audit/rules.d/wordpress-persistence.rules && augenrules --load # rm /etc/dnsmasq.d/sinkhole-imgcdn1.conf && systemctl restart dnsmasq
Step 6: ModSecurity detection-only rules for the two delivery fields
Prerequisites: ModSecurity v2 or v3 with the OWASP Core Rule Set already in DetectionOnly; unique rule ids from your local range
Reboot required: No (web server reload)
# DetectionOnly first. Promote to deny only after reviewing a week of audit logs. SecRule ARGS_NAMES "@rx ^woosb_ids" \ "id:9100101,phase:2,t:none,log,auditlog,pass,chain,msg:'Markup in WPC Product Bundles quantity'" SecRule ARGS "@rx (?i)<\s*(script|img|svg)|onerror\s*=" "t:none" SecRule REQUEST_URI "@contains admin-ajax.php" \ "id:9100102,phase:2,t:none,log,auditlog,pass,chain,msg:'Markup in Ninja Forms AJAX submission'" SecRule ARGS:action "@streq nf_ajax_submit" "chain" SecRule ARGS "@rx (?i)</textarea>|<\s*(script|img|svg)|onerror\s*=" "t:none"
Rollback:
# delete the two SecRule blocks above and reload the web server
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| 0 · Scope | List every WordPress site with Ninja Forms or WPC Product Bundles. For each, note version and whether an administrator opened an order or form submission since 2026-10-04. Run Section 7 checks 1 to 4. Identify the administrator workstations involved | IR lead, WordPress owner | Plugin version inventory; admin list from SQL; access-log extracts |
| 1 · Isolate | Put the site in maintenance mode or restrict access to responders. Sinkhole imgcdn1[.]com. Block _wplogin requests and the fake plugin path. Do not delete implant files yet. Treat the administrator's browser session as compromised and sign administrators out. Capture a disk snapshot, the database dump and the tar archive from playbook step 1 | Platform engineer, IR lead | Snapshot IDs; hash of archive; web logs for the exposure window |
| 2 · Eradicate | Delete unauthorized administrator accounts found by SQL, including the hidden one. Remove wp-smart-thumbnails, every unknown file in mu-plugins (not just the two named ones, the file manager can write anywhere) and the fz_emer_done_v1 and fz_emer_login_tokens options. Search the whole web root for other recently added PHP, remembering timestamps are backdated, so compare against known-good code. Update both plugins. Reinstall WordPress core and all plugins from clean sources | WordPress owner | Before and after file listings; SQL output; list of removed accounts |
| 2b · Rotate | Reset passwords for every administrator, and treat the oldest administrator's password as compromised, since the magic URL authenticated as that account. Rotate WordPress authentication keys and salts (this logs everyone out), database credentials, SFTP or SSH keys and API keys stored in the site. Remove stored payload rows from order and submission data | WordPress owner, IAM | Rotation record; new wp-config salts hash (not the values) |
| 3 · Recover | Re-enable access behind the Section 12 controls (DISALLOW_FILE_MODS, read-only plugin directories, login restriction). Restore from a backup taken before 2026-10-04 only if the clean rebuild is not possible, then re-run checks. Watch Q1, Q5 and Q6 and the audit log for re-infection for at least 30 days | Platform engineer | Post-clean SQL and file checks; monitoring dashboard |
| 4 · Review | Record which administrator views triggered execution, whether any other data was touched through the file manager, and notify per your breach obligations if user data was reachable. Feed gaps back into Section 15 | IR lead, Security | Incident report; closed gaps |
Detection Coverage Map
This pack is intentionally candid: more of the attack chain is outside Falcon endpoint visibility than inside it. The endpoint adds value on the administrator workstation and, with FIM enabled, on the web server file system.
| Technique | Behavior | CQL | IOA | Coverage |
|---|---|---|---|---|
| T1190 | Stored XSS injection via order data or form submission | - | - | GAP not endpoint-visible; web/WAF/DB only. Q7 gives IP context only |
| T1059.007 | x.js executes in the administrator's browser; nonce scraping and session riding | Q1 (DNS) | - | GAP execution and nonce theft invisible to Falcon; the lookup is the only trace |
| T1071.001 / T1105 | C2 check-in, plugin ZIP and installer fetched from imgcdn1[.]com | Q1, Q2 | Q1 domain IOA | Partial admin-workstation DNS is solid; server-side fetch is unconfirmed |
| T1505.003 | Fake plugin and file manager written to wp-content/plugins; second shells | Q3, Q4, Q5 | Q3 IOA | Partial Q5 depends on a FIM policy; PHP file writes are not a native Falcon file-write type; direct hits on the file manager are web-log only |
| T1136.001 | Visible and hidden administrator accounts created | Q4 (CLI only) | - | GAP creation happens inside WordPress and the database; needs the WordPress audit log and SQL |
| T1564 | Hidden administrator via mu-plugins hooks | Q5, Q6 | - | Partial file side only; the hook behavior is not observable |
| T1078 | Magic login URL as the oldest administrator | Q4 | - | GAP web access logs and WAF; a login by this route is indistinguishable by user ID |
| Database state | fz_emer_done_v1 and fz_emer_login_tokens options rows | - | - | GAP database only (Section 7, check 2) |
| T1070.006 | Timestamp backdating of dropped files | Q5, Q6 | - | Partial FIM event time defeats it; mtime-based hunts do not |
| T1027 / T1036 | Packed dropper, obfuscated must-use plugins, fake plugin identity | Q5 | - | Partial name and path only; no fixed hashes exist for the mu-plugins |
Validation gates (Section 11): sensor and FIM coverage must be confirmed before Q5 and Q6 are trusted; run Q2, Q3, Q4 and Q6 for at least 14 days in investigate mode before enforcement; promote only after a clean baseline.
Hunt Summary Ticket
TITLE: WordPress stored-XSS admin takeover: fake wp-smart-thumbnails plugin, hidden administrator, backdoor login (Ninja Forms CVE-2026-94504, WPC Product Bundles CVE-2026-93836) SEVERITY: High (exploited in the wild since 2026-10-04; CVSS 7.1 to 7.2 per sources) SCOPE: WordPress sites running Ninja Forms 3.15.3 or earlier or WPC Product Bundles 8.6.6 or earlier where an administrator viewed an order or form submission; administrator workstations; Linux and Windows web servers HYPOTHESIS: Attacker-stored script ran in an administrator's session, installed the fake plugin, created visible and hidden administrators, dropped two must-use plugins and a magic login token, and left an unauthenticated file manager QUERIES: Q1-Q7 (Section 8); IOA candidates Q1, Q3, Q4 (Section 9); native DB, filesystem and log hunts (Section 7) DO FIRST: 1. Update both plugins, then run the SQL administrator and option checks (Section 7, checks 1 and 2) 2. Run Q1 across 30 days for administrator workstations resolving imgcdn1[.]com 3. List mu-plugins and the fake plugin directory on every WordPress host (Section 7, check 3) FINDINGS: [populate per engagement] GAPS: XSS execution, nonce theft, hidden-administrator creation, option rows and magic-login use are not endpoint-visible; Q5 and Q6 need a FIM policy on wp-content; PHP file writes are not a native Falcon file-write type (Section 5, Section 15) ACTIONS: patch; clean stored payloads; remove implants; rotate credentials and salts; DISALLOW_FILE_MODS; read-only plugin directories; sinkhole the domain OWNER: [assign per engagement] VERSION: v0.1, 2026-10-07