Ethernaut#2 - Fal1out

Ethernaut #2 - Fal1out

#2

Claim ownership of the contract below to complete this level.

Things that might help

  • Solidity Remix IDE
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import 'openzeppelin-contracts-06/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];
}
}

Contract Breakdown

Let us breakdown the contracts to understand what each function and piece of code does. The code consists of a single contract Fallout that is compiled with solidity version ^0.6.0.

1
import "openzeppelin-contracts-06/math/SafeMath.sol";

The contract imports the OpenZeppelin SafeMath library on line 4 to address overflow and underflow issues that were prevalent in earlier versions of Solidity before version 0.5.0.

Constructor
1
2
3
4
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

Defining a constructor like this might look strange but in earlier versions of Solidity prior to 0.4.3, the constructor function could only be set by naming it the same as the contract, and it would be initialized upon contract deployment.
In the constructor, the owner is assigned to the msg.sender, who is typically the contract deployer, and the owner’s allocation is set to the msg.value.

Did you notice anything wrong with the function name? we’d get back to it soon.

Functions

In this contract, there are 4 functions and 1 function modifier. Let’s take a look at them one after the other.

1
2
3
4
5
6
7
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}

The onlyOwner modifier requires that the caller be the owner of the contract. When this modifier is applied to a function, it can only be executed by the contract’s owner. The requirement is enforced through a require statement that checks if the msg.sender is equal to the owner, and if not, it throws an error with the message “caller is not the owner”.

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

This function is used to set allocations of msg.sender to the amount set, msg.value.

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

The sendAllocation function is basically used to withdraw funds back to the corresponding allocator. The function takes in an address and checks if the allocations of that address is > 0 before transfering their balance back to them on line 3 above.

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

This collectAllocations function can only be called by the owner of the contract and this sends all of the funds in the contract to the owner.

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

The last function allocatorBalance gets passed an address and returns the allocation of that address.

Solution

To hack this contract, we need to find a way to set the owner variable to our address. Inspecting the contract, owner is only updated once, and that is in the constructor, which is supposed to be called on contract deployment. However, the constructor function isn’t called.

WHY?

Examining the code, we can see that the contract’s name i Fallout. However, the constructor function is named Fal1out, with an erroneous ‘1’ instead of an ‘l.’ As a result, when the contract is deployed, the constructor function won’t be initiated, and the “owner” variable will not be set. This transforms the fal1out function into a publicly accessible function that can be called by anyone.

To pass this challenge, all we need to do is call the fal1out function. We can easily do this a number of ways.

Method 1.
  • Copy the code to Remix and compile it.
  • Select Environment and choose injected provider then select the network it was deployed on (goerli at the time of writing this).
  • Load the contract from the instance address provided by inspecting the page on the Ethernaut challenge page and copy it from the console.
  • Remix provides tools to interact with contracts on the left sidebar, here we can call the fal1out function directly.
Method 2.
1
2
3
4
5
6
7
contract HackFal1out {
Fallout fallout;
constructor(address HackAddress) public{
fallout = Fallout(HackAddress);
fallout.Fal1out();

}

Another way to externally interact with the fal1out contract is to deploy a new contract HackFal1out, pass it the fallout contract address (ethernaut refers to this as instance address) and use that to initialize an instance of this contract. This instance can then be used to externally call functions on the fal1out contract

Method 3.

We could also interact with the contract by declaring an interface IHackFallout. This interface will consist of the Fallout function(s) definitions we want to interact with without implementation/function body. In our case we only want to interact with the Fal1out function so our interface will look like this.

1
2
3
Interface Fallout{
function Fal1out() external payable;
}
  • Compile this interface on Remix.
  • Copy the instance address from ethernaut.
  • Paste into remix and load the contract at the address.
  • Call the Fal1out function and confirm the transaction from your wallet.

To sum up, this is no longer considered a significant vulnerability as the definition of constructors has changed in more recent versions of Solidity (0.5.0 and above). However, it remains important to have knowledge of this issue.