Ethernaut #2: Fallback


¡Bienvenidos a la segunda entrega de nuestra serie sobre Ethernaut! Si te perdiste la primera parte, ¡la puedes encontrar aquí!.

Hoy, realizaremos el segundo desafío: explotar la función receive para hacerse con el control de un contrato.


¿Qué necesitas saber?

Funciones especiales en Solidity:

  • receive(): Se ejecuta cuando el contrato recibe ETH sin datos (ej: una transferencia normal).
  • fallback(): Actúa como “plan B” para llamadas que no existen en el contrato (repasamos esto en el post anterior).

Anatomía del contrato vulnerable

contract Fallback {
 mapping(address => uint256) public contributions;
 address public owner;

 constructor() {
  owner = msg.sender;
  contributions[msg.sender] = 1000 ether; 
 }

 function contribute() public payable {
  require(msg.value < 0.001 ether); 
  contributions[msg.sender] += msg.value;
  if (contributions[msg.sender] > contributions[owner]) {
   owner = msg.sender; 
  }
 }

 receive() external payable {
  require(msg.value > 0 && contributions[msg.sender] > 0);
  owner = msg.sender;
 }
}

Puntos débiles identificados

  1. La función receive es demasiado permisiva:

    • Solo exige que el remitente haya contribuido antes (contributions > 0) y que envíe algún wei.
    • No hay límite en msg.value, a diferencia de contribute().
  2. Falsa sensación de seguridad del owner inicial:

    • La lógica asume que nadie podrá superar ese valor porque contribute() limita las donaciones a < 0.001 ether.


El ataque paso a paso

1. Contribuir con lo mínimo

await contract.contribute({ value: toWei("0.0005") });
  • Objetivo: Registrar tu dirección en contributions con un valor > 0.
  • Explicación: 0.0005 ether es menor al límite de 0.001 ether, así que el require no se queja.

2. Aprovechar la función receive para robar el ownership

📰

La función sendTransaction es una funcion de utilidad que han dejado los desarrolladores de Ethernaut. No es parte de la especificación del contrato.

sendTransaction({ 
 from: player,
 to: contract.address,
 value: toWei('0.0011')
});
  1. Envías ETH directamente al contrato (sin llamar a contribute).
  2. Se activa receive(), que comprueba:
    • msg.value > 0 ✔️ (0.0011 cumple).
    • contributions[msg.sender] > 0 ✔️ (gracias al paso 1).
  3. ¡BAM! owner = msg.sender. Ahora tú controlas el contrato[[1]][[7]].