Magic Vault
Magic Vault is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Magic Vault attack path
Magic Vault is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Objective
Challenge walkthrough focused on Blockchain evidence, validation, and reusable operator lessons.
Walkthrough flow
Audit Setup.sol and Vault.sol.
Identify the win condition: Setup.isSolved() returns...
Identify the gated state transition: claimContent()...
Analyze unlock(bytes16): high 64 credential bits must...
Read the private passphrase from Vault storage slot 2...
Source coverage
High source coverage
Status: complete. This article is generated from 6 sanitized Markdown sources and keeps raw flags, credentials, keys, cookies, and reusable secrets out of the rendered blog.
High confidence: the page is reconstructed from a primary walkthrough plus multiple supporting notes or evidence sources. Treat the chain as source-backed, while still checking the listed source files for sensitive values.
- Blockchain/Magic-Vault/writeup.md
- htb-challenge/Blockchain/Magic-Vault/notes.md
- htb-challenge/Blockchain/Magic-Vault/memory-summary.md
- htb-challenge/Blockchain/Magic-Vault/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Blockchain__Magic-Vault__memory-summary.md.fcbfa02bde.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Blockchain__Magic-Vault__notes.md.8c548f42d7.md
Technical Walkthrough
Writeup
Challenge
- Name: Magic-Vault
- Category: Blockchain
- Difficulty: Easy
- Mode: hybrid
Summary
The vault is solved when mapHolder() no longer points to the Vault contract. claimContent() can change the holder, but only after unlock(bytes16) sets isUnlocked. The required unlock input is predictable because it is derived from readable storage, public block hashes, the public owner address, the public nonce, and the timestamp parity of the block containing the transaction.
Artifact Inventory
- Original archive:
files/a12c7370-50c6-4b2e-951d-235daee847f1.zip - Extracted contracts:
- files/extracted/blockchain_magic_vault/Setup.sol
- files/extracted/blockchain_magic_vault/Vault.sol
- Remote surface:
- /connection_info for player and deployed contract metadata
- /rpc for JSON-RPC
- /flag for completion after isSolved
Analysis
Setup.isSolved() is:
return TARGET.mapHolder() != address(TARGET);Vault.claimContent() sets map.holder = msg.sender, but only if isUnlocked is true. Vault.unlock(bytes16) checks a structured 16-byte input:
- high 64 bits of the input must equal the low 64 bits of
owner - low 64 bits must equal the computed magic key
- full 128-bit input must not equal the magic key
The magic key is derived in _magicPassword() from:
passphrase, stored in slot 2 despite being markedprivatenonce()blockhash(block.number - 1)orblockhash(block.number - 2), depending on timestamp parityowner()
The source audit is recorded in analysis/source-audit.md. RAG did not return a useful challenge-specific match, so the solve used local source plus live RPC evidence only; see analysis/rag-records.md.
The unlock-input formula was validated safely against a fixed historical block using eth_call; that proof is in analysis/<password redacted>. For the live transaction, the solver computes the input for the immediately next block, sends unlock, and retries if a block advances before the transaction lands.
Solve
The reproducible solver is solve/solve.py.
It performs:
- Fetch
/connection_infoand store raw connection material underloot/, with redacted analysis copies. - Read
owner(),nonce(),mapHolder(),isUnlocked(), and passphrase storage slot 2. - Fetch the latest block once and compute the candidate unlock input for the next block using the latest block hash and parent hash.
- Send
unlock(bytes16). - Retry if the transaction misses the expected block context.
- Call
claimContent(). - Confirm
mapHolder()changed to the player. - Request
/flagand store the response inloot/flag-candidate.txt.
The successful live run is summarized in analysis/solve-run-summary.json. Unlock succeeded on attempt 3, claimContent() succeeded, map_holder_after became the player address, and /flag returned HTTP 200.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Solidity
privatestorage is not confidential on-chain. - Block-derived key material is only safe when attackers cannot predict or compute the same block context.
- When an unlock input depends on the block that contains a transaction, off-chain solvers need to account for timing drift or move computation on-chain.
- Fixed-block
eth_callis useful for proving byte-casting and blockhash formulas without mutating state. - Keep
/connection_infoprivate keys and raw flags inloot/, with redacted copies inanalysis/.
Source-Backed Dossier
The sections below are merged from companion Markdown notes for the same case. They are rendered after sanitization so the article stays precise without publishing raw flags, credentials, or target-specific secrets.
Notes
Scope
- Challenge: Magic-Vault
- Category: Blockchain
- Difficulty: Easy
- Mode: hybrid
- Remote instance: <TARGET>:30537
- Start time: 2026-06-09T11:02:14Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7370-50c6-4b2e-951d-235daee847f1.zip | 1509 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 3 shown in artifact inventory JSON |
files/extracted/blockchain_magic_vault/Setup.sol | 373 | <hash redacted> | Java source, ASCII text | |
files/extracted/blockchain_magic_vault/Vault.sol | 1600 | <hash redacted> | ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-09T11:02:14Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-09T11:02:26Z | artifact inventory | analysis/artifact-inventory.json | 3 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-09T11:02:26Z | hypothesis recorded | hypothesis-board.md | Audit provided Solidity contracts and remote JSON-RPC instance, identify vault unlock/key ownership flaw, then satisfy Setup.isSolved and capture /flag. | Medium | Extract contracts, identify Setup.isSolved condition, fetch /connection_info, inspect storage and exposed functions before any state-changing transaction. |
| 2026-06-09T11:04:14Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-09T11:04:14Z | checkpoint recorded | analysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-09T11:04:33Z | RAG query | analysis/rag/rag-query-20260609T110423051734Z-474e01cb.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-09T11:04:52Z | RAG record | analysis/rag-records.md | Retrieved memory tagged MISSING | Medium | Validate or reject with live evidence |
| 2026-06-09T11:14:13Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-09T11:15:27Z | evaluator | analysis/evaluator-20260609T111527145264Z-c9e0e69a.md | Proceed | High | Run exploit gate and execute solve/solve.py through challenge_exec. |
| 2026-06-09T11:16:04Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-09T11:18:38Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- The remote exposes
/connection_info,/rpc, and/flag. - Raw connection material from
/connection_infois stored underloot/; redacted copies are inanalysis/. Setup.isSolved()requiresVault.mapHolder()to differ from the Vault contract address.Vault.claimContent()changesmap.holderto the caller but requiresisUnlocked.Vault.unlock(bytes16)derives the low 64 password bits from private passphrase storage, recent block hashes, nonce, and block timestamp parity; the high 64 bits must equal the low 64 bits ofowner.- Private storage is readable through JSON-RPC. The passphrase lives in storage slot 2 and
owner/nonceare public. analysis/<password redacted>proves the password formula by runningunlockagainst a fixed historical block context witheth_call.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Blockchain
- Challenge: Magic-Vault
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Audit
Setup.solandVault.sol. - Identify the win condition:
Setup.isSolved()returns true whenVault.mapHolder()no longer equals the Vault address. - Identify the gated state transition:
claimContent()changesmap.holder, but requiresisUnlocked. - Analyze
unlock(bytes16): high 64 password bits must match owner low 64 bits; low 64 bits must match a magic key derived from passphrase, nonce, recent block hashes, and timestamp parity. - Read the private passphrase from Vault storage slot 2 and public owner/nonce through JSON-RPC.
- Validate the password formula against a fixed historical block using
eth_call. - For the live transaction, compute a candidate for the immediately next block, send
unlock, retry if timing misses, then callclaimContent. - Fetch
/flagaftermapHolder()changes to the player.
Reusable Lessons
- Solidity
privatevariables are not secret on public chain state. - Blockhash/timestamp-derived <password redacted> can often be reconstructed if all inputs are public and the target block context can be predicted.
- Fixed-block
eth_callis a good way to validate block-dependent formulas without mutating state. - For transaction-block-dependent secrets, include retry logic because a block may advance between snapshot and
eth_sendTransaction.
Dead Ends
- RAG returned no useful Magic Vault-specific or <password redacted> memory. The solve used current source and live RPC evidence.
- Broad
eth_estimateGasvariant probing was not useful because the chain continued advancing and stale candidates failed.
Tool Quirks
- Local
web3,solc, andfoundrywere unavailable, but PythonrequestspluspycryptodomeKeccak were enough for JSON-RPC and selector/input encoding. - The HTB
/connection_infoprivate key must stay inloot/; analysis copies should be redacted.
Evidence Paths
analysis/source-audit.mdanalysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.mdanalysis/rag/rag-query-20260609T110423051734Z-474e01cb.txtanalysis/rag-records.mdanalysis/<password redacted>analysis/evaluator-20260609T111527145264Z-c9e0e69a.mdanalysis/solve-run-summary.jsonsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Hypothesis Board
Keep no more than 3 active hypotheses on Easy/Medium and 5 on Hard unless the user explicitly asks for breadth.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Audit provided Solidity contracts and remote JSON-RPC instance, identify vault unlock/key ownership flaw, then satisfy Setup.isSolved and capture /flag. | Blockchain Easy hybrid challenge with a source ZIP and remote host <TARGET>:30537; scenario describes a magic vault locked by a key. | Extract contracts, identify Setup.isSolved condition, fetch /connection_info, inspect storage and exposed functions before any state-changing transaction. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Blockchain
- Challenge: Magic-Vault
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Audit
Setup.solandVault.sol. - Identify the win condition:
Setup.isSolved()returns true whenVault.mapHolder()no longer equals the Vault address. - Identify the gated state transition:
claimContent()changesmap.holder, but requiresisUnlocked. - Analyze
unlock(bytes16): <REDACTED>, nonce, recent block hashes, and timestamp parity. - Read the private passphrase from Vault storage slot 2 and public owner/nonce through JSON-RPC.
- Validate the password formula against a fixed historical block using
eth_call. - For the live transaction, compute a candidate for the immediately next block, send
unlock, retry if timing misses, then callclaimContent. - Fetch
/flagaftermapHolder()changes to the player.
Reusable Lessons
- Solidity
privatevariables are not secret on public chain state. - Blockhash/timestamp-derived <password redacted> can often be reconstructed if all inputs are public and the target block context can be predicted.
- Fixed-block
eth_callis a good way to validate block-dependent formulas without mutating state. - For transaction-block-dependent secrets, include retry logic because a block may advance between snapshot and
eth_sendTransaction.
Dead Ends
- RAG returned no useful Magic Vault-specific or <password redacted> memory. The solve used current source and live RPC evidence.
- Broad
eth_estimateGasvariant probing was not useful because the chain continued advancing and stale candidates failed.
Tool Quirks
- Local
web3,solc, andfoundrywere unavailable, but PythonrequestspluspycryptodomeKeccak were enough for JSON-RPC and selector/input encoding. - The HTB
/connection_infoprivate key must stay inloot/; analysis copies should be redacted.
Evidence Paths
analysis/source-audit.mdanalysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.mdanalysis/rag/rag-query-20260609T110423051734Z-474e01cb.txtanalysis/rag-records.mdanalysis/<password redacted>analysis/evaluator-20260609T111527145264Z-c9e0e69a.mdanalysis/solve-run-summary.jsonsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Notes
Notes
Scope
- Challenge: Magic-Vault
- Category: Blockchain
- Difficulty: Easy
- Mode: hybrid
- Remote instance: <TARGET>:30537
- Start time: 2026-06-09T11:02:14Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7370-50c6-4b2e-951d-235daee847f1.zip | 1509 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 3 shown in artifact inventory JSON |
files/extracted/blockchain_magic_vault/Setup.sol | 373 | <hash redacted> | Java source, ASCII text | |
files/extracted/blockchain_magic_vault/Vault.sol | 1600 | <hash redacted> | ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-09T11:02:14Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-09T11:02:26Z | artifact inventory | analysis/artifact-inventory.json | 3 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-09T11: <REDACTED>, identify vault unlock/key ownership flaw, then satisfy Setup.isSolved and capture /flag. | Medium | Extract contracts, identify Setup.isSolved condition, fetch /connection_info, inspect storage and exposed functions before any state-changing transaction. | |||
| 2026-06-09T11:04:14Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-09T11:04:14Z | checkpoint recorded | analysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-09T11:04:33Z | RAG query | analysis/rag/rag-query-20260609T110423051734Z-474e01cb.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-09T11:04:52Z | RAG record | analysis/rag-records.md | Retrieved memory tagged MISSING | Medium | Validate or reject with live evidence |
| 2026-06-09T11:14:13Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-09T11:15:27Z | evaluator | analysis/evaluator-20260609T111527145264Z-c9e0e69a.md | Proceed | High | Run exploit gate and execute solve/solve.py through challenge_exec. |
| 2026-06-09T11: <REDACTED> | |||||
| 2026-06-09T11:18:38Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- The remote exposes
/connection_info,/rpc, and/flag. - Raw connection material from
/connection_infois stored underloot/; redacted copies are inanalysis/. Setup.isSolved()requiresVault.mapHolder()to differ from the Vault contract address.Vault.claimContent()changesmap.holderto the caller but requiresisUnlocked.Vault.unlock(bytes16)derives the low 64 password bits from private passphrase storage, recent block hashes, nonce, and block timestamp parity; the high 64 bits must equal the low 64 bits ofowner.- Private storage is readable through JSON-RPC. The passphrase lives in storage slot 2 and
owner/nonceare public. analysis/<password redacted>proves the password formula by runningunlockagainst a fixed historical block context witheth_call.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Technical analogy
How to remember this solve
Think of the smart contract like a transparent bank ledger with strict but imperfect rules. The trick is to make the rules execute in an order the author did not protect against.
For Magic Vault, keep the mental model simple: identify the trusted assumption, prove it with the smallest safe test, then automate or repeat only the part that directly leads to the flag.