Sitemap
Chetan Patil

Chetan Patil

Cybersecurity Student | Web Application Security Daily writeups on labs, exploits & methodology. Building skills in public.

Press enter or click to view image in full size

TryHackMe: TryHeartMe

5 min read10 hours ago

--

An Easy-rated room from TryHackMe’s “Love at First Breach 2026” Valentine’s event — a gift-shop web app that trusts a role claim baked straight into the client’s own JWT cookie.

Room brief

Press enter or click to view image in full size

A “find the hidden item” prompt on an e-commerce-styled app almost always means one of two things: a hidden route, or an access-control check that trusts something it shouldn’t. Turned out to be the second.

Step 1 — Poking around the shop

The app is a small Valentine’s-themed storefront: roses, heart chocolates, chocolate-dipped strawberries, and a love letter card, each with a credit price. No “ValenFlag” item visible anywhere in the normal listing.

Press enter or click to view image in full size

Creating an account is free and doesn’t need verification, so signed up with a throwaway address to get past the “Guest” wall and see what a logged-in session looks like.

Press enter or click to view image in full size

After logging in, the account page showed the expected starting state:

Press enter or click to view image in full size

Zero credits, no obvious hidden item, and no way to buy anything even if there were one. Time to look at how the app actually tracks who’s logged in and what they’re allowed to do.

Step 2 — Finding the session cookie

Opened DevTools and checked storage. The app sets a single cookie to track the session:

Name: tryheartme_jwt
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. ...
Press enter or click to view image in full size

That eyJhbGciOi... prefix is the unmistakable signature of a Base64url-encoded JWT header. Three dot-separated segments - header, payload, signature - meaning the app's entire idea of "who you are" lives in a token sitting in a cookie I fully control the storage of.

Step 3 — Decoding the token

Pasted the full cookie value into jwt.io’s decoder to see what’s actually inside it.

Press enter or click to view image in full size

Decoded payload:

{
"email": "lame@mail.com",
"role": "user",
"credits": 0,
"iat": 1794288323,
"theme": "valentine"
}
Press enter or click to view image in full size

And there it is — "role": "user", sitting in plain, readable JSON inside a cookie that lives entirely on the client. If the server trusts this claim without re-checking it against something server-side, then the only thing standing between this account and an admin one is the token's signature.

Step 4 — Forging an admin token

Header confirms the signing algorithm:

{ "alg": "HS256", "typ": "JWT" }

HS256 is symmetric - the same secret key both signs and verifies the token. If that secret is weak, default, or otherwise guessable/crackable (a very common misconfiguration in small apps like this one), anyone who recovers it can mint their own, fully valid, arbitrarily-privileged tokens from scratch.

Get Chetan Patil’s stories in your inbox

Join Medium for free to get updates from this writer.

Switched to jwt.io’s JWT Encoder tab, kept the same payload structure, and changed one field:

{
"email": "lame@mail.com",
"role": "admin",
"credits": 0,
"iat": 1794288323,
"theme": "valentine"
}
Press enter or click to view image in full size

Signed it with the matching secret and got back a freshly forged, validly-signed JWT with my own custom claims baked in.

Step 5 — Swapping the cookie

Replaced the browser’s tryheartme_jwt cookie value with the newly forged token:

Reloaded the account page:

Press enter or click to view image in full size

Two things changed at once:

  • A brand-new Admin link appeared in the navigation, confirming the server is reading the role claim straight out of the JWT to decide what UI/routes to expose.
  • Credits jumped from 0 to 5000 - the server evidently grants staff/admin sessions a fixed credit allowance server-side, rather than trusting whatever number happens to be in the token's credits field.

Step 6 — Admin portal and the hidden item

The Admin link leads straight to a staff-only area:

Press enter or click to view image in full size

And there’s the hidden item the room brief was asking about — ValenFlag, gated entirely behind that role claim and otherwise invisible in the normal shop listing.

Press enter or click to view image in full size

With the forged admin session carrying 5000 credits — comfortably more than the 777-credit price tag — purchased it directly.

Step 7 — Flag

The purchase completes and prints a receipt with a redeemable voucher:

Receipt
Order: ValenFlag - 777 credits
Voucher: ValenFlag redeemed
THM{v4l3nt1n3_jwt_c00k13_t4mp3r_4...}
Press enter or click to view image in full size

Root cause

This is a textbook JWT trust boundary failure, stacked two deep:

  1. Client-controlled authorization claims. role (and credits) are stored directly in a token the client holds and can freely decode. Storing authorization-relevant data in a JWT is fine only if the signature is the sole thing standing between a user and forging it - which leads straight to the second issue.
  2. A weak or recoverable signing secret. HS256 is only as strong as the secret backing it. If that secret can be guessed, brute-forced, or was ever leaked (hardcoded in source, a common default, a short/low-entropy string), the token’s signature stops being a meaningful trust boundary at all — anyone can mint whatever claims they like and sign them validly.

Room Completed!

Press enter or click to view image in full size

--

--