$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Solidity contract with private encrypted flag and hash, need to call talk() with correct key. Solution: Read private storage via eth_getStorageAt, find successful transaction via Voice(5) event, extract XOR key from transaction input data.
As Alex and the group journeyed towards the secret treasure, they noticed they were being followed. They decided to investigate and found a rival group with the second key to the treasure. Alex watched as the other group discussed their plans and listened closely, trying to learn as much as she could about their strengths. She knew they had to get the key if they wanted to reach the treasure and was determined to outsmart the rival group.
Goal: Call talk() with the correct key to set solver = msg.sender.
Setup.sol — standard setup, deploys Rivals with an encrypted flag and its hash. Checks solution via isSolved().
Rivals.sol — main contract:
contract Rivals { event Voice(uint256 indexed severity); bytes32 private encryptedFlag; bytes32 private hashedFlag; address public solver; constructor(bytes32 _encrypted, bytes32 _hashed) { encryptedFlag = _encrypted; hashedFlag = _hashed; } function talk(bytes32 _key) external { bytes32 _flag = _key ^ encryptedFlag; if (keccak256(abi.encode(_flag)) == hashedFlag) { solver = msg.sender; emit Voice(5); } else { emit Voice(block.timestamp % 5); } } }
private doesn't mean secret. Variables encryptedFlag and hashedFlag are declared as private, but everything on the blockchain is public — they can be read via eth_getStorageAt.
XOR encryption is reversible. _flag = _key ^ encryptedFlag. If we know _key and encryptedFlag, we can recover _flag.
Event Voice(5) is a success marker. On failure, Voice(block.timestamp % 5) is emitted (severity 0-4), on success — Voice(5). This allows finding the successful transaction among all others.
Task theme is a hint. "Honor Among Thieves" + "spy on rivals" = eavesdrop on competitors' transactions on the blockchain.
curl http://154.57.164.64:31516/connection_info
Received: PrivateKey, Address, TargetAddress, SetupAddress, RPC endpoint.
# Slot 0: encryptedFlag enc = w3.eth.get_storage_at(TARGET, 0) # 0xcf28038644355028a3d6a7a1c5cd3a8dd7df491160c92bd54ad1d25030af72a0 # Slot 1: hashedFlag hashed = w3.eth.get_storage_at(TARGET, 1) # 0x05fc60acafd59adc52ad54afa3bfc0d926b8360fed5699385482e864a9333992a # Slot 2: solver (already non-zero — someone already solved it!) solver = w3.eth.get_storage_at(TARGET, 2)
Since someone already solved the task (solver != 0), their transaction with the correct key exists on the blockchain.
# Get all Voice events from the contract logs = w3.eth.get_logs({ 'fromBlock': 0, 'toBlock': 'latest', 'address': TARGET }) # Look for Voice(5) — success marker for log in logs: severity = int(log['topics'][1].hex(), 16) if severity == 5: print(f"SUCCESS TX: {log['transactionHash'].hex()}") # Block 0x1b, TX: 0xd320c1f64f887972476c89f0ef6eff1e195bc6ce1e39a5f666d9ecbc664f81ed
tx = w3.eth.get_transaction(success_tx_hash) input_data = tx['input'].hex() # 0x52eab0fa + 877c41fd20053e1ffce19390ae920bbce2e87a7f3ffe1b8a79a7e13e079a53dd # selector key parameter (bytes32) key = bytes.fromhex("877c41fd20053e1ffce19390ae920bbce2e87a7f3ffe1b8a79a7e13e079a53dd")
enc_flag = bytes.fromhex("cf28038644355028a3d6a7a1c5cd3a8dd7df491160c92bd54ad1d25030af72a0") flag_bytes = bytes(a ^ b for a, b in zip(key, enc_flag)) # b'HTB{d0n7_741k_11573n_70_3v3n75!}\x00...' print(flag_bytes.rstrip(b'\x00').decode()) # HTB{d0n7_741k_11573n_70_3v3n75!}
#!/usr/bin/env python3 from web3 import Web3 RPC_URL = "http://154.57.164.64:31516/rpc" PRIVATE_KEY = "0xd0562d94ecd3bbbaf8fa9d4988c4d5a2fc71a1f3e4f738b4627c41b6b1711f44" MY_ADDRESS = "0x8550a946EFBf9580EA01db71558721AFC5e32B0a" TARGET = "0xfCa2a7b6A15a4609c3f54566fDfdF2FFE3dde1C0" w3 = Web3(Web3.HTTPProvider(RPC_URL)) # Key extracted from competitor's successful transaction key = bytes.fromhex("877c41fd20053e1ffce19390ae920bbce2e87a7f3ffe1b8a79a7e13e079a53dd") # talk(bytes32) selector = 0x52eab0fa calldata = bytes.fromhex("52eab0fa") + key nonce = w3.eth.get_transaction_count(MY_ADDRESS) tx = { 'to': TARGET, 'from': MY_ADDRESS, 'gas': 100000, 'gasPrice': w3.eth.gas_price, 'nonce': nonce, 'data': calldata, 'chainId': w3.eth.chain_id, } signed_tx = w3.eth.account.sign_transaction(tx, PRIVATE_KEY) tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"TX status: {receipt['status']}") # 1 = success
Transaction successful, solver set to our address, /flag returns the flag.
Use this technique when:
private variables — they are always readable via eth_getStorageAtprivate in Solidity is an access modifier at the contract level (other contracts cannot call the getter). But all data is stored in the public state trie and can be read via RPC:
# Slot 0 = first variable, Slot 1 = second, etc. w3.eth.get_storage_at(contract_address, slot_number)
Indexed event parameters are stored in topics (up to 3 indexed + topic[0] = event signature). Non-indexed parameters are in data. Filtering by topics allows quickly finding the needed transactions.
For simple variables (not mappings/arrays):
encryptedFlag)hashedFlag)solver)$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar