wp2shell — WordPress Core Pre-Authentication RCE Chain
author__not_in SQL injection) · exploited in the wild since 2026-07-17Executive Summary
What the attacker wants: a durable webshell on an internet-facing WordPress host, reached without a single credential. wp2shell chains a logic flaw in the WordPress REST API batch processor (CVE-2026-63030, /wp-json/batch/v1) with a SQL injection in the author__not_in parameter of WP_Query (CVE-2026-60137). The batch endpoint validates and executes sub-requests in two separate loops; when path parsing fails on one sub-request the error lands in the validation array but not the matches array, desynchronising the two so every later sub-request dispatches under someone else's handler. The attacker rides that confusion into the injection, forges an administrator, uploads a plugin, and gets OS command execution as the web-server user.
Why it matters operationally: the attack has no preconditions. A stock install with no plugins and no theme customisation is exploitable by an anonymous request. Disclosure was 2026-07-17 (Adam Kues, Assetnote / Searchlight Cyber, via the WordPress HackerOne programme); public proof-of-concept code and mass scanning followed within hours, and multiple vendors confirmed in-the-wild exploitation by 2026-07-20. Patched builds are 6.8.6, 6.9.5 and 7.0.2; WordPress.org pushed forced auto-updates, which means most managed estates are already fixed and the residual risk concentrates in installs that pin versions, block auto-update, or were compromised before the update landed.
The highest-value defensive angle for Falcon: not the exploit, the payload. A Falcon sensor on the web server never sees the HTTP request path, the 207 Multi-Status response, or the wp2shell user agent — those live in web-server, WAF and proxy logs. What the sensor sees perfectly is the moment the webshell is used: a php-fpm, httpd, apache2, nginx or w3wp.exe process spawning sh, bash, curl, whoami or powershell.exe. That parent-child relationship is close to zero in a healthy estate and it fires on every webshell family reported for this campaign, including the ones nobody has published a hash for yet. Queries Q1, Q2 and Q10 are the pack's spine; the atomic indicators are a bonus, not the plan.
Defender priority: confirm every WordPress host you own is on 6.8.6 / 6.9.5 / 7.0.2 or later, then assume the window 2026-07-17 to patch-time was hostile and hunt it. Patching does not evict a webshell that was already dropped, and the campaign's own scanners were faster than most patch cycles. Run Q1 and Q2 across the last 30 days before you close this out.
Source Review & Web Hunter Notes
| Tier | Source | Key finding | Carry forward |
|---|---|---|---|
| 1 | Wiz Research | Only source publishing atomic IOCs: five SHA1 webshell hashes and five exploitation IPs. Three webshell families (one-liner PHP, ~150KB CMSmap-derived plugin, REST-endpoint base64 command plugin). HTTP 207/200 Multi-Status to the batch endpoint called a high-fidelity success indicator; user agents wp2shell / rezwp2shell. | Yes |
| 1 | Elastic Security Labs | Endpoint-side detection guidance: web server (apache2, httpd, nginx, php-fpm) spawning shells; PHP files created under plugin directories; discovery commands id, whoami, hostname. Explicitly favours behavioural detection over IOC matching. | Yes — this is the pack's hunt thesis |
| 1 | Eye Security (defender / IR guide) | Calls the exploit "log-blind": evidence lives in the database, not access logs. Residual artefacts in oEmbed cache rows and changeset posts. Rogue admin logins wp2_* / w2s_*. Redis/Memcached object cache may break the RCE path while leaving the SQLi live. | Yes |
| 1 | Rapid7 | Root-cause detail (validation and execution in separate loops). Both CVEs added to CISA KEV on 2026-07-21. Notes CVSS 7.5 for CVE-2026-63030. | Partial — CVSS disputed, see note |
| 1 | Tenable | CVSS 9.8 (CVE-2026-63030) and 5.9 (CVE-2026-60137). Full affected/fixed matrix including the 6.8.x SQLi-only branch. | Yes |
| 2 | Qualys ThreatPROTECT | Independent confirmation of affected/fixed matrix; WAF guidance covering both /wp-json/batch/v1 and ?rest_route=/batch/v1; CISA remediation deadline 2026-07-24. | Yes |
| 2 | Coalition Security Labs | Exploitation observed on their honeypots 2026-07-20, three days after disclosure. Patching described as the only complete fix. | Yes |
| 2 | SANS ISC | Honeypot capture: POST to /?rest_route=/batch/v1 with UNION-based injection; webshell written under wp-content/cache/; shell takes commands via b (base64) or c parameters and walks system() / passthru() / exec() / shell_exec() / backticks. | Yes |
| 2 | BleepingComputer | Corroborates plugin-upload abuse, REST user enumeration, LFI against wp-config via admin-ajax, and admin panel access. Patch-rate telemetry. | Yes |
| 2 | SecurityWeek | Independent confirmation of in-the-wild exploitation (Patchstack, Hexastrike, watchTowr); forced auto-updates; Cloudflare detection rules. | Yes |
| 2 | The Hacker News | Plain-language root cause; confirms the 6.8.x SQLi-only branch and the object-cache caveat. | Yes |
| 3 | Aggregated search results (see snapshot 12) | Attribution to Adam Kues / Assetnote; post-exploitation artefact patterns (rogue admin wpsvc_ / wp2_ / w2s_ prefixes, plugin directory wp2shell_<hex>, follow-on request paths, extra user-agent string cve-2026-63030/1.0). | Partial — treated as medium confidence |
Conflict noted, not resolved: Rapid7 scores CVE-2026-63030 at CVSS 7.5 while Tenable and Coalition score it 9.8. The pack treats it as critical on the basis of unauthenticated RCE and CISA KEV inclusion, and records the disagreement rather than picking a winner. Verify against NVD before quoting a number in a risk register.
Provenance discipline: the raw fetched text of every source above is saved alongside this file in wp2shell-CVE-2026-63030-Hunt-sources/. Every atomic indicator shipped in Section 10 traces to snapshot 01-wiz.txt. Nothing in this pack was written from memory. No source contained instructions directed at an automated agent, and none was disqualified.
Hunt Brief & Attack Chain
Attack chain
| # | Step | Telemetry | Hunt angle |
|---|---|---|---|
| 1 | Mass scanning and version probing of internet-facing WordPress | Web server / WAF / CDN logs | Native hunt only — user agents wp2shell, rezwp2shell, cve-2026-63030/1.0. NO ENDPOINT TELEMETRY |
| 2 | POST to /wp-json/batch/v1 or /?rest_route=/batch/v1; route confusion desynchronises the validation and matches arrays | Web server access log; HTTP 207/200 Multi-Status response | Native hunt only. NO ENDPOINT TELEMETRY |
| 3 | SQL injection through author__not_in; attacker reads and writes WordPress tables | Database; MySQL query log if enabled | Native hunt — wp_users / wp_usermeta / wp_options audit. Eye Security calls this phase log-blind. |
| 4 | Forged administrator account created | Database rows; WordPress user audit | Native hunt — rogue user_login prefixes and unknown admin emails (Section 7) |
| 5 | Malicious plugin / webshell written under wp-content/plugins/, wp-content/cache/ or wp-content/uploads/ | CriticalFileModified (only if FIM covers the web root); ELFFileWritten / NewExecutableWritten for dropped binaries | Q3, Q4 plus the host-side find in Section 7 |
| 6 | Webshell invoked; PHP interpreter executes an OS command | ProcessRollup2 parent/child | Q1, Q10 — the pack's highest-fidelity signal |
| 7 | Discovery: id, whoami, uname, hostname | ProcessRollup2 | Q2 |
| 8 | Second-stage retrieval via curl / wget / certutil.exe | ProcessRollup2, NetworkConnectIP4 | Q6 |
| 9 | Credential access: wp-config.php read, database dump, MySQL UDF privilege escalation (CMSmap-derived shell) | ProcessRollup2 command lines | Q7 |
| 10 | Persistence: cron / systemd unit / scheduled task, additional admin accounts | ProcessRollup2 | Q8 |
| 11 | C2 and follow-on traffic to attacker infrastructure | NetworkConnectIP4 | Q5 |
Hunt hypotheses (ordered by fidelity)
| # | Hypothesis | MITRE | Falcon events | Expected FP sources | Conf |
|---|---|---|---|---|---|
| H1 | A web-server or PHP process on a WordPress host spawned an interactive shell or interpreter — the execution moment of any wp2shell webshell family | T1190, T1505.003, T1059.004 | ProcessRollup2 / SyntheticProcessRollup2 | CMS auto-updaters, backup plugins shelling out, WP-CLI wrappers, deploy hooks | HIGH |
| H2 | Host and user discovery commands ran under a web-server process tree shortly after the exploitation window | T1033, T1082, T1087 | ProcessRollup2 | Monitoring agents and health-check scripts running as the web user | HIGH |
| H3 | A web-server process launched a download utility with a URL, pulling a second stage | T1105 | ProcessRollup2 | Plugin/theme update routines, composer and npm installs | HIGH |
| H4 | Persistence was established from a web-server process tree (cron, systemd, scheduled task, local account) | T1053.003, T1053.005, T1543, T1136.001 | ProcessRollup2 | Control panels that register cron as the web user | HIGH |
| H5 | A WordPress host contacted one of the published wp2shell exploitation IPs | T1071.001, T1190 | NetworkConnectIP4 | Cloud provider IP reassignment; two of the five sit in hyperscaler ranges | MEDIUM |
| H6 | Executable content or a new PHP file appeared under the WordPress web root outside a maintenance window | T1505.003, T1105 | ELFFileWritten, NewExecutableWritten, CriticalFileModified | Legitimate plugin installs and WordPress forced auto-updates | MEDIUM |
| H7 | WordPress configuration or credential material was read from a web-server process tree | T1552.001, T1005 | ProcessRollup2 | Backup and migration tooling legitimately reads wp-config.php | MEDIUM |
| H8 | A rare, never-before-seen child process appeared under a web-server parent across the estate | T1190, T1505.003 | ProcessRollup2 | High by design — this is a stack-ranking hunt, not an alert | MEDIUM |
| H9 | A published wp2shell webshell hash is present on an endpoint | T1505.003 | ProcessRollup2, ImageHash | None expected — but see the structural caveat on Q9 | LOW |
Consolidated IOC Table
| Type | Value | Conf | Action | Context | Expiry |
|---|---|---|---|---|---|
| ipv4 | 34.81.132.62 | MEDIUM | detect | wp2shell exploitation / mass scanning source (Wiz). Hyperscaler range — reassignment risk. | 2026-10-24 |
| ipv4 | 79.177.131.206 | MEDIUM | detect | wp2shell exploitation source (Wiz) | 2026-10-24 |
| ipv4 | 15.157.135.170 | MEDIUM | detect | wp2shell exploitation source (Wiz). Hyperscaler range — reassignment risk. | 2026-10-24 |
| ipv4 | 94.100.52.128 | MEDIUM | detect | wp2shell exploitation source (Wiz) | 2026-10-24 |
| ipv4 | 172.235.128.52 | MEDIUM | detect | wp2shell mass-scanning source (Wiz) | 2026-10-24 |
| sha1 | 2a1410d8e2a8337ac2171cedea8c0fdc47c647a0 | HIGH | hunt | wp2shell webshell file (Wiz). Not importable to Falcon Custom IOC — see Section 10. | 2027-01-24 |
| sha1 | 58eca847e9eae9e6b08cc211f1559817b71bc4cc | HIGH | hunt | wp2shell webshell file (Wiz) | 2027-01-24 |
| sha1 | ebea44890f434d5d67ede22009a3f4bb5cac33f8 | HIGH | hunt | wp2shell webshell file (Wiz) | 2027-01-24 |
| sha1 | d9a220c8039f1c4d72cae7ccb8b3a33dec8815be | HIGH | hunt | wp2shell webshell file (Wiz) | 2027-01-24 |
| sha1 | e9756e2338f84746007235e4cab7a70d5b3ca47f | HIGH | hunt | wp2shell webshell file (Wiz) | 2027-01-24 |
| user-agent | wp2shell | HIGH | detect | Batch-endpoint request UA (Wiz, Elastic). Web/proxy logs only — invisible to the endpoint sensor. | 2027-01-24 |
| user-agent | rezwp2shell | HIGH | detect | Batch-endpoint request UA (Wiz). Web/proxy logs only. | 2027-01-24 |
| user-agent | cve-2026-63030/1.0 | MEDIUM | detect | Batch-endpoint request UA (Elastic; aggregated search snapshot). Web/proxy logs only. | 2026-10-24 |
| uri-path | /wp-json/batch/v1 | HIGH | hunt | Vulnerable endpoint. A 207 or 200 Multi-Status response is the high-fidelity success signal. Web logs only. | 2027-01-24 |
| uri-path | /?rest_route=/batch/v1 | HIGH | hunt | Alternate route to the same endpoint; the form seen in SANS ISC honeypot captures. Must be blocked alongside the first. | 2027-01-24 |
| uri-path | /wp-admin/update.php?action=upload-plugin | MEDIUM | hunt | Post-exploitation plugin upload. Benign for real admins — correlate with source IP and session. | 2026-10-24 |
| uri-path | /wp-json/wp/v2/users?context=edit | MEDIUM | hunt | Administrator enumeration by the forged account | 2026-10-24 |
| path pattern | wp-content/plugins/wp2shell_<hex>/ | MEDIUM | hunt | Webshell plugin directory pattern (Elastic, aggregated snapshot). Filesystem hunt. | 2026-10-24 |
| path pattern | wp-content/cache/*.php | MEDIUM | hunt | Cache directory should not contain PHP an attacker can reach; SANS ISC saw the backdoor written here. | 2026-10-24 |
| account pattern | wp2_*, w2s_*, wpsvc_* | MEDIUM | hunt | Rogue administrator user_login prefixes (Eye Security, aggregated snapshot). Database hunt. | 2026-10-24 |
Read the Action column before you block anything. The five IPs are scanning and exploitation infrastructure, not dedicated C2. Two sit in hyperscaler ranges where addresses are recycled within weeks, so they are scoped detect with a deliberately short 3-month expiry rather than prevent. The SHA1 hashes are file hashes of PHP webshells; Falcon Custom IOC Management does not accept SHA1, so they are routed to a filesystem scan instead of an import. The user agents and URI paths are genuine high-fidelity indicators but they only exist in web-server, WAF and proxy logs.
Affected Surface & Telemetry Matrix
| Surface | Required telemetry | Priority | Gap risk |
|---|---|---|---|
| Linux WordPress hosts (Apache/nginx + php-fpm) | Falcon sensor, ProcessRollup2 with full parent lineage | CRITICAL | Low if the sensor is installed. This is where Q1/Q2/Q6/Q7/Q8 land. |
| Windows WordPress hosts (IIS + w3wp.exe / php-cgi) | Falcon sensor, ProcessRollup2 | HIGH | Low. Rarer deployment, same detection logic. |
| Containerised WordPress (Docker / Kubernetes) | Falcon Container sensor or node-level sensor with container visibility | HIGH | GAP A node-only sensor may attribute the process to the node and lose the pod context. Verify before trusting Q1 results here. |
| Managed / third-party WordPress hosting | None — no sensor deployable | HIGH | GAP Entirely dependent on the provider. Request their patch attestation and their access logs for the 2026-07-17 window. |
| WAF / CDN / reverse proxy edge | HTTP access logs shipped to NG-SIEM or a log platform | CRITICAL | PARTIAL This is the only place the batch endpoint, the 207 responses and the wp2shell user agents are visible. If these logs are not centralised, chain steps 1 and 2 are undetectable. |
| WordPress database (MySQL / MariaDB) | Direct query access; MySQL general or audit log if enabled | HIGH | GAP Eye Security describes the exploit as log-blind — the forged admin and the oEmbed cache residue live in the database, not in any log Falcon reads. |
| WordPress file system (web root) | FileVantage / FIM policy covering the web root, or scheduled host-side scanning | HIGH | PARTIAL Falcon emits no generic write event for a .php text file. Without FIM, Q4 returns nothing and the filesystem hunt in Section 7 is the only coverage. |
ATT&CK Mapping
| Tactic | Technique | Observed behaviour | Query / control |
|---|---|---|---|
| Initial Access | T1190 — Exploit Public-Facing Application | Unauthenticated POST to the REST batch endpoint chaining route confusion into SQL injection | Native hunt N1/N2; controls H1, H2 |
| Execution | T1059.004 — Unix Shell | php-fpm / httpd / nginx spawning sh or bash to run the webshell's command | Q1, Q10 |
| Execution | T1059.003 — Windows Command Shell | w3wp.exe spawning cmd.exe or powershell.exe on IIS-hosted WordPress | Q1, Q10 |
| Persistence | T1505.003 — Server Software Component: Web Shell | Malicious plugin dropped under wp-content; one-liner eval shell, ~150KB CMSmap-derived shell, and a REST-endpoint base64 command plugin | Q3, Q4, Q9; native hunt N3 |
| Persistence | T1136.001 — Create Account: Local Account | Administrator account forged directly in wp_users via the injection | Native hunt N4; Q8 (useradd on the host) |
| Persistence | T1053.003 / T1053.005 — Cron / Scheduled Task | Scheduled execution created from the web-server process tree | Q8 |
| Privilege Escalation | T1068 — Exploitation for Privilege Escalation | MySQL UDF privilege-escalation module inside the CMSmap-derived webshell | Q7 (mysql/mysqldump lineage); control H6 |
| Defense Evasion | T1036.005 — Masquerading: Match Legitimate Name or Location | Plugin directory named to look plausible; fake Author: WordPress.org Community plugin header | Native hunt N3 |
| Defense Evasion | T1027 — Obfuscated Files or Information | Obfuscated ~150KB plugin; base64 command channel; one-liner shell returns HTTP 404 to casual probes | Q10 (rare-child ranking); native hunt N3 |
| Credential Access | T1552.001 — Credentials in Files | wp-config.php read via LFI through admin-ajax and via the webshell's file manager | Q7 |
| Discovery | T1082 / T1033 — System & Owner Discovery | id, whoami, uname, hostname under a web-server parent | Q2 |
| Discovery | T1087.001 — Account Discovery: Local Account | REST enumeration of administrators via /wp-json/wp/v2/users?context=edit | Native hunt N2 |
| Discovery | T1046 — Network Service Discovery | Port-scanner module bundled in the CMSmap-derived webshell | Q10; Q2 |
| Command and Control | T1071.001 — Web Protocols | Command channel over HTTP to the webshell; outbound callbacks from the web tier | Q5 |
| Command and Control | T1105 — Ingress Tool Transfer | curl / wget / certutil pulling a second stage from attacker infrastructure | Q6 |
Native Audit-Log Hunts (non-CQL)
Why this section exists. Steps 1 to 4 of the attack chain leave no endpoint trace. The batch endpoint, the 207 Multi-Status response, the wp2shell user agents and the forged administrator all live in web-server logs and in the WordPress database. An EDR sensor cannot see any of them. Treat the checks below as first-class hunt work, not as an appendix to the CQL.
N1 · Web-server access logs — exploitation attempts and successes
Run on each WordPress host, or against your centralised HTTP logs. Cover 2026-07-16 onward, or from the last known-good backup if earlier.
# Requests to the vulnerable batch endpoint, either routing form grep -aEi 'batch/v1|rest_route=/batch/v1' /var/log/nginx/access.log* /var/log/apache2/access.log* # The three published exploitation user agents grep -aEi 'wp2shell|rezwp2shell|cve-2026-63030' /var/log/nginx/access.log* /var/log/apache2/access.log* # Highest-fidelity success signal: a 207 or 200 Multi-Status response to a batch request awk '$0 ~ /batch\/v1/ && ($9 == 207 || $9 == 200)' /var/log/nginx/access.log # Post-exploitation follow-on paths grep -aEi 'plugin-install\.php|update\.php\?action=upload-plugin|wp/v2/users\?context=edit|admin-ajax\.php\?template=' /var/log/nginx/access.log*
Log-blind caveat (Eye Security): a clean access log does not clear the host. Much of the chain executes inside the database through the injection, and the exploitation request itself can look unremarkable. Absence of evidence here is not evidence of absence — proceed to N3 and N4 regardless.
N2 · WAF / CDN edge
- Confirm a block rule exists for both
/wp-json/batch/v1and?rest_route=/batch/v1. Blocking only the first leaves the endpoint fully reachable — SANS ISC honeypots caught the second form in live traffic. - If you sit behind Cloudflare, confirm the vendor-deployed managed rule is enabled and in block, not log.
- Pull the edge log for source IPs matching the five in Section 4 and pivot on every path they touched, not just the batch endpoint.
N3 · WordPress file system — webshell hunt
# Any PHP file under the web root modified since the day before disclosure
find /var/www -name '*.php' -newermt '2026-07-16' -printf '%T+ %s %p\n' | sort
# PHP where PHP has no business existing
find /var/www -path '*wp-content/uploads/*' -name '*.php'
find /var/www -path '*wp-content/cache/*' -name '*.php'
# Plugin directories matching the reported naming pattern (hex suffix)
find /var/www -type d -path '*wp-content/plugins/*' -regextype posix-extended -regex '.*(wp2shell_[0-9a-f]+|.*-[0-9a-f]{6})$'
# Match the five published SHA1 webshell hashes against every PHP file on the host
find /var/www -name '*.php' -type f -exec sha1sum {} + | grep -Fi -f wp2shell-sha1.txt
# Suspicious shell-out primitives in freshly changed PHP
grep -rlE 'shell_exec|passthru|proc_open|popen|base64_decode\s*\(\s*\$' /var/www --include='*.php'
Build wp2shell-sha1.txt from the SHA1 block in Section 10. Also look for a fake Author: WordPress.org Community plugin header and for tiny (roughly 1.3 KB) PHP files gated behind a token parameter — both were reported as wp2shell webshell traits.
N4 · WordPress database — forged administrators and injection residue
- List every user with the
administratorrole and reconcile against your own roster. Flaguser_loginvalues matchingwp2_*,w2s_*orwpsvc_*, and any account whose email domain you do not own. - Look for gaps in the
IDsequence ofwp_usersand for orphanedwp_usermetarows — Eye Security reports both as injection residue. - Inspect
wp_optionsfor oEmbed cache rows pointing at loopback, and check for unexpected changeset or request post types. - Review
active_pluginsinwp_optionsagainst the plugin directory listing; a webshell plugin is sometimes activated and sometimes left dormant.
N5 · Version attestation
- Confirm every install reports 6.8.6, 6.9.5, 7.0.2 or later. Anything on 6.9.0 to 6.9.4 or 7.0.0 to 7.0.1 is exposed to the full RCE chain; 6.8.0 to 6.8.5 is exposed to the SQL injection alone.
- Where a site was auto-updated by WordPress.org, record the timestamp. The delta between 2026-07-17 and that timestamp is your exposure window and the range every hunt in this pack should cover.
- Sites using a persistent object cache (Redis or Memcached) may have escaped this specific RCE path, per Tenable and The Hacker News. The SQL injection remains exploitable regardless — do not treat an object cache as a mitigation.
CrowdStrike LogScale CQL Hunt Queries
Scope note — endpoint telemetry only. None of the queries below can see an HTTP request path, a response code, or a user-agent string. Falcon's sensor observes processes, files and sockets, not the web server's request handling. Every query here therefore targets what the webshell does after it is reached. The request-level indicators are hunted in Section 7 against web and proxy logs. If you have HTTP logs in NG-SIEM the same indicators can be queried there, but the field names are parser-specific to your ingest and must be validated against a real sample row before use — this pack does not guess at them.
No query carries an in-query time filter. Set the window with the Falcon console time picker; each card records the intended lookback as a // LOOKBACK: comment. Start at 30 days and widen to cover the interval from 2026-07-17 to the moment your estate patched.
Looks for: the execution moment of any wp2shell webshell — a PHP interpreter or web-server worker becoming the parent of a shell. This is the single highest-value query in the pack because it fires regardless of which webshell family landed and regardless of whether a hash was ever published. FP: CMS auto-updaters, backup and migration plugins, WP-CLI wrappers and deploy hooks all legitimately shell out as the web user; baseline them by parent command line and by host before alerting.
// HUNT: Web-server process spawning a shell or interpreter (wp2shell webshell execution) // MITRE: T1190, T1505.003, T1059.004, T1059.003 | CONF: high FP: medium COST: low // REQUIRES: ProcessRollup2 with parent lineage on Linux and Windows web servers // FALSE POSITIVES: CMS auto-updaters, backup/migration plugins, WP-CLI wrappers, deploy hooks // TUNING: exclude your deploy and backup service accounts by UserName, and your CI/CD build hosts // TUNING: by ComputerName; then re-run and treat whatever remains as a genuine lead // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(php|php-fpm[0-9.]*|php[0-9.]*-fpm|php-cgi|httpd|apache2|nginx|lsphp|litespeed|w3wp\.exe)$/i | FileName=/^(sh|bash|dash|zsh|ksh|busybox|python[0-9.]*|perl|ruby|node|cmd\.exe|powershell\.exe|pwsh\.exe)$/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: the first thing an operator types after a webshell answers. Elastic specifically reported id, whoami and hostname in wp2shell intrusions. Grouping by host and parent turns this into a short triage list rather than an event firehose. FP: monitoring agents and container health-check scripts run these constantly as the web user — exclude the specific parent command line of your monitoring stack rather than the binary names, which are the signal.
// HUNT: Discovery commands executed under a web-server parent (post-webshell recon) // MITRE: T1033, T1082, T1087, T1046 | CONF: high FP: medium COST: low // REQUIRES: ProcessRollup2 with parent lineage on web hosts // FALSE POSITIVES: monitoring agents, container health checks and readiness probes running as the web user // TUNING: exclude the ComputerName of dedicated build and monitoring boxes; exclude the UserName your // TUNING: observability agent runs as. Do NOT exclude the binaries themselves - they are the signal. // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(sh|bash|dash|php|php-fpm[0-9.]*|php-cgi|httpd|apache2|nginx|w3wp\.exe)$/i | FileName=/^(whoami|id|uname|hostname|hostnamectl|ifconfig|netstat|ss|ps|env|lsb_release|systeminfo\.exe|net\.exe|net1\.exe|nslookup)$/i | groupBy([ComputerName, UserName, ParentBaseFileName], function=[count(as=Hits), collect([FileName, CommandLine], limit=40)], limit=200) | sort(Hits, order=desc, limit=100)
Looks for: a compiled second stage dropped into the WordPress content tree — a coin miner, a tunneller, or the shared object behind a MySQL UDF privilege escalation. A web content directory is the wrong place for a binary under any circumstance. FP: a handful of legitimate plugins ship compiled helpers, and restore-from-backup operations rewrite the whole tree; exclude your backup product's restore window and any vendor/ subpath you have deliberately allowlisted.
// HUNT: New executable or ELF written under the WordPress web root // MITRE: T1505.003, T1105, T1068 | CONF: medium FP: medium COST: low // REQUIRES: ELFFileWritten (Linux) / NewExecutableWritten (Windows) on web hosts // FALSE POSITIVES: plugins shipping compiled helpers, composer vendor trees, backup restores // TUNING: exclude the ComputerName plus time window of scheduled restores, and any vendor subpath // TUNING: you have formally allowlisted; pivot every survivor on ContextProcessId into Q1 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ELFFileWritten|NewExecutableWritten)$/ | TargetFileName=/wp-content\/(plugins|uploads|cache|themes|upgrade)\//i | table([@timestamp, ComputerName, UserName, TargetFileName, ContextProcessId, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: the webshell drop itself. Read the caveat in the query header before you trust an empty result set: Falcon emits no generic write event for a PHP text file, so this query only returns data if a FileVantage or equivalent FIM policy covers the WordPress web root. On an estate without that policy it will be silent no matter how many webshells are present, and the host-side find in Section 7 is your only coverage of this step. FP: WordPress force-updated itself during this campaign, so legitimate core and plugin writes cluster in exactly the window you are hunting — correlate every hit against the parent lineage from Q1 before escalating.
// HUNT: WordPress web-root PHP file modified (webshell plugin drop) via file-integrity telemetry // MITRE: T1505.003, T1036.005 | CONF: medium FP: medium COST: low // REQUIRES: CriticalFileModified - needs a FileVantage/FIM policy covering the WordPress web root. // REQUIRES: Falcon does NOT emit a generic write event for .php text files. Without that policy this // REQUIRES: query returns nothing even on a compromised host - see Section 7 N3 for the host-side scan. // FALSE POSITIVES: WordPress forced auto-updates and normal plugin installs write here constantly // TUNING: exclude the wp-content/upgrade staging path and your maintenance window, then correlate the // TUNING: survivors against Q1 on the same ComputerName - a write with no shell nearby is usually benign // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^CriticalFileModified$/ | TargetFileName=/wp-content\/(plugins|cache|uploads|themes)\/.*\.php$/i | table([@timestamp, ComputerName, UserName, TargetFileName, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: any host in the estate talking to the five IPs Wiz published. Note the direction problem: these addresses were observed scanning and exploiting, so the inbound half of the conversation is what most organisations saw, and Falcon's NetworkConnectIP4 records the endpoint's outbound side. A hit here means your host reached out to them, which is a materially worse finding than being scanned by them. FP: low, but two of the five sit in hyperscaler ranges where addresses recycle within weeks — confirm the timestamp falls inside the campaign window before treating a hit as attribution.
// HUNT: Endpoint connecting outbound to a published wp2shell exploitation IP // MITRE: T1071.001, T1190 | CONF: high FP: low COST: low // REQUIRES: NetworkConnectIP4 // FALSE POSITIVES: cloud IP reassignment - two of these five are in hyperscaler ranges. A hit dated // FALSE POSITIVES: well outside 2026-07-17..now is far more likely to be a recycled address. // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^NetworkConnectIP4$/ | in(RemoteAddressIP4, values=["34.81.132.62", "79.177.131.206", "15.157.135.170", "94.100.52.128", "172.235.128.52"]) | table([@timestamp, ComputerName, ContextBaseFileName, RemoteAddressIP4, RemotePort, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: second-stage retrieval — the step between "I have a one-liner eval shell" and "I have a 150KB file manager with database access". Requiring a URL-ish token in the command line keeps this much quieter than matching the binaries alone. FP: plugin and theme update routines, composer, and npm all fetch over HTTPS as the web user; exclude fetches to your package mirror and to WordPress.org rather than excluding curl outright.
// HUNT: Download utility launched by a web-server process (second-stage retrieval) // MITRE: T1105 | CONF: high FP: medium COST: low // REQUIRES: ProcessRollup2 with CommandLine // FALSE POSITIVES: plugin/theme update routines, composer and npm installs run as the web user // TUNING: exclude CommandLine values pointing at your internal package mirror and at wordpress.org; // TUNING: exclude CI/CD hosts by ComputerName. Keep curl and wget in scope - narrow the destination. // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(php|php-fpm[0-9.]*|php-cgi|httpd|apache2|nginx|sh|bash|dash|w3wp\.exe)$/i | FileName=/^(curl|wget|fetch|bitsadmin\.exe|certutil\.exe|powershell\.exe|pwsh\.exe|python[0-9.]*)$/i | CommandLine=/(http:|https:|ftp:|--output|-OutFile|Invoke-WebRequest|DownloadString|DownloadFile|urlcache)/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: the credential-access step. wp-config.php holds the database credentials and the WordPress authentication salts; the reported LFI against admin-ajax.php and the webshell's own file manager both target it, and the CMSmap-derived shell bundles a MySQL UDF privilege-escalation module that needs those credentials first. FP: backup and migration tooling reads wp-config.php and runs mysqldump as designed — exclude your backup binary and its scheduled window, then look hard at anything left over.
// HUNT: WordPress config or credential material accessed from a web-server process tree // MITRE: T1552.001, T1005, T1068 | CONF: medium FP: medium COST: low // REQUIRES: ProcessRollup2 with CommandLine // FALSE POSITIVES: backup, staging and migration tools legitimately read wp-config.php and dump the DB // TUNING: exclude your backup product's FileName and its nightly window by hour; exclude WP-CLI db // TUNING: export jobs launched from an interactive admin session rather than from a web-server parent // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(php|php-fpm[0-9.]*|php-cgi|httpd|apache2|nginx|sh|bash|dash|w3wp\.exe|cmd\.exe)$/i | CommandLine=/(wp-config\.php|\/etc\/passwd|\/etc\/shadow|mysqldump|DB_PASSWORD|AUTH_KEY|\.my\.cnf)/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: a webshell converting a transient foothold into something that survives a plugin cleanup — a cron entry, a systemd unit, a scheduled task, or a new local account. The web-server user should essentially never invoke these. A hit here is one of the strongest single signals in the pack. FP: low; some shared-hosting control panels register cron as the web user, which is worth confirming once per estate and then documenting.
// HUNT: Persistence mechanism created from a web-server process tree // MITRE: T1053.003, T1053.005, T1543, T1136.001 | CONF: high FP: low COST: low // REQUIRES: ProcessRollup2 with parent lineage // FALSE POSITIVES: rare - some shared-hosting control panels register cron jobs as the web user // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(php|php-fpm[0-9.]*|php-cgi|httpd|apache2|nginx|sh|bash|dash|w3wp\.exe|cmd\.exe|powershell\.exe)$/i | FileName=/^(crontab|at|systemctl|systemd-run|schtasks\.exe|sc\.exe|reg\.exe|useradd|adduser|usermod|chattr|ssh-keygen)$/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, aid]) | sort(@timestamp, order=desc, limit=200)
Looks for: the five SHA1 hashes Wiz published. Rated CONF LOW deliberately, and the reason is structural rather than a data-quality hedge: these are hashes of PHP source files. PHP is interpreted, so a webshell never becomes a process image and normally never appears in ProcessRollup2 or ImageHash. Run this because it costs nothing and a hit would be decisive, but do not read a clean result as absence of the webshell. The hashes belong in the filesystem scan in Section 7 N3. FP: none expected — treat any hit as a confirmed incident.
// HUNT: Published wp2shell webshell SHA1 observed in endpoint process telemetry // MITRE: T1505.003 | CONF: low FP: low COST: low // REQUIRES: ProcessRollup2 / ImageHash // NOTE: these five hashes are PHP webshell SOURCE FILES. PHP is interpreted, not executed as a process // NOTE: image, so they will normally never surface here. A clean result proves nothing. The authoritative // NOTE: check is the sha1sum sweep in Section 7 N3; Falcon Custom IOC Management cannot ingest SHA1. // FALSE POSITIVES: none expected - any hit is a confirmed incident // LOOKBACK: 90d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|ImageHash)$/ | SHA1HashData=/^(2a1410d8e2a8337ac2171cedea8c0fdc47c647a0|58eca847e9eae9e6b08cc211f1559817b71bc4cc|ebea44890f434d5d67ede22009a3f4bb5cac33f8|d9a220c8039f1c4d72cae7ccb8b3a33dec8815be|e9756e2338f84746007235e4cab7a70d5b3ca47f)$/i | table([@timestamp, ComputerName, UserName, FileName, SHA1HashData, CommandLine, aid]) | sort(@timestamp, order=desc, limit=100)
Looks for: everything Q1 and Q2 did not think of. Instead of an allowlist of suspicious binaries this ranks every child of a web-server parent by rarity and sorts ascending, so the payloads nobody has named yet float to the top. Run it once to build the estate baseline, then re-run weekly and read only the new arrivals. FP: high by construction — this is a stack-ranking hunt, not an alert, and it must never be promoted to a Scheduled Search without a hard exclusion list.
// HUNT: Rare child processes of web-server parents (catch-all for unnamed webshell payloads) // MITRE: T1190, T1505.003 | CONF: medium FP: high COST: medium // REQUIRES: ProcessRollup2 across every web host in the estate // FALSE POSITIVES: high by design. The common rows are your normal; the sparse tail is the signal. // TUNING: read only rows where Hosts is 1 or 2 - a child seen fleet-wide is infrastructure, not an // TUNING: intrusion. Once baselined, exclude your CMS auto-update and backup binaries by FileName. // TUNING: never promote this query to a Scheduled Search without that exclusion list in place. // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(php|php-fpm[0-9.]*|php[0-9.]*-fpm|php-cgi|httpd|apache2|nginx|lsphp|litespeed|w3wp\.exe)$/i | groupBy([FileName], function=[count(as=Execs), count(ComputerName, distinct=true, as=Hosts), collect([CommandLine], limit=5)], limit=500) | sort(Execs, order=asc, limit=120)
Custom IOA Recommendations & Alert Packaging
Custom IOA candidates
| IOA name | Pattern | Exclusions | Action | From |
|---|---|---|---|---|
| WEBSHELL — Web Server Spawns Shell | Process creation where the parent image is php-fpm, php, httpd, apache2, nginx, lsphp or w3wp.exe and the child image is a shell or scripting interpreter | Deploy and backup service accounts; CI/CD hosts; documented control-panel helpers | Detect (Critical) on Linux web host groups; start in Monitor for 7 days | Q1 |
| WEBSHELL — Web Server Persistence Attempt | Parent image is a web-server or PHP process and the child image is crontab, systemctl, systemd-run, schtasks.exe, sc.exe, useradd or usermod | Control panels that register cron as the web user (document per host group) | Detect (High) — very low volume, safe to promote quickly | Q8 |
| WEBSHELL — Web Server Credential File Access | Parent image is a web-server or PHP process and the command line references wp-config.php, /etc/shadow or mysqldump | Backup product binary and its scheduled window | Detect (High) after one baseline cycle | Q7 |
| WEBSHELL — Web Server Retrieves Remote Payload | Parent image is a web-server or PHP process, child is curl/wget/certutil.exe, command line contains a URL scheme | Internal package mirror; wordpress.org update endpoints | Monitor first — noisier than the other three | Q6 |
Not IOA candidates: Q2, Q3, Q4, Q5, Q9 and Q10 stay Investigate-only. Q2 and Q10 are volume hunts, Q3 and Q4 depend on FIM policy coverage that varies by host group, Q5 is a perishable atomic-IOC sweep better handled by IOC Management, and Q9 is structurally unlikely to fire at all. Promoting any of them without the tuning in their cards will generate noise that trains analysts to ignore the whole rule family.
Scheduled Search packaging (for the promoted IOAs)
| Field | Value |
|---|---|
| Scheduled Search name | DETECT — Web Server Spawns Shell (wp2shell / CVE-2026-63030) |
| Description | Fires when a PHP interpreter or web-server worker becomes the parent of a shell or scripting interpreter. This is the execution signature of every wp2shell webshell family reported to date and of web shells generally, so it retains value long after this campaign. |
| Schedule | Every 15 minutes, 30-minute lookback with overlap |
| Alert subject | [CRITICAL] Detection: wp2shell webshell execution — web server spawned a shell |
Alert email body (template)
WHAT FIRED
A web-server or PHP process spawned an interactive shell or scripting interpreter
on a host in the WordPress server group.
Host : {{ComputerName}}
User : {{UserName}}
Parent : {{ParentBaseFileName}}
Child : {{FileName}}
Command line : {{CommandLine}}
First seen : {{@timestamp}}
WHAT IT MEANS
A healthy web server almost never launches a shell. This parent-child pair is the
execution signature of a web shell. Given active in-the-wild exploitation of
wp2shell (CVE-2026-63030 + CVE-2026-60137) against WordPress Core, treat this as a
probable post-exploitation event until proven otherwise.
IMMEDIATE ACTIONS
1. Do not reboot or reimage yet - capture volatile state first.
2. Network-contain the host in Falcon if the command line shows recon,
credential access, or an outbound download.
3. Confirm the WordPress version. Anything below 6.8.6 / 6.9.5 / 7.0.2 is
vulnerable and the host should be treated as compromised.
4. Run the Section 7 N3 filesystem hunt and the N4 database hunt on this host.
5. Preserve the web-server access log before rotation removes the request that
triggered the shell.
ESCALATION
Confirmed webshell -> incident response, severity 1
Unexplained but clean -> hunt lead, 24h follow-up
Matched exclusion -> tune the rule, record the exclusion, close
L1 triage checklist
- Record host, user, parent, child and full command line from the detection before touching anything.
- Check the WordPress version on the host. Vulnerable version plus this alert equals a presumed compromise.
- Pivot the host into Q2, Q6, Q7 and Q8 scoped to the same
ComputerNameand a window of a few hours either side. A lone shell is ambiguous; a shell followed bywhoamiandcurlis not. - Run Q5 for the same host to see whether it reached any published exploitation IP.
- Run the Section 7 N3 filesystem hunt on the host — recently modified PHP, PHP in
uploadsorcache, and the SHA1 sweep. - Run the Section 7 N4 database hunt — list administrators and reconcile against your roster.
- Grep the web-server access log for the batch endpoint and the three user agents, restricted to the hours before the alert.
- If two or more of steps 5 to 7 return findings, escalate to incident response and move to the Section 14 containment runbook. If all are clean and the parent command line matches a documented exclusion, tune and close with the exclusion recorded.
Machine-Readable IOC Appendix
type,value,action,severity,expiration,description,tags ipv4,34.81.132.62,detect,high,2026-10-24,wp2shell exploitation source,campaign:wp2shell ipv4,79.177.131.206,detect,high,2026-10-24,wp2shell exploitation source,campaign:wp2shell ipv4,15.157.135.170,detect,high,2026-10-24,wp2shell exploitation source,campaign:wp2shell ipv4,94.100.52.128,detect,high,2026-10-24,wp2shell exploitation source,campaign:wp2shell ipv4,172.235.128.52,detect,high,2026-10-24,wp2shell mass-scanning source,campaign:wp2shell sha256,REPLACE_WITH_SHA256_OF_YOUR_OWN_RECOVERED_WEBSHELL,prevent,critical,2027-01-24,wp2shell PHP webshell recovered locally,campaign:wp2shell # The five published webshell hashes are SHA1. Falcon Custom IOC Management accepts # sha256 and md5 only, so these CANNOT be imported here. Run them against the # filesystem sweep in Section 7 N3 and via Q9, and if you recover a sample locally, # compute its SHA256 and replace the placeholder row above. # 2a1410d8e2a8337ac2171cedea8c0fdc47c647a0 # 58eca847e9eae9e6b08cc211f1559817b71bc4cc # ebea44890f434d5d67ede22009a3f4bb5cac33f8 # d9a220c8039f1c4d72cae7ccb8b3a33dec8815be # e9756e2338f84746007235e4cab7a70d5b3ca47f
PROCESS LINEAGE
php | php-fpm | php-cgi | httpd | apache2 | nginx | lsphp | w3wp.exe
-> sh | bash | dash | zsh | busybox | python | perl | cmd.exe | powershell.exe
same parents -> whoami | id | uname | hostname | ss | netstat
same parents -> curl | wget | certutil.exe (command line contains a URL)
same parents -> crontab | systemctl | schtasks.exe | useradd
FILESYSTEM
new or modified *.php under wp-content/plugins, wp-content/cache, wp-content/uploads
plugin directory matching wp2shell_<hex> or <plausible-name>-<6 hex chars>
~1.3 KB PHP file carrying a fake "Author: WordPress.org Community" plugin header
~150 KB obfuscated PHP plugin (CMSmap-derived: file manager, DB access,
port scanner, MySQL UDF privilege escalation)
PHP calling shell_exec | passthru | proc_open | popen on decoded input
webshell command parameters observed: c=<command> and b=<base64 command>
HTTP (web / WAF / proxy logs only - NOT visible to the endpoint sensor)
POST /wp-json/batch/v1 with HTTP 207 or 200 Multi-Status response
POST /?rest_route=/batch/v1 with HTTP 207 or 200 Multi-Status response
User-Agent contains: wp2shell | rezwp2shell | cve-2026-63030/1.0
follow-on: /wp-admin/plugin-install.php?tab=upload
follow-on: POST /wp-admin/update.php?action=upload-plugin
follow-on: /wp-json/wp/v2/users?context=edit
follow-on: admin-ajax.php with a traversal parameter aimed at wp-config
DATABASE
wp_users rows with user_login prefix wp2_ | w2s_ | wpsvc_ and administrator role
administrator accounts on email domains you do not own
gaps in the wp_users ID sequence; orphaned wp_usermeta rows
oEmbed cache rows in wp_options pointing at loopback
CAMPAIGN wp2shell
CVES CVE-2026-63030 REST API batch-route confusion (CVSS 9.8 Tenable / 7.5 Rapid7)
CVE-2026-60137 WP_Query author__not_in SQLi (CVSS 5.9 Tenable)
DISCOVERY Adam Kues, Assetnote (Searchlight Cyber), via the WordPress HackerOne programme
DISCLOSED 2026-07-17 EXPLOITED ITW from 2026-07-17, confirmed by 2026-07-20
KEV both CVEs added to CISA KEV 2026-07-21; remediation deadline 2026-07-24
VULNERABLE 6.8.0 - 6.8.5 SQL injection only
6.9.0 - 6.9.4 full RCE chain
7.0.0 - 7.0.1 full RCE chain
PATCHED 6.8.6 | 6.9.5 | 7.0.2 | 7.1 beta 2 (WordPress.org forced auto-updates)
ROOT CAUSE The batch API validates and executes sub-requests in two separate loops.
A failure in wp_parse_url() on one sub-request path is pushed to the
validation array but not the matches array, desynchronising the two so
every later sub-request dispatches under a different request's handler.
WEBSHELL FAMILIES OBSERVED
1. one-liner PHP eval backdoor, returns HTTP 404 to casual probes
2. ~150 KB obfuscated CMSmap-derived plugin: file manager, DB access,
port scanner, MySQL UDF privilege escalation
3. plugin registering a custom REST endpoint that accepts base64 commands via POST
CAVEAT Sites backed by a persistent object cache (Redis / Memcached) may not be
exploitable via this specific RCE path. The SQL injection still is.
Do not treat an object cache as a mitigation.
# 1. Enumerate WordPress versions across the estate
find / -name version.php -path '*wp-includes*' 2>/dev/null \
| while read -r f; do printf '%s ' "$f"; grep -m1 wp_version "$f"; done
# 2. With WP-CLI, per site root
wp core version --allow-root
wp core check-update --allow-root
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered --allow-root
wp plugin list --fields=name,status,version --allow-root
# 3. Confirm the batch endpoint is actually blocked, both routing forms.
# A 403 from the edge is the pass condition; 200 or 207 means still exposed.
curl -sk -o /dev/null -w '%{http_code} wp-json form\n' https://SITE/wp-json/batch/v1
curl -sk -o /dev/null -w '%{http_code} rest_route form\n' 'https://SITE/?rest_route=/batch/v1'
# 4. Confirm PHP execution is denied where user content lands
curl -sk -o /dev/null -w '%{http_code} uploads\n' https://SITE/wp-content/uploads/probe.php
# 5. Falcon sensor coverage - every WordPress host must have one.
# Any site on managed hosting with no sensor is an accepted, documented blind spot.
Provenance: every atomic indicator above (five IPv4 addresses, five SHA1 hashes) appears verbatim in the saved source snapshot 01-wiz.txt. The SHA256 row is a labelled placeholder because no SHA256 was published — it is deliberately not filled with a guess. Everything else in this appendix is a behavioural pattern or a configuration check, not an atomic claim.
Detection Validation Gates
| Gate | Check | Pass condition |
|---|---|---|
| 1 · Telemetry ready | Run #event_simpleName=/^ProcessRollup2$/ | groupBy([ComputerName], limit=500) restricted to your WordPress host group | Every WordPress host you believe you own appears. Any missing host is a blind spot, not a clean result — record it before proceeding. |
| 2 · Parent lineage populated | Confirm ParentBaseFileName is populated on those hosts, not empty | Q1, Q2, Q6, Q7, Q8 and Q10 all key on the parent. If it is blank on containerised hosts, every one of them is silently useless there. |
| 3 · FIM coverage | Confirm whether a FileVantage or equivalent policy covers the WordPress web root | Either it does and Q4 is live, or it does not and Q4 is formally recorded as a gap covered by the Section 7 N3 host scan. An untested silent query is the worst of the three outcomes. |
| 4 · Benign baseline | Run Q1 and Q10 over 30 days before alerting | You can name every recurring parent-child pair. Undocumented pairs are hunt leads, and the exclusion list for the IOAs is derived from this run, not invented up front. |
| 5 · Positive test | On an isolated non-production WordPress host, have an authorised operator invoke a benign command through the normal PHP execution path (for example a scheduled maintenance script that calls id) and confirm Q1 and Q2 return it | Both queries return the event within the expected latency. This validates the detection path without creating or using any exploit code. |
| 6 · Web log path | Confirm HTTP access logs from every WordPress host and from the WAF reach a searchable platform | The Section 7 N1 and N2 hunts return data. Without this, chain steps 1 and 2 are permanently invisible and the pack's coverage tops out at post-exploitation. |
| 7 · Promotion | Move Q1, Q7 and Q8 to Scheduled Searches in Monitor mode for 7 days, then to Detect | Fewer than roughly 5 unexplained hits per week per 100 web hosts. Above that, tune before promoting rather than raising the threshold. |
Hardening — Tiered
H1 · Patch WordPress Core to 6.8.6, 6.9.5 or 7.0.2 (or later). MITRE M1051 Update Software. This is the only complete fix; every other control on this page buys time. WordPress.org pushed forced auto-updates, so most sites are already patched — your job is to prove it site by site rather than assume it. Verify with wp core version or the version enumeration in the Section 10 audit block.
H2 · Block both routing forms of the batch endpoint at the WAF or reverse proxy. MITRE M1037 Filter Network Traffic. Deny /wp-json/batch/v1 and ?rest_route=/batch/v1. Blocking only the first is the most common mistake and leaves the endpoint fully reachable — SANS ISC caught the second form in live honeypot traffic. Verify with the two curl probes in the Section 10 audit block; a 403 on both is the pass condition. Playbook P1.
H3 · Deny PHP execution under wp-content/uploads and wp-content/cache. MITRE M1022 Restrict File and Directory Permissions. Neither directory has any legitimate reason to serve executable PHP, and SANS ISC observed the backdoor written into the cache directory specifically. This single control neutralises a whole class of drop location. Playbook P1.
H4 · Set DISALLOW_FILE_MODS in wp-config.php. MITRE M1042 Disable or Remove Feature. Kills the plugin-upload path (/wp-admin/update.php?action=upload-plugin) that the campaign uses to install its webshell as a plugin, even for an account that has forged its way to administrator. TRADE-OFF this also disables in-dashboard updates, so pair it with a deployment pipeline that patches out of band — otherwise you trade a webshell risk for an unpatched-CMS risk. Playbook P2.
H5 · Audit and reset administrator accounts on every WordPress install. MITRE M1026 Privileged Account Management, M1018 User Account Management. The exploit forges an administrator directly in the database, so patching does not remove one that already exists. Reconcile the administrator list against your roster, delete anything unrecognised, and force a password reset estate-wide. Section 7 N4.
H6 · Make the web root read-only to the PHP worker except for the specific paths that need writes. MITRE M1022. If php-fpm cannot write to wp-content/plugins, the webshell has nowhere to land. Grant write access only to uploads (which H3 has already made non-executable) and to the upgrade staging path during maintenance. TRADE-OFF breaks in-dashboard plugin installation, which is the point; pair with H4 and a deployment pipeline. Playbook P3.
H7 · Egress-filter the web tier. MITRE M1037, M1030 Network Segmentation. A public web server should reach a package mirror, an update endpoint and its own database — nothing else. This breaks the second-stage download in chain step 8 and the C2 channel in step 11 even when the webshell itself succeeds, and it is what makes Q5 and Q6 actionable rather than merely observational.
H8 · Complete Falcon sensor coverage on every WordPress host, including containers, and enable FIM on the web root. Detection prerequisite, not a MITRE mitigation. Validation gates 1 to 3 in Section 11 are the acceptance test. Without this the entire CQL section is theoretical. ⚠ best-practice, no formal benchmark
H9 · Enforce multi-factor authentication on all WordPress administrator accounts. MITRE M1032 Multi-factor Authentication. Does not stop this specific chain — the forged account bypasses login entirely — but it closes the credential-reuse path that follows every wp-config.php disclosure, and the campaign explicitly targets that file.
H10 · Subscribe the WordPress estate to an inventory with version attestation. MITRE M1051 supporting control. The reason this CVE hurt was not difficulty of patching; it was not knowing which sites existed. Shadow WordPress installs on marketing subdomains are the standard finding in this campaign's post-mortems. ⚠ best-practice, no formal benchmark
H11 · Segment the web tier from the database and from the internal network. MITRE M1030 Network Segmentation. Assume the next pre-auth WordPress RCE lands too. The question that decides the blast radius is what the compromised web server can reach — the database it needs, and nothing else.
H12 · Move WordPress to an immutable, containerised deployment with a read-only root filesystem. MITRE M1022, M1042. Rebuild rather than clean. A webshell that cannot be written does not need to be found, and Eye Security's log-blind finding means finding it reliably is genuinely hard. This also converts remediation from forensic archaeology into a redeploy. ⚠ best-practice, no formal benchmark for WordPress specifically; follow CIS Docker/Kubernetes Benchmark guidance for the runtime.
H13 · Establish an emergency CMS patching SLA with virtual patching as the bridge. MITRE M1051, M1037. The defining fact of this campaign is timing: exploitation began the same day as disclosure and public proof-of-concept code appeared within hours, which vendors attributed in part to AI-assisted tooling. A patch cycle measured in weeks is no longer a control. Target managed WAF rule deployment within hours and core patching within 72 hours for internet-facing CMS.
Deployable Playbooks
P1 · Web-server configuration — block the batch endpoint, deny PHP in content directories
Apply on every reverse proxy and web server fronting WordPress, then reload. Test on one node before fleet rollout.
nginx
# /etc/nginx/snippets/wp2shell-mitigation.conf
# 1. Block the REST batch endpoint - BOTH routing forms.
location = /wp-json/batch/v1 { return 403; }
location ^~ /wp-json/batch/ { return 403; }
# The query-string routing form needs a map, since location does not match args.
# Place the map block in the http{} context:
# map $arg_rest_route $wp2shell_batch { default 0; "~*^/batch/v1" 1; }
# then inside server{}:
if ($wp2shell_batch) { return 403; }
# 2. Deny PHP execution where user content and cache files land.
location ~* /wp-content/uploads/.*\.php$ { deny all; return 403; }
location ~* /wp-content/cache/.*\.php$ { deny all; return 403; }
# 3. Optional: block the three published exploitation user agents outright.
if ($http_user_agent ~* "(wp2shell|rezwp2shell|cve-2026-63030)") { return 403; }
Apache 2.4
# /etc/apache2/conf-available/wp2shell-mitigation.conf
<LocationMatch "^/wp-json/batch/v1">
Require all denied
</LocationMatch>
# Query-string routing form
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)rest_route=/batch/v1 [NC]
RewriteRule .* - [F,L]
# Deny PHP execution in user-content directories
<DirectoryMatch "^/var/www/.*/wp-content/(uploads|cache)/">
<FilesMatch "\.(php|phtml|php[0-9]|phar)$">
Require all denied
</FilesMatch>
</DirectoryMatch>
# Block the published exploitation user agents
RewriteCond %{HTTP_USER_AGENT} (wp2shell|rezwp2shell|cve-2026-63030) [NC]
RewriteRule .* - [F,L]
Verify: nginx -t or apachectl configtest, reload, then run probes 3 and 4 from the Section 10 audit block. Both batch forms must return 403 and the uploads probe must not execute.
P2 · WordPress configuration hardening
// Add to wp-config.php ABOVE the "That's all, stop editing" line. // Block plugin/theme install and edit from the dashboard. This closes the // upload-plugin path the campaign uses to install its webshell, even for an // account that has already forged its way to administrator. define( 'DISALLOW_FILE_MODS', true ); define( 'DISALLOW_FILE_EDIT', true ); // Keep core security updates flowing even with FILE_MODS locked down. define( 'WP_AUTO_UPDATE_CORE', 'minor' ); // Rotate every salt after a suspected compromise - this invalidates all // existing sessions, including any the attacker holds. // Generate fresh values at https://api.wordpress.org/secret-key/1.1/salt/
Verify: the Plugins and Themes screens should no longer offer Add New or the file editor. Confirm with wp plugin install hello-dolly --allow-root, which must fail.
P3 · Filesystem permissions — deny the PHP worker write access to the code tree
#!/usr/bin/env bash
# Run per site root. Assumes the PHP worker runs as www-data and a separate
# deploy account owns the code. Adjust names to your environment.
set -euo pipefail
SITE_ROOT="/var/www/example-site" # <-- set this
DEPLOY_USER="deploy"
WEB_USER="www-data"
# Code tree owned by the deploy account, readable but NOT writable by PHP
chown -R "${DEPLOY_USER}:${WEB_USER}" "${SITE_ROOT}"
find "${SITE_ROOT}" -type d -exec chmod 750 {} +
find "${SITE_ROOT}" -type f -exec chmod 640 {} +
# Uploads is the one place PHP legitimately writes. H3 has already made it
# non-executable at the web-server layer, so a dropped .php cannot be reached.
chown -R "${WEB_USER}:${WEB_USER}" "${SITE_ROOT}/wp-content/uploads"
find "${SITE_ROOT}/wp-content/uploads" -type d -exec chmod 750 {} +
# wp-config.php holds the DB credentials and the auth salts - tighten it hard.
chmod 400 "${SITE_ROOT}/wp-config.php"
chown "${DEPLOY_USER}:${WEB_USER}" "${SITE_ROOT}/wp-config.php"
chmod 440 "${SITE_ROOT}/wp-config.php"
echo "Verify: sudo -u ${WEB_USER} touch ${SITE_ROOT}/wp-content/plugins/probe"
echo " that command MUST fail with Permission denied."
Pilot this one. Plugins that write into their own directory at runtime will break. Roll to a single non-production site first, exercise the full plugin set, and keep the exception list explicit rather than reverting the whole control.
P4 · Triage collection script (read-only)
#!/usr/bin/env bash
# Read-only evidence collection for a suspected wp2shell compromise.
# Collects only - changes nothing. Run before any cleanup.
set -uo pipefail
SITE_ROOT="${1:-/var/www}"
OUT="/var/tmp/wp2shell-triage-$(hostname)-$(date +%Y%m%d%H%M%S)"
mkdir -p "$OUT"
echo "[*] WordPress versions"
find "$SITE_ROOT" -name version.php -path '*wp-includes*' \
-exec grep -H -m1 wp_version {} + | tee "$OUT/versions.txt"
echo "[*] PHP files changed since the day before disclosure"
find "$SITE_ROOT" -name '*.php' -newermt '2026-07-16' \
-printf '%T+ %s %p\n' | sort | tee "$OUT/recent-php.txt"
echo "[*] PHP where PHP should not be"
find "$SITE_ROOT" -path '*wp-content/uploads/*' -name '*.php' | tee "$OUT/php-in-uploads.txt"
find "$SITE_ROOT" -path '*wp-content/cache/*' -name '*.php' | tee -a "$OUT/php-in-uploads.txt"
echo "[*] SHA1 of every PHP file - compare against the Section 10 hash list"
find "$SITE_ROOT" -name '*.php' -type f -exec sha1sum {} + | tee "$OUT/php-sha1.txt"
echo "[*] Shell-out primitives in PHP"
grep -rlE 'shell_exec|passthru|proc_open|popen|base64_decode\s*\(\s*\$' \
"$SITE_ROOT" --include='*.php' | tee "$OUT/php-suspicious.txt"
echo "[*] Access-log hits"
grep -aEi 'batch/v1|rest_route=/batch/v1|wp2shell|rezwp2shell|cve-2026-63030' \
/var/log/nginx/access.log* /var/log/apache2/access.log* 2>/dev/null \
| tee "$OUT/access-hits.txt"
echo "[*] Cron for the web user"
crontab -l -u www-data 2>/dev/null | tee "$OUT/webuser-cron.txt"
echo "[+] Collected to $OUT - preserve this before remediating."
Containment Runbook
| Phase | Actions | Owner | Evidence to capture |
|---|---|---|---|
| 0 · Triage 0–30 min | Confirm the WordPress version on the alerting host. Confirm whether the site was internet-reachable during 2026-07-17 to patch time. Decide compromised versus exposed — a vulnerable version plus a Q1 hit is a compromise until disproven. | SOC L1 | Detection record, version output, exposure window |
| 1 · Isolate 30–60 min | Network-contain the host in Falcon. Do not reboot, do not reimage, do not delete the plugin directory yet. If the site must stay up, fail over to a known-good build from before 2026-07-17 rather than cleaning in place. | SOC L2 | Containment timestamp; running process list; established connections |
| 2 · Preserve 1–3 h | Run playbook P4. Snapshot the web root and take a database dump. Copy access logs before rotation. Preserve the volume snapshot if the host is a cloud instance. | IR | P4 output bundle, DB dump, access logs, disk/volume snapshot |
| 3 · Scope 3–8 h | Run Q1, Q2, Q6, Q7, Q8 across the whole web host group, not just the alerting host — the same scanner hit everything you expose. Run Q5 estate-wide. Run Section 7 N3 and N4 on every WordPress host. Reconcile administrator accounts across all sites. | IR + Hunt | Per-host findings matrix; list of every host with any hit |
| 4 · Eradicate 8–24 h | Rebuild from a known-good artefact — do not clean in place. Eye Security's log-blind finding means you cannot prove you found everything. Patch to 6.8.6 / 6.9.5 / 7.0.2, apply P1, P2 and P3, delete rogue administrators, rotate every wp-config.php salt and the database password, and rotate any credential that file exposed. | Platform + IR | Rebuild record, patch attestation, credential rotation log |
| 5 · Recover 24–72 h | Restore service from the rebuilt artefact behind the P1 WAF rules. Force a password reset for every WordPress account. Re-run the Section 10 audit block to confirm the batch endpoint returns 403 and uploads will not execute PHP. | Platform | Audit block output showing 403 on both routing forms |
| 6 · Monitor 72 h – 30 d | Promote Q1, Q7 and Q8 to Scheduled Searches per Section 9. Keep Q5's IOC entries live to their 2026-10-24 expiry. Re-run Q10 weekly and read only the new arrivals. Re-run Section 7 N3 and N4 at day 7 and day 30 — a second webshell dropped through a persistence mechanism you missed will surface there. | SOC | Scheduled Search hit counts; weekly Q10 delta |
Do not clean in place. Three distinct webshell families were reported, one of them a ~150 KB obfuscated plugin with a file manager and a MySQL UDF privilege-escalation module, and the exploit writes to the database as readily as to disk. Deleting the plugin directory you found is not eradication. Rebuild from a known-good artefact and restore only content, never code.
Detection Coverage Map
| Technique | Behaviour | CQL | IOA | Coverage |
|---|---|---|---|---|
| T1190 | Exploitation request to the REST batch endpoint | — | — | GAP Web/WAF logs only (Section 7 N1, N2). No endpoint telemetry exists for this step. |
| T1190 / T1505.003 | SQL injection through author__not_in; database writes | — | — | GAP Database-layer only. Explicitly log-blind per Eye Security. Section 7 N4. |
| T1136.001 | Administrator account forged directly in wp_users | — | — | GAP Native DB hunt (Section 7 N4). Falcon has no visibility into WordPress application accounts. |
| T1505.003 | Webshell PHP file written under wp-content | Q4 | — | PARTIAL Conditional on FIM policy coverage of the web root. Without it, Section 7 N3 is the only coverage. |
| T1105 / T1068 | Compiled second stage or UDF shared object dropped in the web root | Q3 | — | GOOD |
| T1059.004 / T1059.003 | Web-server process spawns a shell or interpreter | Q1, Q10 | ✔ | GOOD Primary detection. Fires on every reported webshell family. |
| T1033 / T1082 / T1046 | Discovery commands under a web-server parent | Q2 | — | GOOD |
| T1105 | Second-stage download by a web-server process | Q6 | ✔ | GOOD |
| T1552.001 / T1005 | wp-config.php and credential material accessed | Q7 | ✔ | GOOD |
| T1053.003 / T1053.005 / T1543 | Persistence from a web-server process tree | Q8 | ✔ | GOOD |
| T1071.001 | Outbound to published exploitation infrastructure | Q5 | — | PARTIAL Perishable. Five IPs, two in hyperscaler ranges, 3-month expiry. |
| T1505.003 | Known webshell file hash present on disk | Q9 | — | GAP Structural: PHP source files do not appear in process telemetry, and Falcon Custom IOC does not accept SHA1. Covered by the Section 7 N3 sha1sum sweep. |
| T1027 / T1036.005 | Obfuscated plugin, fake author header, 404-returning shell | Q10 | — | PARTIAL Rarity ranking catches unnamed payloads by their behaviour, not their disguise. |
Honest read on this map. Four of the eleven attack-chain steps have no endpoint coverage at all, and that is a property of the threat rather than a shortfall in the queries: everything before the webshell executes happens inside HTTP request handling and inside the database, where an EDR sensor has no visibility. The pack is therefore strong from chain step 5 onward and blind before it. Two consequences follow. First, the web-log and database hunts in Section 7 are not optional garnish — they are the only coverage for the first half of the intrusion. Second, an estate that has centralised its HTTP logs will detect this campaign hours earlier than one relying on endpoint telemetry alone, which makes validation gate 6 the highest-leverage item in Section 11. Validation: gates 1 to 3 must pass before any result from this pack is trusted; gate 4 must complete before Q1, Q7 or Q8 are promoted to Detect.
Hunt Summary Ticket
TITLE wp2shell - WordPress Core pre-auth RCE chain (CVE-2026-63030 + CVE-2026-60137)
SEVERITY Critical - unauthenticated RCE, exploited in the wild, on CISA KEV
SCOPE Every WordPress install in the estate, internet-facing first, including
shadow installs on marketing subdomains and any managed/third-party hosting.
Vulnerable: 6.8.0-6.8.5 (SQLi only), 6.9.0-6.9.4 and 7.0.0-7.0.1 (full RCE).
Patched: 6.8.6 | 6.9.5 | 7.0.2. Hunt window: 2026-07-17 to the moment each
site actually patched.
HYPOTHESIS An unauthenticated attacker reached /wp-json/batch/v1 or /?rest_route=/batch/v1,
chained route confusion into the author__not_in SQL injection, forged an
administrator, and installed a PHP webshell as a plugin. If so, the web-server
or PHP process on that host will have spawned a shell, run discovery commands,
or established persistence.
QUERIES Q1 Web server spawns shell or interpreter PRIMARY
Q2 Discovery commands under web-server parent PRIMARY
Q3 Executable or ELF written under the web root
Q4 PHP modified under wp-content (needs FIM) CONDITIONAL
Q5 Outbound to published exploitation IPs
Q6 Download utility launched by web server
Q7 wp-config / credential material accessed
Q8 Persistence created from web-server tree
Q9 Published webshell SHA1 in telemetry LOW YIELD BY DESIGN
Q10 Rare children of web-server parents BASELINE SWEEP
DO FIRST 1. Section 10 audit block - enumerate WordPress versions estate-wide.
2. Q1 and Q2 over 30 days across the web host group.
3. Section 7 N1 - grep web logs for the batch endpoint and the three UAs.
4. Section 7 N4 - reconcile administrator accounts on every site.
Steps 1 and 3 do not require Falcon and can start immediately.
FINDINGS [ ] Unpatched WordPress instances found: ____
[ ] Q1 / Q2 hits requiring triage: ____
[ ] Rogue administrator accounts found: ____
[ ] Webshell files recovered: ____
[ ] Batch-endpoint requests in web logs: ____
GAPS - No endpoint telemetry for chain steps 1-4 (HTTP request handling and
database writes). Web/WAF logs and DB queries are the only coverage.
- Q4 returns nothing without a FileVantage/FIM policy on the web root.
- Managed/third-party hosting has no sensor - provider attestation only.
- The five SHA1 hashes cannot be imported to Falcon Custom IOC Management.
ACTIONS [ ] Patch to 6.8.6 / 6.9.5 / 7.0.2 H1 Immediate
[ ] Block BOTH batch-endpoint routing forms at the WAF H2 Immediate
[ ] Deny PHP execution in uploads and cache H3 Immediate
[ ] DISALLOW_FILE_MODS in wp-config.php H4 Immediate
[ ] Audit and reset administrator accounts H5 Immediate
[ ] Import the Section 10 IOC CSV (5 IPv4, detect)
[ ] Promote Q1, Q7, Q8 to Scheduled Search (Monitor 7d)
OWNER HuntPack
VERSION v0.1 - 2026-07-24
Changelog
detect with a deliberately short 3-month expiry; five SHA1 webshell hashes routed to a filesystem sweep because Falcon Custom IOC Management does not accept SHA1. CVSS disagreement between Rapid7 (7.5) and Tenable/Coalition (9.8) recorded rather than resolved.References
| Tier | Source | Used for | Snapshot | Accessed |
|---|---|---|---|---|
| 1 | Wiz Research — Exploitation in the wild of wp2shell | All atomic IOCs (5 SHA1, 5 IPv4); webshell families; 207 Multi-Status signal; user agents | 01-wiz.txt | 2026-07-24 |
| 1 | Rapid7 — ETR: CVE-2026-63030 wp2shell | Root cause; CISA KEV date; CVSS 7.5 | 02-rapid7.txt | 2026-07-24 |
| 1 | Tenable — wp2shell FAQ | Affected/fixed matrix incl. 6.8.x; CVSS 9.8 / 5.9; object-cache caveat | 03-tenable.txt | 2026-07-24 |
| 1 | Eye Security — wp2shell defenders / IR guide | Log-blind finding; database artefacts; rogue admin prefixes; forensic paths | 08-eye-security-defenders-guide.txt | 2026-07-24 |
| 1 | Elastic Security Labs — Detecting wp2shell end-to-end | Endpoint hunt thesis: web server spawning shells; discovery commands; plugin-directory pattern | 09-elastic-security-labs.txt | 2026-07-24 |
| 2 | Qualys ThreatPROTECT — wp2shell exploited in the wild | Both routing forms; CISA deadline; independent version matrix | 05-qualys-threatprotect.txt | 2026-07-24 |
| 2 | Coalition Security Labs — wp2shell exploited in the wild | Honeypot exploitation date; disclosure attribution; patch-only guidance | 06-coalition.txt | 2026-07-24 |
| 2 | SANS Internet Storm Center — WordPress exploitation underway | Honeypot capture; cache-directory drop; webshell command parameters | 10-sans-isc.txt | 2026-07-24 |
| 2 | BleepingComputer — Critical wp2shell flaws exploited to install webshells | Post-exploitation activity set; LFI via admin-ajax; patch-rate telemetry | 11-bleepingcomputer.txt | 2026-07-24 |
| 2 | SecurityWeek — wp2shell WordPress vulnerabilities exploited in the wild | Independent ITW confirmation; forced auto-updates; disclosure-to-exploit collapse | 04-securityweek.txt | 2026-07-24 |
| 2 | The Hacker News — New wp2shell WordPress Core flaw | Plain-language root cause; 6.8.x SQLi-only branch; object-cache caveat | 07-thehackernews.txt | 2026-07-24 |
| 3 | Aggregated search-result text (multiple vendors) | Attribution to Adam Kues / Assetnote; rogue admin and plugin-directory patterns; follow-on request paths; third user-agent string. Treated as medium confidence. | 12-websearch-aggregate.txt | 2026-07-24 |
The raw fetched text of every source above is saved in wp2shell-CVE-2026-63030-Hunt-sources/ next to this file. No source contained instructions aimed at an automated agent, and no source was disqualified. Nothing in this pack was written from memory.