Ethernaut_刷题小记(一)

Ethernaut_刷题小记(一)

本人所有题目是把它拉到Remix VM里面做的,效果都是一样。

拿到合约代码,目标是要求获得Fallback合约的所有权,并将其余额减少到0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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;
}
}

审计这个代码,我们可以看到有两个函数涉及到了owner的分配

1
2
3
4
5
6
7
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
1
2
3
4
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}

第一个成为owner的条件是要搞到1000eth(contributions[msg.sender] = 1000 * (1 ether);)。。。。显然有点。。。十分不可能!!

而第二个,就是只要存款大于0就能成为owner,那我们固然选择后者

选择后者就是看怎么样调用它了

Solidity文档中对receive的描述

一个合约最多可以有一个 receive 函数,声明为 receive() external payable { ... } (不带 function 关键字)。 该函数不能有参数,不能返回任何内容,必须具有 external 可见性和 payable 状态可变性。 它可以是虚拟的,可以重写,并且可以有 修改器modifier。

接收函数在调用合约时执行,且没有提供任何 calldata。

对fallback函数的描述

一个合约最多可以有一个 fallback 函数,声明为 fallback () external [payable]fallback (bytes calldata input) external [payable] returns (bytes memory output) (两者均不带 function 关键字)。

该函数必须具有 external 可见性。回退函数可以是虚拟的,可以重写,并且可以有修改器。

如果没有其他函数与给定的函数签名匹配,或者根本没有提供数据且没有 接收以太函数,、则在调用合约时执行回退函数。 回退函数始终接收数据,但为了接收以太币,它必须标记为 payable

而函数withdraw()是可以清空余额的

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

那我们的思路就很明了了,先用contribute,给点当前用户余额,然后我们设置一个不为0的value的值,再将calldata置为空发送交易就可以让owner变成我们了~,再调用withdraw函数清空,这关就挑战完成了

那你可能会有一个疑惑,为什么没有用到fallback函数,但是有点文章却说fallback是考点呢?其实最初版本是用到的下面的这个源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;

constructor() public {
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 {
owner.transfer(address(this).balance);
}

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

就是利用的fallback函数哦~,在calldata为空的情况下,fallback和receive是等价的!

修复建议

  1. 移除 receive() 逻辑

    receive() 内添加 onlyOwner 限制:

    1
    2
    3
    receive() external payable onlyOwner {
    require(msg.value > 0);
    }
  2. 严格限制 owner 更换条件,例如:

    1
    2
    3
    4
    5
    6
    7
    function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if (contributions[msg.sender] > contributions[owner] && msg.sender != owner) {
    owner = msg.sender;
    }
    }
  3. receive() 函数中加入额外验证,避免 owner 被轻易修改。


Ethernaut_刷题小记(一)
https://eznp.github.io/2025/03/12/Ethernaut-刷题小记(一)/
作者
Zer0
发布于
2025年3月12日
许可协议