Skip to content

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.

Exporter une image depuis Docker :

Terminal window
docker save nom-image -o image.tar
# ou si l'image est déjà un fichier tar fourni dans le challenge
tar -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
└── VERSION

Chaque répertoire <hash>/ correspond à une couche (RUN, COPY, ADD dans le Dockerfile).

Le manifest liste les couches dans leur ordre d’application et identifie le fichier de configuration :

Terminal window
cat docker/manifest.json | python3 -m json.tool
[
{
"Config": "abc123def456.json",
"RepoTags": ["monapp:latest"],
"Layers": [
"layer1hash/layer.tar",
"layer2hash/layer.tar",
"layer3hash/layer.tar"
]
}
]

Le fichier de configuration (nommé d’après le hash indiqué dans manifest.json) contient l’historique complet des instructions du Dockerfile :

Terminal window
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 :

Terminal window
cat docker/abc123def456.json | python3 -c "
import json, sys
config = json.load(sys.stdin)
for layer in config.get('history', []):
print(layer.get('created_by', ''))
"
Terminal window
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”
Terminal window
for d in docker/*/; do
echo "--- Couche : $d"
tar -tf "${d}layer.tar" 2>/dev/null | grep "secret.txt" && echo "TROUVÉ dans $d"
done

Extraire un fichier depuis une couche spécifique

Section titled “Extraire un fichier depuis une couche spécifique”

Une fois la couche identifiée :

Terminal window
# Extraire un fichier précis
tar -xf docker/<hash>/layer.tar root/secret.txt -O
# Ou extraire la couche entière dans un répertoire
mkdir layer_content
tar -xf docker/<hash>/layer.tar -C layer_content/

L’historique des commandes oriente la recherche. Quelques patterns à tester systématiquement :

Terminal window
# Mots de passe et secrets dans les couches
for 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 configuration
for 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 credentials
for d in docker/*/; do
tar -tf "${d}layer.tar" 2>/dev/null | grep "\.sh$"
done
# Fichiers dans /root et /home
for d in docker/*/; do
tar -tf "${d}layer.tar" 2>/dev/null | grep -E "^(root|home)/"
done

Extraire et afficher directement le contenu sans créer de fichier intermédiaire :

Terminal window
# Afficher le contenu d'un fichier depuis une couche
tar -xf docker/<hash>/layer.tar root/secret.txt -O
# Chercher des chaînes dans le contenu brut d'une couche entière
tar -xf docker/<hash>/layer.tar -O 2>/dev/null | strings | grep -iE "(password|secret|key|token)"
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

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.