Forensic Docker : analyser les couches d'une image exportée
Une image Docker exportée en fichier .tar contient l’intégralité de ses couches, le manifest et l’historique des commandes ayant servi à la construire. Ces couches représentent des instantanés successifs du système de fichiers, y compris des fichiers supprimés dans une couche ultérieure mais toujours présents dans les couches antérieures.
Structure d’une image exportée
Section intitulée « Structure d’une image exportée »Exporter une image depuis Docker :
docker save nom-image -o image.tar# ou si l'image est déjà un fichier tar fourni dans le challengetar -xf image.tar -C docker/L’arborescence extraite contient :
docker/├── manifest.json # index des couches et nom de l'image├── <hash>.json # configuration complète de l'image└── <hash>/ ├── json # métadonnées de la couche ├── layer.tar # système de fichiers de la couche └── VERSIONChaque répertoire <hash>/ correspond à une couche (RUN, COPY, ADD dans le Dockerfile).
Lire le manifest et l’historique
Section intitulée « Lire le manifest et l’historique »manifest.json
Section intitulée « manifest.json »Le manifest liste les couches dans leur ordre d’application et identifie le fichier de configuration :
cat docker/manifest.json | python3 -m json.tool[ { "Config": "abc123def456.json", "RepoTags": ["monapp:latest"], "Layers": [ "layer1hash/layer.tar", "layer2hash/layer.tar", "layer3hash/layer.tar" ] }]Historique des commandes
Section intitulée « Historique des commandes »Le fichier de configuration (nommé d’après le hash indiqué dans manifest.json) contient l’historique complet des instructions du Dockerfile :
cat docker/abc123def456.json | python3 -m json.tool | grep -A2 '"created_by"'Sortie typique :
"created_by": "/bin/sh -c apt-get install -y curl","created_by": "/bin/sh -c echo 'password123' > /root/secret.txt","created_by": "/bin/sh -c rm /root/secret.txt","created_by": "/bin/sh -c COPY app/ /app",L’historique révèle les commandes exécutées pendant le build. Un rm indique qu’un fichier a été supprimé, mais il reste dans la couche précédente.
Extraire uniquement les lignes de l’historique sans le bruit :
cat docker/abc123def456.json | python3 -c "import json, sysconfig = json.load(sys.stdin)for layer in config.get('history', []): print(layer.get('created_by', ''))"Localiser les fichiers dans les couches
Section intitulée « Localiser les fichiers dans les couches »Lister toutes les couches disponibles
Section intitulée « Lister toutes les couches disponibles »find docker/ -name "layer.tar"Chercher un fichier précis dans toutes les couches
Section intitulée « Chercher un fichier précis dans toutes les couches »for d in docker/*/; do echo "--- Couche : $d" tar -tf "${d}layer.tar" 2>/dev/null | grep "secret.txt" && echo "TROUVÉ dans $d"doneExtraire un fichier depuis une couche spécifique
Section intitulée « Extraire un fichier depuis une couche spécifique »Une fois la couche identifiée :
# Extraire un fichier précistar -xf docker/<hash>/layer.tar root/secret.txt -O
# Ou extraire la couche entière dans un répertoiremkdir layer_contenttar -xf docker/<hash>/layer.tar -C layer_content/Patterns de recherche courants
Section intitulée « Patterns de recherche courants »L’historique des commandes oriente la recherche. Quelques patterns à tester systématiquement :
# Mots de passe et secrets dans les couchesfor d in docker/*/; do tar -tf "${d}layer.tar" 2>/dev/null | grep -iE "(password|secret|key|token|credential|\.env|id_rsa)"done
# Fichiers de configurationfor d in docker/*/; do tar -tf "${d}layer.tar" 2>/dev/null | grep -iE "\.(conf|cfg|ini|yml|yaml|json|env)$"done
# Scripts shell pouvant contenir des credentialsfor d in docker/*/; do tar -tf "${d}layer.tar" 2>/dev/null | grep "\.sh$"done
# Fichiers dans /root et /homefor d in docker/*/; do tar -tf "${d}layer.tar" 2>/dev/null | grep -E "^(root|home)/"doneLire le contenu des fichiers suspects
Section intitulée « Lire le contenu des fichiers suspects »Extraire et afficher directement le contenu sans créer de fichier intermédiaire :
# Afficher le contenu d'un fichier depuis une couchetar -xf docker/<hash>/layer.tar root/secret.txt -O
# Chercher des chaînes dans le contenu brut d'une couche entièretar -xf docker/<hash>/layer.tar -O 2>/dev/null | strings | grep -iE "(password|secret|key|token)"Workflow complet
Section intitulée « Workflow complet »1. Extraire le tar de l'image tar -xf image.tar -C docker/
2. Lire le manifest, identifier l'ordre des couches et le fichier de config cat docker/manifest.json | python3 -m json.tool
3. Lire l'historique des commandes, repérer les rm, echo, COPY suspects cat docker/<config>.json | python3 -m json.tool | grep "created_by"
4. Lister les couches find docker/ -name "layer.tar"
5. Chercher les fichiers supprimés dans les couches antérieures au rm for d in docker/*/; do tar -tf "${d}layer.tar" 2>/dev/null | grep "fichier_cible"; done
6. Extraire et lire les fichiers trouvés tar -xf docker/<hash>/layer.tar chemin/fichier -OÀ retenir
Section intitulée « À retenir »Un fichier supprimé avec RUN rm dans un Dockerfile n’est pas effacé de l’image, il disparaît de la couche courante mais reste accessible dans la couche où il a été créé. L’historique des commandes indique quoi chercher ; les couches indiquent où le trouver.
Pour construire des images sans laisser de secrets dans les couches intermédiaires, utiliser un build multi-stage ou passer les secrets via --secret au moment du build plutôt que de les écrire dans des fichiers supprimés ensuite.