Another day another CTF!
A new NFT has appeared, Alpha Goat Club. You are not on the presale and the public sale is not open. Your goal is to mint an NFT for yourself.
This was a contract deployed on Polygon:
0xc80fC50b697b20F2F0a1Cef247D77bC851620B07. It appears to be a simple NFT contract, should be an easy job to hack it! I immediately checked exposed mint functions. There were two:
mint function immediately because
publicSale variable was set to false, and only the owner could set it to true. This leaves us with one function:
exclusiveBuy. Before looking for solutions first I did a
commit transaction so that
alreadyComitted modifier does not revert. The commit-then-submit scheme was supposedly for preventing frontrunning the solutions. After that was out of the way, I started looking for the solution.
exclusiveBuy function it was easy to recognize that we only had to pass the
require(matchAddressSigner(hash_, signature) check. This check simply ensures that the
signer had signed the
signer address was a state variable set as
Pinpointing the vulnerability
The first thing I did was to check the
diff of the ECDSA library used in the contract against the official one from the OpenZeppelin contracts repository. The library was not tampered with. Then I started reading the library to figure out if I might be overlooking something. Then I came accross the comments by the OpenZeppelin team, clearly laying out the vulnerability of this puzzle.
There was the answer: [I]t is possible to craft signatures that recover to arbitrary addresses for non-hashed data.
The first mint
Now, before we go over how I went about to craft a signature for an arbitrary address, I will briefly mention the easier solution. While the solution I was going for was practically infinitely replicable, there was one easy solution that could be only used once. This was simply to find an existing signature of
0x000000097C7e6f43bb3f225DB275B22C666402f1 in the wild, and submit that. Checking this address on Polygon, one can see that this address created the AlphaGoatClub NFT contract. From that transaction data, one could get the message hash and its signature, and simply use that to mint through
exclusiveBuy. This trick could only be used once, because this is the only message-signature pair we know of the address, and the
exclusiveBuy function ensures a signature cannot be used twice.
Cracking unlimited minting
Back to crafting signatures that recover to arbitrary addresses.
After some websearching, I came accross an article about the “Faketoshi Signature”. After confirming that Bitcoin and Ethereum use the exact same ECDSA scheme, I was certain that I found the solution. This was quite early in the CTF, however I had to now put all these into practice. Working with ECDSA is hard if you are not already experienced. Because when you check that a signature does not recover to the address you intended, you only get a different address. You do not get an error message telling you what you did wrong. So figuring it all out to write necessary scripts took some time.
Getting the public key
I realized I needed the public key, and not the public hash. Public key was necessary to do any work with ECDSA. I learned that we can recover public key from a signature. This makes sense, I assume operations like
ecrecover first recover the public key then convert that to 160-bit Ethereum address. As I previously mentioned we only had one signature to work with, and that was from the contract create transaction. I slightly modified a script by Vlad Faust for this purpose.
Running it after setting Polygon as the Hardhat network returned the public key:
Crafting the signature
Well, for this purpose I once again used someone else’s script after slightly modyfing it. This time a script by David Burkett.
I would have loved to explain how this works, but I need to do some serious learning before I am comfortable teaching this to others. So I will leave this ChatGPT drivel here instead.
The Python script uses the properties of ECDSA to create a message and signature combination that recovers to a chosen Ethereum address. Given a public key `Q`, we generate random nonzero values `a` and `b`. We then compute `K` as the sum of `a*G` and `b*Q`, where `G` is the generator point of the elliptic curve, resulting in a new point `K` on the curve. We use the x coordinate of the point `K` as the signature component `r`. To compute the other signature component `s`, we multiply `r` by the modular inverse of `b` with respect to the curve's order, denoted as `b_inv`. The pair (r, s) forms an ECDSA signature for the "message" given by `(((a * r) % n) * b_inv) % n`, where `n` is the order of the elliptic curve. It's important to note that deriving a message and signature combination that recovers to an arbitrary address doesn't compromise the security of the Ethereum network. Crafting such a combination doesn't enable an attacker to access the private key or sign transactions for the targeted address. Instead, it demonstrates the flexibility and mathematical properties of the ECDSA.
With that, we can conclude today’s CTF write up. Thanks to RareSkills for this challenge, and congratulations to ChainLight for another first blood.
And here is my cool goat NFT.