$ cat writeup.md…
$ cat writeup.md…
HackTheBox
The challenge simulates an attack on a BB84-like Quantum Key Distribution (QKD) protocol. We play the role of a compromised Trusted Node between Transmitter (Alice) and Receiver (Bob). The goal is to intercept the quantum key, encrypt the command `TX|FETCH|SECRET`, and send it to the Receiver to obt
The challenge simulates an attack on a BB84-like Quantum Key Distribution (QKD) protocol. We play the role of a compromised Trusted Node between Transmitter (Alice) and Receiver (Bob). The goal is to intercept the quantum key, encrypt the command TX|FETCH|SECRET, and send it to the Receiver to obtain the flag.
k identical copies of the qubit are created (k = Poisson(λ=2) + 2, i.e., k ≥ 2, average ~4)-1) to the Receiverserver.py — TrustedNode: accepts our gates, measures/passes qubitstransmitter.py — Alice: generates qubits, checks basis matchesreceiver.py — Bob: measures qubits, processes commands, stores FLAGutil.py — XOR, validate_entropy (≥64 bits, binomial test p > 0.01)In real BB84, each qubit is a single photon, and measurement destroys the state. Here TX creates k ≥ 2 identical copies of each qubit. This is the classic PNS vulnerability — we can take some copies for ourselves while forwarding the rest to Bob without distortion.
Since k ≥ 2, we can:
One of our results (R_Z or R_X) is guaranteed to match the original TX bit — the one measured in the correct basis.
In the reconciliation phase, TX looks for the first match of its basis with the passed gates:
for j, g in enumerate(gates_chunk): if g == self.gates[i]: match_index = j break
We send:
When TX reports matches, we know which positions went into the key and which basis TX used (= Bob's gate at the matched position):
Connect to the server, receive the k_values array — the number of copies for each of the 128 bits.
For each group of k qubits:
The server returns our measurement results and the gates chosen by Bob.
TX reports a boolean array of matches. For each matched position, determine TX's basis (= Bob's gate) and take the corresponding result from our measurement.
SHA256(key_bits) XOR "TX|FETCH|SECRET" → send as hex → receive FLAG.
#!/usr/bin/env python3 """ MITM on BB84-like QKD — Untrusted Node exploit. Usage: python3 solve.py <ip:port> """ import sys import ast import hashlib from pwn import * context.timeout = 6000 def xor(a: bytes, b: bytes): return bytes([x ^ y for x, y in zip(a, b)]) def solve(host, port): r = remote(host, port) r.recvuntil(b"Sync signal: ") sync_data = r.recvline().strip().decode() sync_signal = ast.literal_eval(sync_data) log.info(f"Sync Signal: {len(sync_signal)} chunks, {sum(sync_signal)} qubits") # Phase 1: measure qubit#0 in Z, qubit#1 in X, pass rest tn_gates_measure = [] for k in sync_signal: tn_gates_measure.extend([0, 1]) # Z and X measurements if k > 2: tn_gates_measure.extend([-1] * (k - 2)) # passthrough payload_1 = ",".join(map(str, tn_gates_measure)) r.sendlineafter(b"measurement: ", payload_1.encode()) log.info("Payload 1 sent. Waiting for quantum simulation...") # Get TN results (our measurements) r.recvuntil(b"Trusted Node results: ") tn_results_data = r.recvline().strip().decode() tn_results = ast.literal_eval(tn_results_data) log.success(f"TN results: {len(tn_results)}") # Parse: 2 results per chunk (Z-measurement, X-measurement) chunk_intercepts = [] res_idx = 0 for _ in sync_signal: val_z = tn_results[res_idx] val_x = tn_results[res_idx + 1] chunk_intercepts.append({"0": val_z, "1": val_x}) res_idx += 2 # Get receiver gates r.recvuntil(b"Receiver gates: ") rx_gates_data = r.recvline().strip().decode() rx_gates = ast.literal_eval(rx_gates_data) # Phase 2: garbage(2) for measured positions, copy Bob's gates for rest tn_gates_matches = [] global_idx = 0 for k in sync_signal: tn_gates_matches.extend([2, 2]) # garbage for our measured qubits global_idx += 2 for _ in range(k - 2): tn_gates_matches.append(rx_gates[global_idx]) # Bob's real gate global_idx += 1 payload_2 = ",".join(map(str, tn_gates_matches)) r.sendlineafter(b"intercept receiver gates : ", payload_2.encode()) # Get matches from TX r.recvuntil(b"Transmitter matches: ") tx_matches_data = r.recvline().strip().decode() tx_matches = ast.literal_eval(tx_matches_data) # Recover key: for each matched position, use our measurement in the correct basis raw_key_bits = "" global_idx = 0 for chunk_idx, k in enumerate(sync_signal): for i in range(k): if tx_matches[global_idx]: basis_used = rx_gates[global_idx] # TX basis = Bob's gate at match bit = chunk_intercepts[chunk_idx][str(basis_used)] raw_key_bits += bit global_idx += 1 log.success(f"Key bits ({len(raw_key_bits)}): {raw_key_bits}") # Encrypt command and send final_key = hashlib.sha256(raw_key_bits.encode()).digest() encrypted_cmd = xor(b"TX|FETCH|SECRET", final_key).hex() r.sendlineafter(b"to receiver : ", encrypted_cmd.encode()) resp = r.recvline().decode().strip() log.success(f"Response: {resp}") if "Command:" in resp: flag = resp.split("Command: ", 1)[1] log.success(f"FLAG: {flag}") r.close() if __name__ == "__main__": if len(sys.argv) != 2 or ":" not in sys.argv[1]: print(f"Usage: {sys.argv[0]} <ip:port>") sys.exit(1) host, port = sys.argv[1].rsplit(":", 1) solve(host, int(port))
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar