webfreehard

Hens and Roosters

umasscybersec

Task: a Flask app gives a uid, signs 0|uid with UOV, and lets /work trade valid signatures for studs behind an HAProxy URL-based rate limit. Solution: reuse genuine signatures from the app, bypass the limiter with unique query strings, then win a race at studs=2 using HTTP/1.1 last-byte synchronization so many verified requests increment the same uid before the counter update is observed.

$ ls tags/ techniques/
signature_reuseurl_keyed_rate_limit_bypasslast_byte_synchronizationsocket_burst_race

Hens and Roosters — UMassCTF 2026

Description

Organizer description was not preserved in the local task notes.

We receive a web service that creates a temporary uid, gives one free signed token through /buy, and allows /work to exchange a valid signature for more studs. Reaching at least 7 studs makes /buy return the flag.

Analysis

The challenge mixed crypto flavor with a web race, but the intended break was not a public-key forgery against UOV.

Relevant behavior:

  • GET / creates uid with 0 studs.
  • GET /buy?uid=... at 0 studs returns a valid UOV signature for 0|uid.
  • POST /work verifies the provided signature against the current payload <studs>|<uid>.
  • If verification succeeds, the handler runs r.incr(uid).
  • For resulting studs 1 and 2, the server returns a fresh valid signature for the next payload.
  • For studs > 2, the request still increments the counter, but no longer gives new signatures.

The proxy was equally important:

stick-table type string len 2048 size 100k expire 20s store http_req_rate(20s) http-request track-sc0 url http-request deny deny_status 429 if { sc_http_req_rate(0) gt 1 }

Because the stick table keys on the exact full URL string, /work?a=1 and /work?a=2 are rate-limited independently. That makes unique query parameters enough to bypass the intended one-request limit.

Root Cause

The bug is a state-changing race in /work: verification is tied to the current <studs>|<uid> value, but the increment happens after that check and is not protected against many parallel requests validating the same state. Once we have a genuine signature for 2|uid, multiple workers can all verify against studs=2 and each call r.incr(uid).

...

🔒

Permission denied (requires auth)

Sign in to read this free writeup

This writeup is free — just sign in with GitHub to read it.

$ssh [email protected]