Challenge / Blockchain

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

EasyPublished 2024-02-07Sanitized local writeup

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.

Magic Vault sanitized attack graph

Walkthrough flow

01

Audit Setup.sol and Vault.sol.

02

Identify the win condition: Setup.isSolved() returns...

03

Identify the gated state transition: claimContent()...

04

Analyze unlock(bytes16): high 64 credential bits must...

05

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.

100% coverage
Evidence verdict

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:

solidity
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 marked private
  • nonce()
  • blockhash(block.number - 1) or blockhash(block.number - 2), depending on timestamp parity
  • owner()

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:

  1. Fetch /connection_info and store raw connection material under loot/, with redacted analysis copies.
  2. Read owner(), nonce(), mapHolder(), isUnlocked(), and passphrase storage slot 2.
  3. Fetch the latest block once and compute the candidate unlock input for the next block using the latest block hash and parent hash.
  4. Send unlock(bytes16).
  5. Retry if the transaction misses the expected block context.
  6. Call claimContent().
  7. Confirm mapHolder() changed to the player.
  8. Request /flag and store the response in loot/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 private storage 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_call is useful for proving byte-casting and blockhash formulas without mutating state.
  • Keep /connection_info private keys and raw flags in loot/, with redacted copies in analysis/.

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

FileSizeSHA256TypeNotes
files/a12c7370-50c6-4b2e-951d-235daee847f1.zip1509<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 3 shown in artifact inventory JSON
files/extracted/blockchain_magic_vault/Setup.sol373<hash redacted>Java source, ASCII text
files/extracted/blockchain_magic_vault/Vault.sol1600<hash redacted>ASCII text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-09T11:02:14Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-09T11:02:26Zartifact inventoryanalysis/artifact-inventory.json3 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-09T11:02:26Zhypothesis recordedhypothesis-board.mdAudit provided Solidity contracts and remote JSON-RPC instance, identify vault unlock/key ownership flaw, then satisfy Setup.isSolved and capture /flag.MediumExtract contracts, identify Setup.isSolved condition, fetch /connection_info, inspect storage and exposed functions before any state-changing transaction.
2026-06-09T11:04:14Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-09T11:04:14Zcheckpoint recordedanalysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-09T11:04:33ZRAG queryanalysis/rag/rag-query-20260609T110423051734Z-474e01cb.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-09T11:04:52ZRAG recordanalysis/rag-records.mdRetrieved memory tagged MISSINGMediumValidate or reject with live evidence
2026-06-09T11:14:13Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-09T11:15:27Zevaluatoranalysis/evaluator-20260609T111527145264Z-c9e0e69a.mdProceedHighRun exploit gate and execute solve/solve.py through challenge_exec.
2026-06-09T11:16:04Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-09T11:18:38Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • The remote exposes /connection_info, /rpc, and /flag.
  • Raw connection material from /connection_info is stored under loot/; redacted copies are in analysis/.
  • Setup.isSolved() requires Vault.mapHolder() to differ from the Vault contract address.
  • Vault.claimContent() changes map.holder to the caller but requires isUnlocked.
  • 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 of owner.
  • Private storage is readable through JSON-RPC. The passphrase lives in storage slot 2 and owner/nonce are public.
  • analysis/<password redacted> proves the password formula by running unlock against a fixed historical block context with eth_call.

RAG / Advisory Memory

RAG output is advisory only. Record evaluated retrievals with:

bash
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.

  1. Audit Setup.sol and Vault.sol.
  2. Identify the win condition: Setup.isSolved() returns true when Vault.mapHolder() no longer equals the Vault address.
  3. Identify the gated state transition: claimContent() changes map.holder, but requires isUnlocked.
  4. 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.
  5. Read the private passphrase from Vault storage slot 2 and public owner/nonce through JSON-RPC.
  6. Validate the password formula against a fixed historical block using eth_call.
  7. For the live transaction, compute a candidate for the immediately next block, send unlock, retry if timing misses, then call claimContent.
  8. Fetch /flag after mapHolder() changes to the player.

Reusable Lessons

  • Solidity private variables 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_call is 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_estimateGas variant probing was not useful because the chain continued advancing and stale candidates failed.

