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
| 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是等价的!
修复建议:
移除 receive()
逻辑
或
在 receive()
内添加 onlyOwner
限制:
1 2 3
| receive() external payable onlyOwner { require(msg.value > 0); }
|
严格限制 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; } }
|
在 receive()
函数中加入额外验证,避免 owner
被轻易修改。