TryHackMe's HackerHotel 2026 Event - Day 03 - ComplimentaryTryHackMe's HackerHotel 2026 Event - Day 03 - Complimentary
This writeup is part of a series going through TryHackMe's 2026 14-day Hacker Holidays daily challenge event
Background & Info from the Challenge PageBackground & Info from the Challenge Page
This section was copied from the Day 3 challenge page on THM.
This challenge was rated as Easy.
The category was Cloud and the associated tags were:
- Cloud
- AWS
- Cognito
- IAM Misconfiguration
Concierge BriefingConcierge Briefing
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.
Room AccessRoom Access
- A URL to a AWS webapp http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.com/
Today's Itinerary - GoalsToday's Itinerary - Goals
- Track down the AWS mechanism issuing you credentials behind the scenes.
- Use those credentials to dump more than your own record from the app's DynamoDB table.
- Retrieve the flag from another guest's data.
0xMia's Story0xMia's Story
@0xMia
· posted 40 min after room unlock
"okay wait, the wellness app never once asked me to log in and it STILL knew my name when I opened it 💀 something has to be quietly handing it access behind the scenes... if you find whatever that something is, don't just check what it gives YOU. ask it for more 👀"
First Look at the AppFirst Look at the App
From the start I knew my hands-on experience and knowledge with AWS and Cloud stacks in general is pretty weak beyond cursory knowledge of what they are and what they do.
I was also unfamiliar with DynamoDB, and didn't know whether it was related to AWS. (Spoiler: it is. DynamoDB is an AWS service, and it's a reason that aws-sdk script is on the page.)
My notetaking was quite scarce during this challenge. I might have taken bad habits from the very easy challenges not thinking that as these progress in difficulty, it would be harder to recall everything by memory and note taking was going to become more and more valuable especially for the writeup.
Anyway, time to pop open the URL to see a plain webapp on load:
To see if there's more than meets the eye, I viewed source, and there wasn't much more except two scripts. One is the AWS sdk which is likely, the other is an app.js:
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1500.0.min.js"></script>
<script src="app.js"></script>Understanding app.jsUnderstanding app.js
I read the app.js to see what its all about:
// 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);
}
);
});Things of note:
Looks like it sets up new Cognito credentials for each guest.
function guestId() fetches the id, named byteLotusGuestId, from browser's localstorage. If it's a new user and therefore there is no existing id, it generates a "throwaway id" and shows us exactly how:id = "guest-" + Math.random().toString(36).slice(2, 10); which it stores in localstorage to be fetched by this same function.
function renderDashboard(item) is just UI stuff. Not relevant here.
AWS.config.credentials.get() creates a new Dynamo database instance. It then fetches data using the guestId db key. It then uses the data to render the UI.
After visiting the page, I confirmed my localstorage and throwaway guestId were indeed stored:
Interacting with the AWS BackendInteracting with the AWS Backend
From here, I tried to interact with the AWS backend. Conveniently, I had a lot of information about ways it's used, even if previously unfamiliar, thanks to the app.js which including constants that show details like AWS region, table name, pool id, etc.
I simply used the console in-browser to confirm these are loaded and then played with the objects.
Let's follow along the app.js script and see what are the constructions being made and values being assigned.
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: IDENTITY_POOL_ID,
});Checking what AWS.config.credentials ends up looking like after construction:
Highlighted are 3 values of interest that I wrote down on a notepad in case they come into play later: accessKeyId, sessionToken and secretAccessKey.
The Guest ID Rabbit HoleThe Guest ID Rabbit Hole
For now let's spend some time on how the guest id is formed.Math.random() into a 36-character string, and then a slice of 8 characters is taken out of that result.
Math.random() gives a number like 0.9631704153199483.toString() on that would simply make it a string: "0.9631704153199483".toString(36) would make it "0.yo9ofumjm7".slice(2, 10) would take the 8 characters from index 2 to 9, giving "yo9ofumj" (dropping the leading 0. and everything from index 10 on)
What the argument in toString() does is specify the radix for conversion. No argument = no conversion = just becomes a string. A radix can have a value between 2 and 36. A value of 2 would yield binary. A value of 36 would encode it as base36, including 10 digits + 26 letters of the alphabet (=36). A value of 10 for the radix base would yield the same result as no argument at all.
So we know how guestId is formed, but is it useful in the end? We could generate any number of other guestId values and try to GetItem that user's DB info, but that's basically brute forcing and there's way too many combinations. There should be another way.
The Pivot: Scanning the Whole TableThe Pivot: Scanning the Whole Table
Since I had access to some dynamodb commands, like GetItem, I checked to see what else I had have access to. Dynamodb docs
One that looked good to try is scan():
"The Scan operation returns one or more items and item attributes by accessing every item in a table or a secondary index. "
For scanning, I need a params object with at least a TableName, which I do have, conveniently enough.
Looks like it actually worked and it dumped data from the whole table, including for other guestId. One of them happens to have the good ol' flag.
Kill-ChainKill-Chain
- Open the S3-hosted webapp and view source to find the AWS SDK bundle +
app.js - Read
app.jsand note presence of Cognito Identity Pool ID, AWS region, and DynamoDB table name (complimentary-GuestWellnessProfiles) - Skip brute-forcing
guest_id(base36, ~60B+ possibilities) dynamodb.scan({ TableName: "complimentary-GuestWellnessProfiles" })enabled by IAM misconfig. Every guest record dumped.- Read the flag in another guest's wellness profile
NotesNotes
I was a bit perplexed that the CTF challenge would reveal things like accessKeyId, sessionToken and secretAccessKey and then those ended up not being useful or required. Seemed like a misdirection for a CTF challenge.
However the only reason they weren't useful is because I ran the console commands in the browser console, where app.js had already fetched guest credentials on page load.
Those secrets would have come into play if I had tried to do things with AWS API from outside the browser, like via my regular terminal. In that case, I would have needed those to authenticate the requests and commands.