Egis-Security / CTF_Challenge

Repository containing CTF challenges from nmirchev8, deth and bOgO.
14 stars 8 forks source link

b0g0_ctf - Reentrancy Bug-- Reentrancy Vulnerability in withdraw Function of BuggyNFTVault Contract** #4 #11

Open Chidubemkingsley opened 3 months ago

Chidubemkingsley commented 3 months ago

Severity : High

Vulnerability Details:

The BuggyNFTVault contract has a reentrancy vulnerability in the withdraw function. The function transfers ETH to the caller before updating the user's deposit balance in the deposits mapping. This sequence of operations opens up the possibility for an attacker to re-enter the withdraw function multiple times within the same transaction, allowing them to withdraw more funds than they initially deposited.

Proof of Code:

Here is the problematic section of the withdraw function:

function withdraw(uint256 tokenId) external {
    require(ownerOf(tokenId) == msg.sender, "Only the owner can withdraw");

    // Burn the NFT to complete the withdrawal process.
    _burn(tokenId);

    // Vulnerable code: sending ETH before updating the deposit balance.
    (bool success, ) = msg.sender.call{value: depositRequired}(" ");
    require(success, "Transfer failed");

    // The deposit balance is updated after ETH is sent, allowing reentrancy.
    deposits[msg.sender] -= depositRequired;
}

Impact:

The impact of this vulnerability is severe. An attacker can exploit this flaw to withdraw more funds than they originally deposited by repeatedly calling the withdraw function before their deposit balance is updated. This can lead to the depletion of the contract's ETH balance, resulting in significant financial losses for both the contract owner and legitimate users.

Recommendation:

To fix this vulnerability, update the deposits mapping before transferring ETH to the caller. This ensures that the user's balance is reduced before any external calls are made, mitigating the risk of reentrancy attacks.

Here's the corrected withdraw function:

function withdraw(uint256 tokenId) external {
    require(ownerOf(tokenId) == msg.sender, "Only the owner can withdraw");

    // Update the deposits before transferring funds to prevent reentrancy.
    deposits[msg.sender] -= depositRequired;

    // Burn the NFT to complete the withdrawal process.
    _burn(tokenId);

    (bool success, ) = msg.sender.call{value: depositRequired}("");
    require(success, "Transfer failed");
}

Consider using OpenZeppelin's ReentrancyGuard to further protect against reentrancy attacks across the entire contract:


import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract BuggyNFTVault is ERC721, ReentrancyGuard {
    // Existing code
}

Then, modify the withdraw function to include the nonReentrant modifier:


function withdraw(uint256 tokenId) external nonReentrant {
    // Function code
}

Implementing these changes will effectively mitigate the reentrancy vulnerability and protect user funds.

BogoCvetkov commented 3 months ago

The provided code snippet is incorrect (it does not reflect the actual code of withdraw) and hence the issue invalid.

Chidubemkingsley commented 3 months ago

The provided code snippet is incorrect (it does not reflect the actual code of withdraw) and hence the issue invalid.

    function withdraw(uint256 tokenId) external {
        require(ownerOf(tokenId) == msg.sender, "Only the owner can withdraw");

        // Burn the NFT to complete the withdrawal process.
        _burn(tokenId);
        deposits[msg.sender] -= depositRequired;

        (bool success, ) = msg.sender.call{value: depositRequired}(" ");
        require(success, "Transfer failed");
    }
}

This is the actual code and the one i provided was just a way to show how reentrancy was in the code. i was trying to show with comment how reentrancy was in the code as i didnt write it in the order as it was in the real code. I dont understand how this minor issue can get me invalidated. Thanks for understanding.

BogoCvetkov commented 3 months ago

The vulnerability is not based on the actual code, making the issue based of it invalid. You should refer to the actual code, where it you can see that the low-level call is executed at the end and reentrancy is not possible since the token is already burned