The Coin That Wont Land
The Coin That Wont Land
Platform: CACTF2026 | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-28 | Status: Solved Techniques: epr_attack, bell_state_preparation, basis_correlation_cheat, deferred_measurement
Summary
Task: quantum bit-commitment (Oathbinding) game exposed over an HTTP JSON API; the committer keeps qubit a while the Warden seals qubit b, and must survive 32 rounds with a per-round challenge basis (2^-32 by luck). Solution: EPR/Bell-state cheat — commit Phi+ entangled pairs, then peek the kept qubit in the challenged basis and open with those outcomes, exploiting the no-go theorem that quantum bit commitment cannot be both binding and hiding.
The Coin That Wont Land — CACTF2026
Description
After the realm cracked apart, nobody trusted a word anyone said, so the Registry built the Oathbinding to settle disputes for good. You swear a vow before the court, the Warden seals it in a box nobody can open, and later they call you back and ask what you swore. Lie about it and the box catches you. Lands, titles, old debts, it is all decided this way now, because it is the one thing nobody can argue with. Or so they say. The court throws question after question at you, round after round, waiting for you to fold. Get through every round without a slip and the confession you drag out is your flag.
English summary: A quantum bit-commitment ("Oathbinding") game is exposed over an HTTP JSON API. Each round you prepare 8 two-qubit strands, keep one qubit (a) and let the Warden seal the other (b). After committing, the court reveals a challenge basis (Z or X); you must "open" with values that match what the Warden measures on the sealed qubits in that basis. All 8 strands must match, for all 32 rounds. Honest play wins each round with probability 1/2 per round (2^-32 total), so brute luck is hopeless. The goal is to pass all 32 rounds deterministically to extract the flag.
Recon
The target is 154.57.164.70:32173. Raw TCP probing is a red herring:
nc 154.57.164.70 32173returns nothing (silent, no banner).nmap -sV -p 32173 -Pn 154.57.164.70identifies the service as HTTP / Gunicorn.curl http://154.57.164.70:32173/returns an HTML app titled "Oathbinding · The Coin That Won't Land"; itsapp.jsreferences/api/new,/api/commit,/api/peek,/api/open,/api/oath.curl http://154.57.164.70:32173/api/oathreturns a self-documenting JSON API reference.
API (from GET /api/oath)
POST /api/new->{token, strands:8, rounds:32}POST /api/commit {token, slots:[<circuit> x 8 strands]}->{challenge: c, round}. Each strand is a 2-qubit circuit over qubitsaandbapplied to|00>. You keep qubita; the Warden seals qubitb. Gates: single-qubitI X Y Z H S SDGonaorb, e.g.['H','a']; andCXwith control/target amonga,b, e.g.['CX','a','b'].POST /api/peek {token, basis}->{a_outcomes:[...]}measures YOUR kepta-qubits in basisZorX.POST /api/open {token, values:[0/1 x 8]}-> the Warden measures eachbin basisc(c=0-> Z,c=1-> X); the oath holds only if every value matches.- Rule: 32 rounds, every round must hold (all 8 strands match) or the court walks. Complete all 32 rounds to get the flag.
Analysis
This is a textbook quantum bit-commitment scheme. The security promise is that the sealed qubit b binds the committer (they cannot change their sworn value) while hiding it from the court until "opening" time. The homepage teaser "2^-32 by luck alone" makes the soundness bound explicit: 32 independent rounds, each with a random binary challenge basis, so an honest-or-guessing committer wins with probability 2^-32.
The decisive observation is the classic no-go theorem: unconditionally secure quantum bit commitment is impossible. A scheme cannot be simultaneously binding and hiding against a committer who keeps half of an entangled pair and defers the choice of what they committed to until after seeing the challenge. That is exactly the escape hatch here — peek lets us measure the kept qubit a in any basis after the challenge is revealed, and the committed circuit is fully attacker-chosen.
Key primitive: the Bell state Phi+ = (|00> + |11>)/sqrt(2), prepared by:
[["H","a"], ["CX","a","b"]]
Phi+ is maximally correlated in BOTH the computational (Z) basis and the Hadamard (X) basis:
- Z-basis: measuring
aandbalways yields equal bits (ZZ correlation +1). - X-basis: measuring
aandbalways yields equal bits (XX correlation +1).
So whatever basis c the court challenges with, the outcome of measuring our kept qubit a in that basis is identical to what the Warden will get measuring the sealed qubit b. The flag text epr_0ath_0p3ns_b0th_w4ys ("opens both ways") confirms the intended EPR/Bell attack: the commitment opens correctly in both Z and X.
Exploitation
Per round, for each of the 8 strands:
- Commit the Bell state
Phi+([["H","a"],["CX","a","b"]]) — this entangles kept qubitawith sealed qubitbinstead of committing a definite basis state. - Read the challenge bit
cfrom the commit response (c=0-> Z,c=1-> X). - Peek the kept
a-qubits in the challenged basis. Becauseaandbare maximally correlated in that basis, thea-outcomes exactly equal what the Warden will measure onb. - Open with those peeked outcomes -> every value matches deterministically.
This holds all 8 strands every round with probability 1, defeating the 2^-32 soundness bound. Running the loop over all 32 rounds yields the flag on the final round.
Solution
python read-only • copy allowed#!/usr/bin/env python3 import requests, json BASE = "http://154.57.164.70:32173" def new(): return requests.post(BASE+"/api/new", timeout=10).json() def commit(token, slots): return requests.post(BASE+"/api/commit", json={"token": token, "slots": slots}, timeout=10).json() def peek(token, basis): return requests.post(BASE+"/api/peek", json={"token": token, "basis": basis}, timeout=10).json() def open_(token, values): return requests.post(BASE+"/api/open", json={"token": token, "values": values}, timeout=10).json() t = new() token = t["token"]; strands = t["strands"]; rounds = t["rounds"] # Bell state per strand: H on a, then CX a,b -> Phi+ = (|00>+|11>)/sqrt(2) bell = [["H", "a"], ["CX", "a", "b"]] for r in range(rounds): slots = [list(bell) for _ in range(strands)] ch = commit(token, slots)["challenge"] basis = "Z" if ch == 0 else "X" # match the Warden's measurement basis outs = peek(token, basis)["a_outcomes"] # a-qubits equal b-qubits in this basis res = open_(token, outs) # every value matches print(f"round {r}: challenge={ch} basis={basis} -> {res}") if "flag" in json.dumps(res).lower(): print("FLAG:", res["flag"]); break
Sample winning output (final round):
round 31: challenge=0 basis=Z ... -> {'flag': 'HTB{REDACTED}', 'passes': 32, 'round_held': True, 'rounds_done': 32, 'total': 32}