Intro: Challenge 1
Sjors Provoost has put up the original challenge with the intent “to hone [his] skills at writing secure smart contract code.” He funded the contract with ~ 9.5 ETH for there to be a real bounty for completing the challenge (i.e. hacking it). The code is simple and doesn’t leave much to imagination, though.
…It even keeps the money if the receiving end is another dapp that does, well, almost anything. That has become “classic” fast, in both theoretical attack vectors, and practical malicious ones.
However, it could also be normal for a well-meaning dapp. These
days, it’s a wallet. But say you’re a self-flying postal drone
trying to withdraw the client’s tokens to pay for electricity, and
doing some accounting in the process. Remember, as a drone you don’t
get to send your own transactions, and might as well piggy-back on
client-initiated transactions (if you know the token you’re withdrawing
uses call()
instead of send()
).
Vulnerable: Challenge 2
Anyway, Sjors put another challenge up, and funded it with ~ 10 ETH, that should allow for described behaviour. In that, he had this:
DaoChallenge (segment)
:
In the Ethereum language Solidity, all functions are public
by default.
public
functions can be called by anyone - that is, any account.
The obvious way to sweep this contract would then be to
start an Ethereum client, load the contract’s ABI, and call
withdrawEtherOrThrow(...)
with the desired amount.
The day before I found a possible 50/50 drain vector in
Peter Borah’s TokenWithInvariants
example; I couldn’t provide
code then, since I didn’t know the dapp deployment procedure, and
had done zero actual Solidity programming. This time, I decided I’d
write a contract to do the sweep. Here it is, in the form it’s been
deployed onto the blockchain.
chal2sweep.sol
:
The interesting part is, of course, the fallback function (the nameless one). It should have worked like this:
- I send a transaction from account
noel
with anything but 10 ether attached to it. chal2sweep
makes a call to the vulnerableDaoChallenge
method and requests a withdrawal of 10 ether.DaoChallenge
honours the request and sends it tomsg.sender
, which is againchal2sweep
.- That hits the fallback function of
chal2sweep
again, but since the amount is exactly 10 ether, the body is not executed.
The code is also available in this repo, along
with a simple script to do deployment copy-pasting into
geth
.
Any updates I do to it will be pushed there. Of those, I can immediately think of:
- being able to change the target address - although this is kind of useless without being able to specify the target’s vulnerable function, and perhaps the amount;
- using the
only_noel
modifier instead ofmsg_value_not(...)
- after all, I only want my calls to result in further malicious calls.
Oops
I never got to test if that code works as intended, because someone
beat me to it. In particular, I never got to test if
chal.call("withdrawEtherOrThrow", 10000000000000000000)
is the correct
syntax.
It was somewhat disappointing that some schmuck, who probably even wouldn’t share his exploit, beat me to it by less than 30 minutes. (I assumed it was a “schmuck”, since they didn’t come public immediately after - not on Medium, Reddit, or GitHub.)
It turned out, however, that it was Sjors himself. He was working on a new challenge, and remembered he forgot something in the old one.
Fallout
In retrospect, a de-facto bug bounty is much motivation for entry-level people like myself. No one in their right minds would hire us for fixed-salary code audit, but money on the line is good enough for all-or-nothing, learn-as-you-go activities.
I might just fire up a testnet node again to test all my sloppy code. And other folk’s, too.