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 titled “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 titled “Lire le manifest et l’historique”manifest.json
Section titled “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 titled “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 titled “Localiser les fichiers dans les couches”Lister toutes les couches disponibles
Section titled “Lister toutes les couches disponibles”find docker/ -name "layer.tar"Chercher un fichier précis dans toutes les couches
Section titled “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 titled “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 titled “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 titled “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 titled “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 titled “À 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.