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/
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 /createsuidwith0studs.GET /buy?uid=...at0studs returns a valid UOV signature for0|uid.POST /workverifies the provided signature against the current payload<studs>|<uid>.- If verification succeeds, the handler runs
r.incr(uid). - For resulting studs
1and2, 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]