M4cCrypt0
back to overview

Ponzi — Race Condition in Daily Reward Claim (difficulty: Medium)

Ponzi — Race Condition in Daily Reward Claim

Briefing

Target app: Ponzi, a fictional crypto-rewards app ("wellness portal, poolside edition") with a daily claim mechanism. Goal: prove the 24-hour cooldown on the daily reward can be bypassed, collect enough reward points for "Whale Vault" status, and pull the flag from the vault.

The category tags on the room already hinted at where the problem was: Business Logic and API Abuse, not classic injection. That turned out to be correct.

Reconnaissance

Created a guest account and claimed the daily-reward button once normally via the UI. Then used Burp Proxy History to find the actual API call behind that button (the dashboard page itself is just a server-rendered GET, the claim action is separate):

text
POST /claim HTTP/1.1
Host: <MACHINE_IP>:3000
Cookie: connect.sid=<SESSION_TOKEN>
Content-Length: 0

Important detail: no body. All state (who you are, how much you already have, when you last claimed) hangs entirely off the session cookie. That means there's no field to manipulate — the only lever is timing.

A repeated claim attempt confirmed the server-side cooldown check:

text
HTTP/1.1 429 Too Many Requests
{"error":"Reward already claimed. Please wait before claiming again.","secondsRemaining":86374}

An explicit cooldown, so no client-side trick possible — the server enforces this itself on every request.

Vulnerability analysis: TOCTOU

The server logic is presumably built as:

text
1. lastClaim = database.get(userId)
2. if (now - lastClaim < 24h): return 429
3. database.set(userId, now)
4. give reward

That's a classic Time-Of-Check to Time-Of-Use (TOCTOU) pattern: between the check (steps 1-2) and the write (step 3) there's a gap. As long as the server can handle multiple requests on parallel threads/workers, several requests can all see the "not yet claimed" state before the first write is committed. That gap is exactly what the room briefing referred to with "a gap wide enough to walk a whale through".

Exploitation

My first assumption was that a tool like Turbo Intruder (raw sockets, single-packet timing) would be needed to make requests truly arrive simultaneously. Turned out not to be necessary: recent Burp versions support grouping multiple tabs with the same request in Repeater and sending them via "Send group (parallel)" — this sends the requests almost simultaneously, enough to win the race without extra extensions.

Steps:

  1. Created a new guest account for a clean cooldown cycle.
  2. Sent the /claim request (with a fresh session cookie) to Repeater, duplicated three times as separate tabs within the same group.
  3. Sent the group in parallel.

Result: all three requests arrived close enough together that the server still saw the old ("not yet claimed") state for each of them during the check. All three granted a reward, instead of just one — three times the normal claim value was enough to cross the Whale Vault threshold.

Result

Why this is relevant beyond CTF

This wasn't an exploit against memory corruption or an injection point — purely business logic / concurrency abuse. That makes it extra relevant in a banking context: the same pattern (check, then write, with a gap in between) is exactly the risk in:

Falls under OWASP API6:2023 — Unrestricted Access to Sensitive Business Flows. Mitigation is typically: merge the check-and-write into a single atomic database operation (e.g. an UPDATE ... WHERE last_claim < NOW() - INTERVAL '24 hours' with an affected-rows check), or a distributed lock/mutex per user id around the whole claim flow.

Tools used

Note on the approach

The first instinct was to use Turbo Intruder (single-packet attack) for maximum concurrency — not available/installable in this environment. The simpler Repeater parallel-group option turned out to be enough, which is a good lesson: for race conditions the required degree of concurrency depends on how tight the server's concurrency window actually is. Not every race requires raw-socket precision; sometimes "a few hundred milliseconds" is already enough slack.