Overview
We’re given a file called layer_cake.tar. Running file on it says it’s a POSIX tar archive. The obvious first move is to see what’s inside:
$ tar tf layer_cake.tar | head -20
blobs/
blobs/sha256/
blobs/sha256/f44f286046d9...
blobs/sha256/beb52a9ce8ec...
blobs/sha256/c5a09b1f5d09...
...
index.json
manifest.json
oci-layout
repositories
The presence of manifest.json, oci-layout, and content-addressed blobs under blobs/sha256/ is a giveaway that this is a Docker image saved with docker save. The structure follows the Docker image format, where each layer is stored as a separate archive.
Understanding Docker layers
Docker images are built in layers. Each RUN, COPY, or ADD instruction in a Dockerfile produces a new layer that records only the filesystem changes from that step. When a file is deleted in a later layer, Docker doesn’t touch the earlier layers. Instead, it creates a special file called a whiteout file named .wh.<original-filename> in the deleting layer. The overlay filesystem uses this to hide the file from the running container’s view.
The original file is still sitting untouched in whichever layer originally wrote it.
Finding the whiteout
Extract the outer tarball and read manifest.json to get the ordered list of layers:
$ tar xf layer_cake.tar
$ python3 -c "
import json
m = json.load(open('manifest.json'))
for i, l in enumerate(m[0]['Layers']):
print(i+1, l)
"
1 blobs/sha256/f44f286046d9...
2 blobs/sha256/beb52a9ce8ec...
...
15 blobs/sha256/2f5a49ac9565...
Now scan each layer for whiteout files:
$ for i in $(seq 1 15); do
layer=$(python3 -c "import json; print(json.load(open('manifest.json'))[0]['Layers'][$((i-1))])")
wh=$(tar tf "$layer" 2>/dev/null | grep '\.wh\.' || true)
[ -n "$wh" ] && echo "Layer $i: $wh"
done
Layer 14: opt/app/config/.wh.production.env
Layer 14 deletes opt/app/config/production.env. The real file must be in an earlier layer.
Locating the original file
Walk backward (or just scan all layers) to find which one actually contains opt/app/config/production.env:
$ for i in $(seq 1 13); do
layer=$(python3 -c "import json; print(json.load(open('manifest.json'))[0]['Layers'][$((i-1))])")
hit=$(tar tf "$layer" 2>/dev/null | grep 'production.env' || true)
[ -n "$hit" ] && echo "Layer $i: $hit"
done
Layer 11: opt/app/config/production.env
Layer 11 has it. Extract it:
$ LAYER=$(python3 -c "import json; print(json.load(open('manifest.json'))[0]['Layers'][10])")
$ tar xOf "$LAYER" opt/app/config/production.env
# Noirtech Production Configuration
# !! DO NOT COMMIT — contains production secrets !!
# Generated: 2024-11-18 (CI pipeline build #847)
APP_ENV=production
APP_SECRET_KEY=b3f2a1c8d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
# Primary database
DB_HOST=db-primary.noirtech.internal
DB_PORT=5432
DB_NAME=noirtech_prod
DB_USER=noirtech_app
DB_PASSWORD=VFdWMFlVTlVSbnRrTUdOck0zSmZjbTFmWkRBemMyNTBYM0l6ZDNKcGRETmZhR2x6ZERCeWVYMD0K
# Replica
...
DB_PASSWORD looks like a base64-encoded value. Decode it twice to recover the original credential:
$ echo "VFdWMFlVTlVSbnRrTUdOck0zSmZjbTFmWkRBemMyNTBYM0l6ZDNKcGRETmZhR2x6ZERCeWVYMD0K" | base64 -d | base64 -d
MetaCTF{d0ck3r_rm_d03snt_r3writ3_hist0ry}
DB_PASSWORD decoded is the flag.
What happened here
The developer added production.env to the image (likely via a CI/CD pipeline that injects secrets at build time), realized the mistake, and removed it in a subsequent RUN rm step. The commit comment in the logs confirms this: "production.env removed from image (INC-2024-1141)".
But the rm only affects the top-most layer — the earlier COPY layer is immutable and still contains the file. Once the image is saved and distributed, anyone with docker save output can dig it out.
Alternative approaches
Instead of parsing layers manually, you can reach the same result via:
docker load < layer_cake.tar
docker history noirtech-recipes-challenge
docker history will show you the command for each layer. The COPY step is visible in the history, pointing to exactly which layer to inspect.
Or use dive (a dedicated Docker layer explorer), which provides an interactive UI for this exact workflow.
Flag
MetaCTF{d0ck3r_rm_d03snt_r3writ3_hist0ry}