July 17, 2026. WordPress drops patches for two vulnerabilities. Within hours, mass scanning begins. Within a day, working PoCs land on GitHub. By the weekend, tens of thousands of exploitation attempts, over a hundred backdoor accounts, and a spot on CISA’s KEV list.
Two CVEs. One chain. Unauthenticated remote code execution on stock WordPress. No plugins needed.
what actually happened#
Two vulnerabilities chained together:
CVE-2026-60137 — SQL injection in author__not_in. The value gets dropped directly into raw SQL. Requires authentication on its own. High severity, but contained.
CVE-2026-63030 — logic flaw in the REST batch API at /wp-json/batch/v1. Validation and execution run in separate loops. When one sub-request errors during validation, it gets removed from the validation array but not the execution array. The arrays desync by one position. Sub-request N executes under the permission context of sub-request N-1.
Chain them together? The batch confusion removes the auth requirement from the SQLi. Unauthenticated attacker reaches a protected endpoint, injects SQL, escalates to admin, drops a webshell, and cleans up. All in one request.
the timeline (so far)#
| When | What |
|---|---|
| July 17, ~18:00 UTC | Patches released (6.9.5, 7.0.2). Forced auto-updates enabled. |
| July 17, within hours | First honeypot exploitation. Mass scanning of /wp-json/batch/v1. |
| July 18 | Public PoC on GitHub. Full IPv4 scanning begins. |
| July 19 | IR engagements active. Webshells confirmed in the wild. CISA advisory. |
| July 20–22 | Exploitation ongoing. Tens of millions of unpatched sites globally. |
the attack chain in plain english#
Step 1 — Route confusion
Craft a sub-request that errors during validation. The batch handler drops it from the permission-check array but not from the execution array. Now every following sub-request runs with the wrong permissions. Auth-required endpoints become reachable without login.
Step 2 — SQL injection
With the auth gate gone, the author__not_in parameter accepts a raw string instead of an integer array. PHP type juggling skips sanitization, and the attacker’s value lands straight in the SQL WHERE clause. UNION injection reads admin password hashes from wp_users and writes to wp_posts and wp_options.
Step 3 — Customizer escalation to RCE
Forged customize_changeset and request database rows trick WordPress’s customizer preview into executing in the context of an existing admin (usually user ID 1). From there: create a rogue admin, install a webshell plugin, run arbitrary PHP, delete the evidence. Done.
what a Security analyst watches for#
Forget complex queries for a moment. Here’s the signal:
Network: POST requests to /wp-json/batch/v1 or /?rest_route=/batch/v1. HTTP 207 Multi-Status is a high-fidelity indicator — the batch endpoint returns this on partial success. User-agent strings wp2shell and rezwp2shell are dead giveaways. Request bodies containing author__not_in, UNION ALL SELECT, or SLEEP( are the smoking gun.
Host: New admin accounts created after July 17. customize_changeset rows with suspiciously high parent IDs in wp_posts. PHP files sitting in wp-content/uploads/ — that directory should never have executable code. Webshell signatures like eval(base64_decode or system($_ are worth grepping for.
None of this is subtle. The batch API abuse lights up access logs. The SQL injection leaves fingerprints in request bodies. The post-exploitation artifacts persist until someone looks.
confirmed IOCs (as of July 22)#
Exploitation IPs:
- 34.81.132[.]62
- 79.177.131[.]206
- 15.157.135[.]170
- 94.100.52[.]128
- 172.235.128[.]52
Webshell hashes (SHA1):
- 2a1410d8e2a8337ac2171cedea8c0fdc47c647a0
- 58eca847e9eae9e6b08cc211f1559817b71bc4cc
- ebea44890f434d5d67ede22009a3f4bb5cac33f8
- d9a220c8039f1c4d72cae7ccb8b3a33dec8815be
- e9756e2338f84746007235e4cab7a70d5b3ca47f
Rogue accounts:
- Username prefix:
wp2_*,w2s_* - Email domains:
@wp2shell.invalid,@wp2shell.shellcode.lol
post-exploitation behavior#
Attackers aren’t just dropping shells and leaving. Observed activity includes:
- Malicious plugin uploads via
/wp-admin/update.php?action=upload-plugin - User enumeration through
/wp-json/wp/v2/users?context=edit - LFI attempts targeting
wp-config.phpfor database credentials - Admin panel access confirmed from attacker IPs
One webshell found in the wild was a 150KB “CMSmap” disguised as a legitimate plugin — full GUI, file manager, DB access, port scanner. Obfuscated with hex-encoded, gzip-compressed base64. That’s not script kiddie territory. That’s operational infrastructure.
MITRE ATT&CK quick map#
| Tactic | Technique |
|---|---|
| Initial Access | T1190 — Exploit Public-Facing Application |
| Execution | T1059.004 — Unix Shell |
| Persistence | T1505.003 — Web Shell |
| Privilege Escalation | T1548 — Abuse Elevation Control Mechanism |
| Discovery | T1087.001 — Account Discovery |
| Credential Access | T1003 — OS Credential Dumping |
| Defense Evasion | T1027 — Obfuscated Files |
what to do right now#
Patch. WordPress 6.9.5, 7.0.2, or newer. On 6.8.x (SQLi only), apply 6.8.6. Forced auto-updates are active — don’t disable them.
If patching isn’t immediate, block at the WAF:
location ~* /wp-json/batch/ { return 403; }
location ~* [?&]rest_route=/batch { return 403; }Check for compromise. Look for new admin accounts created after July 17. Sweep for PHP files in uploads. If you find any, assume full compromise and initiate IR.
why this one matters#
WordPress vulnerabilities are common. Most need a plugin. Most need authentication. Most are patched before mass exploitation.
This one was different. Stock WordPress. No auth required. Weaponized within hours. The chain — two vulnerabilities, neither catastrophic alone, devastating together — is a reminder that the gaps between systems matter as much as the systems themselves.
If you’re defending WordPress, run the checks. If you’re a Security analyst, tune your alerts for that batch endpoint. The window is still open.
Maximum effort.
Sources: Searchlight Cyber, Wiz Research, Eye Security, Brandefense, CISA KEV. IOCs confirmed as of July 22, 2026.
