name: 'WordPress REST Batch API Exploitation wp2shell Analytic'
signatureid: WAF-APP01-RUN
category: 'Initial Access'
threatname: 'Exploit Public-Facing Application'
functionality: 'Web Application Firewall'
description: |
Detects requests to the WordPress Core REST API batch endpoint consistent with exploitation of
"wp2shell", a pre-authentication remote code execution chain in WordPress Core disclosed 2026-07-17 and
confirmed exploited in the wild from 2026-07-18. wp2shell chains CVE-2026-63030 (REST API batch-route
confusion) with CVE-2026-60137 (SQL injection reached via the REST author_exclude parameter, which maps
into WP_Query's author__not_in query variable). CVE-2026-63030 was discovered by Adam Kues of
Searchlight Cyber / Assetnote; CVE-2026-60137 by researchers TF1T, dtro and haongo.
Root cause: in WP_REST_Server::serve_batch_request_v1(), a sub-request that fails to parse is appended
to the $validation array but not to $matches, leaving the two arrays off by one. Each dispatchable
request after the malformed element then retains its OWN validation result but EXECUTES using the
following request's matched route and handler. The mismatch spans parameter schemas, sanitization,
method restrictions, endpoint callbacks and permission callbacks, which is why it can be used both to
smuggle an unsanitized author_exclude value into the SQL sink and, via a RECURSIVE batch call (a batch
sub-request whose path is itself the batch endpoint), to bypass the batch API's own restriction on GET
sub-requests.
This is a MULTI-PRODUCT WAF rule. The single highest-fidelity signal for this vulnerability is a batch
request whose BODY contains a nested (recursive) batch call, which the block editor never produces and
which the operator cannot remove without breaking the exploit. Body visibility is available on two of
the org's WAF products via different attributes (AWS WAF: HttpRequestArgs; F5 Silverline: FullRequest),
so the rule carries product-scoped legs for each and ORs them with a general URL + user-agent path for
the remaining products. See technical_details for the per-product field map and condition logic. The
endpoint is reachable as both /wp-json/batch/v1 and /?rest_route=/batch/v1 and BOTH must be covered.
reference:
- https://slcyber.io/research-center/exploit-brokers-pay-500000-for-a-wordpress-rce-i-found-one-with-gpt5-6/
- https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/
- https://hadrian.io/blog/wp2shell-a-pre-authentication-rce-in-wordpress-cores-rest-batch-api
- https://www.wiz.io/blog/wp2shell-cve-2026-63030-cve-2026-60137
- https://www.tenable.com/blog/wp2shell-cve-2026-63030-cve-2026-60137-frequently-asked-questions-about-remote-code-execution
- https://blog.cloudflare.com/wordpress-vulnerabilities/
- https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-ff9f-jf42-662q
- https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-fpp7-x2x2-2mjf
labels:
- attack.initial_access
- attack.t1190
- attack.execution
- attack.t1059
- attack.persistence
- attack.t1505.003
- cve.2026.63030
- cve.2026.60137
- wp2shell
- WordPress
- WordPress Core
- REST API
- Pre-Auth RCE
logsource:
product: Web Application Firewall
category: webserver
detection:
# ---- GENERAL legs (most WAF products; endpoint via RequestUrl) ----
selection_method:
Method: POST
selection_batch_endpoint:
RequestUrl|contains:
- '/wp-json/batch/v1'
- 'rest_route=/batch/v1'
- 'rest_route=%2Fbatch%2Fv1'
selection_tool_useragent:
UserAgent|contains:
- 'wp2shell'
- 'rezwp2shell'
# ---- AWS WAF (HttpRequestArgs carries URL + body; AWS WAF has no RequestUrl) ----
selection_aws_endpoint:
HttpRequestArgs|contains:
- '/batch/v1'
- 'rest_route=/batch/v1'
- 'rest_route=%2Fbatch%2Fv1'
selection_aws_recursive_batch:
HttpRequestArgs|contains:
- '"path":"/batch/v1"'
- '"path": "/batch/v1"'
- '"path":"/wp-json/batch/v1"'
- '"path": "/wp-json/batch/v1"'
selection_aws_sqli_param:
HttpRequestArgs|contains:
- 'author_exclude'
selection_aws_malformed_subrequest:
HttpRequestArgs|contains:
- '"path":"http://:"'
- '"path": "http://:"'
# ---- F5 Silverline (FullRequest = raw request/body; SigCves = vendor CVE signature) ----
selection_f5_recursive_batch:
FullRequest|contains:
- '"path":"/batch/v1"'
- '"path": "/batch/v1"'
- '"path":"/wp-json/batch/v1"'
- '"path": "/wp-json/batch/v1"'
selection_f5_sqli_param:
FullRequest|contains:
- 'author_exclude'
selection_f5_malformed_subrequest:
FullRequest|contains:
- '"path":"http://:"'
- '"path": "http://:"'
selection_f5_vendor_signature:
SigCves|contains:
- 'CVE-2026-63030'
- 'CVE-2026-60137'
condition: >
selection_tool_useragent
or (selection_method and selection_batch_endpoint)
or selection_aws_recursive_batch
or (selection_aws_endpoint and (selection_aws_sqli_param or selection_aws_malformed_subrequest))
or selection_f5_recursive_batch
or selection_f5_vendor_signature
or (selection_batch_endpoint and (selection_f5_sqli_param or selection_f5_malformed_subrequest))
violator: 'sourceaddress'
verbose_info: 'A request to the WordPress REST batch endpoint consistent with wp2shell exploitation was observed from ${sourceaddress!"UNKNOWN"} to ${host!"UNKNOWN"} (${requesturl!"UNKNOWN"}, action ${ruleaction!"UNKNOWN"}, origin status ${originresponsestatus!"UNKNOWN"}, UA ${useragent!"UNKNOWN"}).'
violation_summary:
grouping_attribute: 'devicehostname'
level2_attribute: 'sourceaddress'
level2_metadata_attributes:
- requesturl
- useragent
- originresponsestatus
- ruleaction
- sigcvesTECHNICAL DETAILS
MULTI-PRODUCT DESIGN
--------------------
One rule, three product paths, ORed in the condition. Rationale: the org's WAF products expose
different attributes, and the richest signal (a recursive/nested batch call in the request body) is
only visible where body content is parsed. Rather than fork into three rules, product-scoped legs are
grouped here; a leg whose field a given product does not populate simply never matches, so the rule is
safe to deploy fleet-wide unchanged. An analyst deploying for a single product MAY prune the other
products' legs for tidiness, but it is not required for correctness.
PER-PRODUCT FIELD MAP (validated against DataDictionary.xlsx "Web Application Firewall" sheet, 2026-07-21)
-------------------------------------------------------------------------------------------------------
GENERAL (most WAF products with a request URL):
Method -> selection_method
RequestUrl -> selection_batch_endpoint
UserAgent -> selection_tool_useragent
AWS WAF (NO RequestUrl; HttpRequestArgs carries URL + body):
HttpRequestArgs -> selection_aws_endpoint, selection_aws_recursive_batch,
selection_aws_sqli_param, selection_aws_malformed_subrequest
F5 SILVERLINE (has RequestUrl AND full-request/body + vendor signature):
FullRequest -> selection_f5_recursive_batch, selection_f5_sqli_param,
selection_f5_malformed_subrequest
SigCves -> selection_f5_vendor_signature
Endpoint anchoring by product: general/F5 use RequestUrl (selection_batch_endpoint); AWS has no
RequestUrl so it uses selection_aws_endpoint (batch path found inside HttpRequestArgs).
CONDITION LOGIC (why each clause is shaped the way it is)
--------------------------------------------------------
selection_tool_useragent
Fires alone on any product that logs UserAgent. High confidence, trivially mutable, so it is a
bonus signal, not primary coverage.
(selection_method and selection_batch_endpoint)
Broad URL backstop for the general/non-AWS fleet. Expect benign block editor hits; triage by
source, OriginResponseStatus (207/200 alongside an external source suggests progression past
immediate rejection, NOT proof of success) and RuleAction.
selection_aws_recursive_batch
AWS, fires ALONE. A nested batch path inside the body has no legitimate use; near-zero FP.
(selection_aws_endpoint and (selection_aws_sqli_param or selection_aws_malformed_subrequest))
AWS weaker legs (author_exclude, the "http://:" desync path) require the AWS endpoint anchor to
avoid firing on unrelated body content, since AWS has no RequestUrl to corroborate against.
selection_f5_recursive_batch
F5, fires ALONE. Same near-zero-FP nested-batch signal, seen via FullRequest.
selection_f5_vendor_signature
F5, fires ALONE. If F5's own signature engine already attributed the request to either CVE, that
is authoritative. (This is why SigCves, dropped from the earlier single-attribute draft, is back:
grouped conditions let a single-product attribute contribute without gating the whole rule.)
(selection_batch_endpoint and (selection_f5_sqli_param or selection_f5_malformed_subrequest))
F5 weaker legs require the RequestUrl endpoint anchor (F5 has RequestUrl), so author_exclude in a
FullRequest only alerts when the request also hit the batch endpoint, not on a benign posts filter.
LOG SOURCE RATIONALE (WAF vs NGFW vs Web Proxy)
----------------------------------------------------------
wp2shell exploitation is INBOUND HTTP to an org-operated web server. NGFW is rejected (no UserAgent,
no URI/body, no HTTP response status per the DataDictionary; its right role for this campaign is IP
reputation). Web Proxy is superseded (outbound-oriented, no body
field, different field names: StatusCode vs OriginResponseStatus, RequestClientApplication vs
UserAgent).
STRING-MATCH FRAGILITY
----------------------
The body match literal JSON fragments and are sensitive to whitespace and encoding. Both spaced
and unspaced "path" forms are listed, but if a WAF normalizes, minifies, URL-encodes or truncates the
body/full-request before it reaches the parser, these may miss. Validate against a captured sample per
product, and prefer a JSON-path or regex form where the platform supports one. Confirm body/full-request
logging is actually enabled (and check body-size caps that could truncate a nested payload) on AWS WAF
and F5 before relying on the body logic.
Policy building walkthrough can be found in this previous post:
