Ethernaut walkthrough [#3]
This is the third part of the Ethernaut challenges walkthrough.
As previously mentioned, the solutions could be found here:
however, I suggest trying to solve the challenges first as the posts are structured in a walkthrough fashion (with hints). Of course, if you feel stuck at any moment, look at my interpretations and try again yourself.
Level 8 — Vault
This challenge is a great example that nothing is “hidden” nor “private” on the blockchain. Although both variables,
password, are marked as private, their values are in the contract’s state. However, you can’t just call the contract instance and variable, the contract’s storage has to be accessed.
To understand exactly how different state variables are positioned in the storage, read the Solidity documentation.
As for how to access that storage, maybe this web3 function could help.
So, take a peek at the
password state variable and then invoke
unlock() with it.
Level 9 — King
When approaching this level, don’t be discouraged by the stated difficulty (6/10) rather analyze what you’re given. As a hint, your end goal is to simulate a Denial-of-Service (DoS) attack. That way, you essentially block the contract and no entity can use it afterward.
How do you achieve that? Well, look at the contract and what’s the only way to interact with it. That would be the
fallback() function. Hence you have to send some ether to become the
king. The next step is to find out how the contract interacts with the outside world because you obviously can’t block it only by sending ether to it. Did you see it?
king.transfer(msg.value); this line right here.
By now you might have a clue what the game plan is. Create a contract, send some money from it to the challenge contract so your deployed contract is now the
king, and make sure to include some logic that doesn’t allow ether to be sent to your contract via the
transfer() function. This ensures that whenever some address after you sends more ether than your contract, the
transfer() will fail/revert and the challenge contract will become unusable.
There are two ways to achieve this:
- In the
receive()function of your contract, call
revert(). In my opinion, this is a more obvious exploit contract.
- Have neither a
receive(), nor a
fallback()function. This way, any ether sent to your contract’s address with cause the transaction to fail.
Load up Remix and try out both approaches. Make sure to enter the
msg.value in the Remix UI itself!
Level 10 — Re-entrancy
Ah, one of the spookiest terms that you will always hear or read when starting out with Solidity — REENTRANCY. This is for a reason, as the DAO hack is something that shouldn’t be forgotten and we should learn from past mistakes.
Building upon the previous challenge, try to spot the weak spot in the contract. Where does it interact with the outside world? I’m sure you saw it from the beginning:
(bool result, bytes memory data) = msg.sender.call.value(_amount)("");
How could you take advantage of that?
Take a closer look at the whole
withdraw() function flow.
You call it with some value => the contract checks that the value doesn’t exceed your balance => it sends you the value and asserts that the transaction was successful => after that, the contract decrements your balance by the amount withdrawn.
If it’s not clear, read the previous paragraph again, while keeping in mind that any ether transfer is a potential possibility for the recipient to call some function.
One last approximate analogy for the exploit of this contract. Imagine that you enter a 1-Dollar Store with $1 worth of store credit, you take an item that costs 1 dollar or less and go to the counter to pay. You use your store credit and before the cashier hands you the receipt (and maybe change) back, you tell the cashier: “Actually, I’d like another item and please use the initial $1 store credit as payment again”. You continue doing that until the store has no items left. This is what the challenge contract allows a malicious user to do.
Now go and try to build an attack contract that calls the challenge contract’s
withdraw() function when receiving ether.
Also, keep in mind that this attack will be more expensive than the previous ones as it includes several transactions within one function call, meaning more gas than usual. Make sure to provide enough gas that it succeeds.
Hopefully, my hints helped you along the way. Maybe you jumped straight to the solutions repo, then came back to make sure you got everything right. Either way, the important thing is to have understood the topics and the issues that made the exploits possible. As always, feedback is highly appreciated :)