Akutar-nft验证漏洞分析

说明

前面NFT常见认证漏洞总结这篇文章讲解了NFT项目在认证过程中常见的漏洞,本篇文章则是以Akutar这个项目为例讲解一些匪夷所思的漏洞,你永远不知道业务方也写出来什么样的代码。所以,当我们在实际参与项目时,如果项目方的合约代码是开源的,最好先人为审计保证没有安全问题之后再参与。

目前合约的漏洞,导致目前合约中还被封锁了11539.5ETH,永远没有办法取出。

基本信息

Akutar NFT在线查看Akutar NFT

其中与漏洞有关的函数分别是:processRefunds()claimProjectFunds()

  • processRefunds(),用于处理用户退款
  • claimProjectFunds(),用于项目方从合约中取出所有的资金

这个合约最基本的防止重入禁止合约调用都没有校验,这也为后面的漏洞利用埋下了隐患。

转账校验漏洞

此漏洞对应的就是processRefunds()。这个函数中使用循环来给所有用户进行退款。源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function processRefunds() external {
require(block.timestamp > expiresAt, "Auction still in progress");
uint256 _refundProgress = refundProgress;
uint256 _bidIndex = bidIndex;
require(_refundProgress < _bidIndex, "Refunds already processed");

uint256 gasUsed;
uint256 gasLeft = gasleft();
uint256 price = getPrice();

for (uint256 i=_refundProgress; gasUsed < 5000000 && i < _bidIndex; i++) {
bids memory bidData = allBids[i];
if (bidData.finalProcess == 0) {
uint256 refund = (bidData.price - price) * bidData.bidsPlaced;
uint256 passes = mintPassOwner[bidData.bidder];
if (passes > 0) {
refund += mintPassDiscount * (bidData.bidsPlaced < passes ? bidData.bidsPlaced : passes);
}
allBids[i].finalProcess = 1;
if (refund > 0) {
(bool sent, ) = bidData.bidder.call{value: refund}("");
require(sent, "Failed to refund bidder");
}
}

gasUsed += gasLeft - gasleft();
gasLeft = gasleft();
_refundProgress++;
}

refundProgress = _refundProgress;
}

这个函数存在两个很严重的漏洞:

  • 没有限制仅有owner可以调用,这意味着任何人都可以调用
  • 没有限制合约调用,导致合约可以调用这个方法

这个函数的主要功能就是为所有的用户退款,转账中最为关键的代码是:

1
2
3
4
5
6
for (uint256 i=_refundProgress; gasUsed < 5000000 && i < _bidIndex; i++) {
if (refund > 0) {
(bool sent, ) = bidData.bidder.call{value: refund}("");
require(sent, "Failed to refund bidder");
}
}

因为没有对bidData.bidder校验是EOA账户。如果是一个部署了fallback()函数(fallback的原理参考发送和接受ETH方式汇总)的恶意合约,可以revert这个转账交易,这就会导致所有的用户的转账交易都会被revert。

以下就是一个简单的攻击示例程序。

逻辑漏洞

这个逻辑漏洞对应的函数是claimProjectFunds(),限制了仅owner调用,用于项目方取出所有的资金。具体的代码逻辑如下:

1
2
3
4
5
6
7
8
function claimProjectFunds() external onlyOwner {
require(block.timestamp > expiresAt, "Auction still in progress");
require(refundProgress >= totalBids, "Refunds not yet processed");
require(akuNFTs.airdropProgress() >= totalBids, "Airdrop not complete");

(bool sent, ) = project.call{value: address(this).balance}("");
require(sent, "Failed to withdraw");
}

如果需要成功取款,必须满足三个必备的条件:

  • require(block.timestamp > expiresAt, "Auction still in progress"),要求所有的拍卖进程完成后进行
  • require(refundProgress >= totalBids, "Refunds not yet processed"),要求所有的用户完成退款
  • require(akuNFTs.airdropProgress() >= totalBids, "Airdrop not complete");,要求空头必须全部完成

最关键的是第二个逻辑require(refundProgress >= totalBids, "Refunds not yet processed"),由于这个逻辑永远无法满足,所以导致合约中的钱用于无法取出。下面开始分析:

refundProgress

refundProgress是在processRefunds() _refundProgressß赋值的,表示已经处理过的退款的用户数量。

totalBids

_bid中由uint256 _totalBids = totalBids + amount;,实际表示的是所有商品的数量

因为商品的数量是一定大于用户的数量的,这就导致refundProgress >= totalBids永远无法满足,导致require(refundProgress >= totalBids, "Refunds not yet processed")永远失败,最后无法提款成功

修正

refundProgress的比较对象应该是bidIndex,这里的bidIndex指的是投标账户的总数,在bid函数中,每当有一个账户投标,bitIndex就会加1,且不会重复。bidIndex的逻辑如下所示:

1
2
3
4
5
6
7
8
9
10
11
function _bid(uint8 amount, uint256 value) internal {
uint256 myBidIndex = personalBids[msg.sender];
if (myBidIndex > 0) {
allBids[myBidIndex] = myBids;
} else {
myBids.bidder = msg.sender;
personalBids[msg.sender] = bidIndex;
allBids[bidIndex] = myBids;
bidIndex++;
}
}

总结

漏洞的本质的原因是因为Akutar NFT的逻辑复杂,编写NFT的开发人员并没有按照常规的开发逻辑去实现,导致整个合约的逻辑复杂,变量命名不够规范,所以基本的安全防护也没有。变量命名不规范这也是导致最后取款操作失败的原因,这也从侧面反映了这个合约没有进行很好的代码测试,没有测试出claimProjectFunds()存在的问题。

区块链的透明性和不可篡改性一方面成就了区块链,但是也对开发人员的要求更高。作为我们个人用户,也需要综合分析判断项目方。

参考

  1. https://www.youtube.com/watch?v=EstPlZDMqTQ
  2. https://blocksecteam.medium.com/how-akutar-nft-loses-34m-usd-60d6cb053dff
  3. https://github.com/calvwang9/akutars-auction-post-mortem
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/10/08/Akutar-nft-vuln/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow