Skip to main content
Blog

BETA DETECTION: WordPress REST Batch API Exploitation wp2shell

  • July 21, 2026
  • 0 replies
  • 10 views
Aaron Beardslee
Forum|alt.badge.img
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
- sigcves

TECHNICAL 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:

 

https://connect.securonix.com/threat%2Dresearch%2Dintelligence%2D62/beta%2Ddetection%2Dtelnyx%2Dteampcp%2Dcredential%2Dexfiltration%2D241