Tool Quirks

  • Local web3, solc, and foundry were unavailable, but Python requests plus pycryptodome Keccak were enough for JSON-RPC and selector/input encoding.
  • The HTB /connection_info private key must stay in loot/; analysis copies should be redacted.

Evidence Paths

  • analysis/source-audit.md
  • analysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.md
  • analysis/rag/rag-query-20260609T110423051734Z-474e01cb.txt
  • analysis/rag-records.md
  • analysis/<password redacted>
  • analysis/evaluator-20260609T111527145264Z-c9e0e69a.md
  • analysis/solve-run-summary.json
  • solve/solve.py
  • loot/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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Audit 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.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit 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.

  1. Audit Setup.sol and Vault.sol.
  2. Identify the win condition: Setup.isSolved() returns true when Vault.mapHolder() no longer equals the Vault address.
  3. Identify the gated state transition: claimContent() changes map.holder, but requires isUnlocked.
  4. Analyze unlock(bytes16): <REDACTED>, nonce, recent block hashes, and timestamp parity.
  5. Read the private passphrase from Vault storage slot 2 and public owner/nonce through JSON-RPC.
  6. Validate the password formula against a fixed historical block using eth_call.
  7. For the live transaction, compute a candidate for the immediately next block, send unlock, retry if timing misses, then call claimContent.
  8. Fetch /flag after mapHolder() changes to the player.

Reusable Lessons

  • Solidity private variables 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_call is 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_estimateGas variant probing was not useful because the chain continued advancing and stale candidates failed.

Tool Quirks

  • Local web3, solc, and foundry were unavailable, but Python requests plus pycryptodome Keccak were enough for JSON-RPC and selector/input encoding.
  • The HTB /connection_info private key must stay in loot/; analysis copies should be redacted.

Evidence Paths

  • analysis/source-audit.md
  • analysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.md
  • analysis/rag/rag-query-20260609T110423051734Z-474e01cb.txt
  • analysis/rag-records.md
  • analysis/<password redacted>
  • analysis/evaluator-20260609T111527145264Z-c9e0e69a.md
  • analysis/solve-run-summary.json
  • solve/solve.py
  • loot/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

FileSizeSHA256TypeNotes
files/a12c7370-50c6-4b2e-951d-235daee847f1.zip1509<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 3 shown in artifact inventory JSON
files/extracted/blockchain_magic_vault/Setup.sol373<hash redacted>Java source, ASCII text
files/extracted/blockchain_magic_vault/Vault.sol1600<hash redacted>ASCII text

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-09T11:02:14Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-09T11:02:26Zartifact inventoryanalysis/artifact-inventory.json3 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-09T11: <REDACTED>, identify vault unlock/key ownership flaw, then satisfy Setup.isSolved and capture /flag.MediumExtract contracts, identify Setup.isSolved condition, fetch /connection_info, inspect storage and exposed functions before any state-changing transaction.
2026-06-09T11:04:14Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-09T11:04:14Zcheckpoint recordedanalysis/checkpoint-analysis-20260609T110414927113Z-00bdb3cb.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-09T11:04:33ZRAG queryanalysis/rag/rag-query-20260609T110423051734Z-474e01cb.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-09T11:04:52ZRAG recordanalysis/rag-records.mdRetrieved memory tagged MISSINGMediumValidate or reject with live evidence
2026-06-09T11:14:13Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-09T11:15:27Zevaluatoranalysis/evaluator-20260609T111527145264Z-c9e0e69a.mdProceedHighRun exploit gate and execute solve/solve.py through challenge_exec.
2026-06-09T11: <REDACTED>
2026-06-09T11:18:38Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • The remote exposes /connection_info, /rpc, and /flag.
  • Raw connection material from /connection_info is stored under loot/; redacted copies are in analysis/.
  • Setup.isSolved() requires Vault.mapHolder() to differ from the Vault contract address.
  • Vault.claimContent() changes map.holder to the caller but requires isUnlocked.
  • 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 of owner.
  • Private storage is readable through JSON-RPC. The passphrase lives in storage slot 2 and owner/nonce are public.
  • analysis/<password redacted> proves the password formula by running unlock against a fixed historical block context with eth_call.

RAG / Advisory Memory

RAG output is advisory only. Record evaluated retrievals with:

bash
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.