← Back to Writeups
HTBMediumMisc

Noisy Vault Challenge Scenario

D3v0o0Nu119/2/20265 min read
#misc#htb#medium#majority_vote_decoding#minimal_valid_circuit#qiskit_bitstring_reversal#single_query_oracle_recovery

Noisy Vault Challenge Scenario

Platform: HackTheBox | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-31 | Status: Solved Techniques: majority_vote_decoding, minimal_valid_circuit, qiskit_bitstring_reversal, single_query_oracle_recovery

Summary

Task: recover a 64-bit vault key from a single noisy Qiskit oracle while satisfying structural ancilla-validation constraints. Solution: submit the smallest valid CX-only circuit, aggregate 4096 measurement results with per-bit majority voting, then reverse the recovered bitstring to match Qiskit's endianness.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: HackTheBox | ID: 20260331_hackthebox_noisy_vault_challenge_scenario
  • Tags: endianness, qiskit, quantum_simulation, noisy_oracle, depolarizing_noise, majority_vote
  • Indicators: one oracle query and one final unlock attempt, 64 data qubits plus 16 ancilla qubits, validator requires 16 data-ancilla links, validator requires 4 active ancillas after transpilation, oracle returns 4096 noisy measurement counts
  • Source: 20260331_hackthebox_noisy_vault_challenge_scenario.md

Foothold

Vulnerability / Misconfiguration

  1. majority_vote_decoding
  2. minimal_valid_circuit
  3. qiskit_bitstring_reversal
  4. single_query_oracle_recovery
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • majority_vote_decoding
  • minimal_valid_circuit
  • qiskit_bitstring_reversal
  • single_query_oracle_recovery
  • Tags: endianness, qiskit, quantum_simulation, noisy_oracle, depolarizing_noise, majority_vote

Original Writeup

<details><summary>Click to expand original content</summary>

Description

Noisy Vault Challenge Scenario

The archive contained quantum_noisy_vault/server.py. The service prepares a random 64-bit secret key on 64 data qubits and exposes 16 ancilla qubits. We are allowed exactly one oracle query and one final unlock attempt, so the whole attack must recover the secret from a single noisy measurement batch.

Summary

The key idea is to satisfy the validator with the smallest possible corrective circuit, so we do not disturb the already-encoded secret more than necessary. A chain of 16 CX gates from data qubits 0..15 into ancillas 64..79 passes validation after transpilation, while leaving the data qubits themselves unchanged in the computational basis.

The oracle then returns noisy measurement counts over 4096 shots. Because the circuit mostly preserves the prepared basis state and the noise rates are modest, each secret bit still appears more often than its flipped value. Taking a per-bit majority vote across all returned bitstrings recovers the full key, and reversing the final bitstring fixes Qiskit's classical-bit endianness.

Analysis

Reading server.py shows the important constraints:

  • visible_bits = 64
  • ancilla_qubits = 16
  • one oracle call only: max_oracle_calls = 1
  • one unlock attempt only
  • noise model uses depolarizing error on single-qubit gates, two-qubit gates, and measurement
  • oracle output is get_counts() from 4096 noisy shots

The validator does not care whether our circuit actually corrects noise. It only checks structural activity after transpile(..., optimization_level=3):

  • at least total_data_qubits // 4 = 16 data-ancilla links
  • at least ancilla_qubits // 4 = 4 active ancillas

So the goal is not fancy error correction. The goal is to pass validation while touching the data register as little as possible.

Minimal valid circuit

This was enough:

text
CX:0,64;CX:1,65;CX:2,66;CX:3,67;CX:4,68;CX:5,69;CX:6,70;CX:7,71;CX:8,72;CX:9,73;CX:10,74;CX:11,75;CX:12,76;CX:13,77;CX:14,78;CX:15,79

Why this works:

  • it creates exactly 16 data-ancilla links
  • it activates 16 ancillas, which is comfortably above the minimum 4
  • each operation is a single CX
  • the target is an ancilla initialized to |0⟩, so the data qubit is only used as control and its classical value is preserved

This gives the validator what it wants without adding unnecessary disturbance to the secret-bearing qubits.

Why majority vote works

The server prepares a computational-basis state corresponding to the secret key by applying X only on qubits whose secret bit is 1. If we avoid destructive transformations on the data qubits, every shot ideally measures the same 64-bit string.

Noise changes some outcomes:

  • depolarizing noise can randomize qubit states during gates
  • measurement noise can flip some observed bits
  • the 16 CX gates add some extra two-qubit noise, but only on part of the register

Still, with 4096 shots and relatively small error probabilities, the true value of each bit remains biased above 50%. That means we do not need the most common full 64-bit string. We can recover each bit independently by counting how many shots produced 1 in that position and taking the majority.

This is more robust because different shots can have noise in different positions. Even if no single complete bitstring dominates, the correct value of each individual bit still dominates.

Qiskit endianness gotcha

result.get_counts() returns classical bitstrings in Qiskit's usual order, where the leftmost character corresponds to the highest-index classical bit. Since the server measures range(self.total_data_qubits) directly into range(self.total_data_qubits), the recovered count strings appear reversed relative to the secret key's original qubit indexing.

So after building the majority-voted bitstring, it must be reversed:

python
recovered = measured[::-1]

Without this reversal, the unlock attempt fails even if the per-bit voting is otherwise correct.

Exploitation / Solution

  1. Connect to the service.
  2. Choose the oracle option.
  3. Submit the minimal valid corrective circuit with 16 data-to-ancilla CX links.
  4. Parse the JSON counts dictionary returned from 4096 shots.
  5. For each bit position, sum how many samples contain 1 at that position.
  6. Output 1 if that count is greater than half the total shots, otherwise 0.
  7. Reverse the resulting 64-bit string because of Qiskit bit ordering.
  8. Send the recovered key as the one allowed unlock attempt.

Remote recovered key:

text
1000011100010101100000000100010101101000000101100011001000100111

Remote result:

text
[+] Access Granted! The vault opens: HTB{REDACTED}

Solver

Local solver path:

text
tasks/hackthebox/Noisy_Vault_Challenge_Scenario/solver.py

Minimal solver logic:

python
#!/usr/bin/env python3
import json

DATA_QUBITS = 64
ANCILLA_BASE = 64
CIRCUIT = ";".join(f"CX:{i},{ANCILLA_BASE + i}" for i in range(16))

def decode_counts(counts):
    totals = [0] * DATA_QUBITS
    shots = sum(counts.values())

    for bitstring, count in counts.items():
        for i, bit in enumerate(bitstring):
            if bit == "1":
                totals[i] += count

    measured = "".join("1" if total > shots / 2 else "0" for total in totals)
    return measured[::-1]  # Qiskit bitstring order

# 1) send oracle choice
# 2) send CIRCUIT
# 3) parse JSON counts from server output
# 4) key = decode_counts(counts)
# 5) send unlock choice, then send key
</details>

signed by XESXOR