Good Samaritan - The New Ethernaut CTF Challenge

Written by kralizec | Published 2022/09/25
Tech Story Tags: ethernaut | ethernaut-game | ethernaut-game-solution | good-samaritan | ctf | smart-contracts | evm | open-zeppelin-puzzle-guide

TLDREthernaut is a smart contract security CTF game, presented by OpenZeppelin. The first person to win all levels gets awarded a free lambo; no, just kidding, the only awards are the educational and entertainment value gleaned from the exercise. For a while it’s been the same 26 puzzles, each one of which presents a security vulnerability. The game is to find an exploit the vulnerability, then submit proof of having done so in order to pass the level. The new one is called “Good Samaritan” and I will walk you through the problem.via the TL;DR App

If you’re not familiar with Ethernaut, it’s an Ethereum-based smart contract security CTF game, presented by OpenZeppelin. For a long time, it’s been the same 26 puzzles, each one of which presents a smart contract with a security vulnerability. The game is to find and exploit the vulnerability, then submit proof of having done so to pass the level. The first person to win all levels gets awarded a free lambo! Or, the only awards are the entertainment/educational value gleaned from the exercise, I forget which.

Spoiler alert: you can see some of my solutions explained here.

Avoid that link if you plan to solve the problems on your own.

Well, for a while it’s been the same 26 puzzles, but recently I visited the site again and noticed that a new one was added:

The new one is called “Good Samaritan”, and (again spoiler alert) I will walk you through the problem and the solution.

The Problem

The problem is explained succinctly here, on the Ethernaut website.

There is a network of three smart contracts, comprising a Coin (a very simplified version of an ERC20 token), a Wallet (holds the coin/token), and a front-end contract for donating 10 of the token upon request. This front-end contract owns the wallet which owns a large quantity of the token. The condition for winning is to rob the entire contents of the GoodSamaritan’s wallet.

The Solution

So, there is actually nothing stopping you from just requesting 10 tokens over and over, except that it will require 100,000 separate requests, will take forever and will cost a ton in gas fees. If you did it this way, I imagine that you would be declared a winner. But that wouldn’t be fun and would miss the point of the challenge.

There’s a second way, just using a plain old reentrancy attack. But I won’t get into the details of this, as it’s still too time-consuming and costly. It’s basically the same thing as repeatedly requesting, but with slightly fewer steps.

Finally, the real solution (as the Ethernaut description indicates) has to do with the newer way of throwing errors.

So, we have a call stack that goes from

GS.requestDonation() → Wallet.donate10() → Coin.transfer()

Examining this, it’s pretty unremarkable; on examination, there’s no obvious hook to be seen, whereby one could disrupt the process or inject some malicious functionality. However, if the caller happens to be another contract, then the chain is like this:

GS.requestDonation() → Wallet.donate10() → Coin.transfer() → INotifyable.notify

That last step is clearly the opportunity to disrupt the expected flow. The INotifyable.notify() is an optional hook back to the caller (the calling contract). This is essentially allowing you, the caller, to inject code into the process, right at that point. You code and deploy your own contract to act on your behalf, and execute custom code at that exact spot. So, what code to execute?

Well, the first thing that comes to mind is reentrancy, the core of so many attacks in this game (and in real life). Looking at the Coin.transfer function, we can see that it follows the checks-effects-interactions pattern to defend against reentrancy attacks. It has its checks:

if(amount_ <= currentBalance)

Followed by its effects:

balances[msg.sender] -= amount_;
balances[dest_] += amount_;

and finally, interactions:

INotifyable(dest_).notify(amount_);

Now, the trick is that we know one thing: in the GoodSamaritan contract, the result of a NotEnoughBalance error being caught, is that the GoodSamaritan will dump its entire wallet contents onto the caller!

} catch (bytes memory err) {
      if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) {
           // send the coins left
           wallet.transferRemainder(msg.sender);
           return false;
      }
}

Hmmm… it seems that the way to get that code to execute, would be to simply cause a NotEnoughBalance error to be thrown. And since the process is allowing us to execute our own custom code…. well there it is.

You can see and download my complete solution here. The solution itself is visible in the execute script and the unit tests. The custom contract used in the solution is here.

Conclusion

There are other smart contract CTFs. What I don’t like about Ethernaut’s is that I feel that they do a bit too much to ‘feed’ you the solution. It would be nice to see a more realistic scenario that contains more complexity and ‘noise’ - a bunch of details that are not related to the solution, that might serve to distract one from the real solution. Which is what you’d see in the real world.

What I do like about Ethernaut is that IMO they do a good job in covering many of the most crucial aspects of EVM smart contract security. I’d definitely recommend Ethernaut for someone who is already fairly well-versed in smart contract development, but who is maybe wanting to expand their knowledge of security issues. Also, these exercises are a fun challenge that you can do within a few spare hours, in most cases.


Written by kralizec | An explorer by nature, by art as well as by nature.
Published by HackerNoon on 2022/09/25