Flash CTF – Layer Cake

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.jsonoci-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 RUNCOPY, 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}