8 minutes
Solving RareSkills AlphaGoatClub CTF
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.
The scouting
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:
|
|
|
|
I overruled 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.
From the 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 _hash
.
|
|
The signer
address was a state variable set as 0x000000097C7e6f43bb3f225DB275B22C666402f1
.
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.
End
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.