TCTF NFT Market
题目¶
Welcome to TCTF NFT Market, a secure, open-source, and decentralized NFT marketplace!
Trade your favourite NFTs (and flag) here!
nc 47.102.40.39 20000
解题思路¶
PoW.py
-
持有 tokenId 为 1、2、3 的 TNFT 即可触发事件
SendFlag
- 可以调用一次
airdrop()
获得 5 TTK - 当持有或被批准使用 TNFT 时,可以
createOrder()
或cancelOrder()
- 当持有足够 TTK 时可以
purchaseOrder()
- 可以使用经 TNFT 所有者签名的 coupon 调用一次
purchaseWithCoupon()
,以修改后的价格进行购买 - 可以进行一次
purchaseTest()
,TctfMarket
将自己完成订单的创建与购买,由于approve
不能授权给所有者,可以利用purchaseTest()
来转移TctfMarket
的 TTK
task.sol
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract TctfNFT is ERC721, Ownable { constructor() ERC721("TctfNFT", "TNFT") { _setApprovalForAll(address(this), msg.sender, true); } function mint(address to, uint256 tokenId) external onlyOwner { _mint(to, tokenId); } } contract TctfToken is ERC20 { bool airdropped; constructor() ERC20("TctfToken", "TTK") { _mint(address(this), 100000000000); _mint(msg.sender, 1337); } function airdrop() external { require(!airdropped, "Already airdropped"); airdropped = true; _mint(msg.sender, 5); } } struct Order { address nftAddress; uint256 tokenId; uint256 price; } struct Coupon { uint256 orderId; uint256 newprice; address issuer; address user; bytes reason; } struct Signature { uint8 v; bytes32[2] rs; } struct SignedCoupon { Coupon coupon; Signature signature; } contract TctfMarket { event SendFlag(); event NFTListed( address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 price ); event NFTCanceled( address indexed seller, address indexed nftAddress, uint256 indexed tokenId ); event NFTBought( address indexed buyer, address indexed nftAddress, uint256 indexed tokenId, uint256 price ); bool tested; TctfNFT public tctfNFT; TctfToken public tctfToken; CouponVerifierBeta public verifier; Order[] orders; constructor() { tctfToken = new TctfToken(); tctfToken.approve(address(this), type(uint256).max); tctfNFT = new TctfNFT(); tctfNFT.mint(address(tctfNFT), 1); tctfNFT.mint(address(this), 2); tctfNFT.mint(address(this), 3); verifier = new CouponVerifierBeta(); orders.push(Order(address(tctfNFT), 1, 1)); orders.push(Order(address(tctfNFT), 2, 1337)); orders.push(Order(address(tctfNFT), 3, 13333333337)); } function getOrder(uint256 orderId) public view returns (Order memory order) { require(orderId < orders.length, "Invalid orderId"); order = orders[orderId]; } function createOrder(address nftAddress, uint256 tokenId, uint256 price) external returns(uint256) { require(price > 0, "Invalid price"); require(isNFTApprovedOrOwner(nftAddress, msg.sender, tokenId), "Not owner"); orders.push(Order(nftAddress, tokenId, price)); emit NFTListed(msg.sender, nftAddress, tokenId, price); return orders.length - 1; } function cancelOrder(uint256 orderId) external { Order memory order = getOrder(orderId); require(isNFTApprovedOrOwner(order.nftAddress, msg.sender, order.tokenId), "Not owner"); _deleteOrder(orderId); emit NFTCanceled(msg.sender, order.nftAddress, order.tokenId); } function purchaseOrder(uint256 orderId) external { Order memory order = getOrder(orderId); _deleteOrder(orderId); IERC721 nft = IERC721(order.nftAddress); address owner = nft.ownerOf(order.tokenId); tctfToken.transferFrom(msg.sender, owner, order.price); nft.safeTransferFrom(owner, msg.sender, order.tokenId); emit NFTBought(msg.sender, order.nftAddress, order.tokenId, order.price); } function purchaseWithCoupon(SignedCoupon calldata scoupon) external { Coupon memory coupon = scoupon.coupon; require(coupon.user == msg.sender, "Invalid user"); require(coupon.newprice > 0, "Invalid price"); verifier.verifyCoupon(scoupon); Order memory order = getOrder(coupon.orderId); _deleteOrder(coupon.orderId); IERC721 nft = IERC721(order.nftAddress); address owner = nft.ownerOf(order.tokenId); tctfToken.transferFrom(coupon.user, owner, coupon.newprice); nft.safeTransferFrom(owner, coupon.user, order.tokenId); emit NFTBought(coupon.user, order.nftAddress, order.tokenId, coupon.newprice); } function purchaseTest(address nftAddress, uint256 tokenId, uint256 price) external { require(!tested, "Tested"); tested = true; IERC721 nft = IERC721(nftAddress); uint256 orderId = TctfMarket(this).createOrder(nftAddress, tokenId, price); nft.approve(address(this), tokenId); TctfMarket(this).purchaseOrder(orderId); } function win() external { require(tctfNFT.ownerOf(1) == msg.sender && tctfNFT.ownerOf(2) == msg.sender && tctfNFT.ownerOf(3) == msg.sender); emit SendFlag(); } function isNFTApprovedOrOwner(address nftAddress, address spender, uint256 tokenId) internal view returns (bool) { IERC721 nft = IERC721(nftAddress); address owner = nft.ownerOf(tokenId); return (spender == owner || nft.isApprovedForAll(owner, spender) || nft.getApproved(tokenId) == spender); } function _deleteOrder(uint256 orderId) internal { orders[orderId] = orders[orders.length - 1]; orders.pop(); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { return this.onERC721Received.selector; } } contract CouponVerifierBeta { TctfMarket market; bool tested; constructor() { market = TctfMarket(msg.sender); } function verifyCoupon(SignedCoupon calldata scoupon) public { require(!tested, "Tested"); tested = true; Coupon memory coupon = scoupon.coupon; Signature memory sig = scoupon.signature; Order memory order = market.getOrder(coupon.orderId); bytes memory serialized = abi.encode( "I, the issuer", coupon.issuer, "offer a special discount for", coupon.user, "to buy", order, "at", coupon.newprice, "because", coupon.reason ); IERC721 nft = IERC721(order.nftAddress); address owner = nft.ownerOf(order.tokenId); require(coupon.issuer == owner, "Invalid issuer"); require(ecrecover(keccak256(serialized), sig.v, sig.rs[0], sig.rs[1]) == coupon.issuer, "Invalid signature"); } }
- 可以调用一次
-
通过
airdrop()
和purchaseTest()
容易获得 tokenId 为 1、2 的 TNFT。要获得 tokenId 为 3 的 TNFT 显然需要使用到purchaseWithCoupon()
,但所有者为合约,不存在能够用于签名的私钥,verifyCoupon()
的判断条件也相当严格,无法伪造签名- 虽然有想过通过
purchaseWithCoupon()
来窃取合约TctfToken
的 TTK,但purchaseWithCoupon()
限制了msg.sender
必须为coupon.user
(╥ω╥)
- 虽然有想过通过
- 在 0.8.16 之前存在 Head Overflow 的 Bug,发生在 calldata tuple 进行 ABI 重编码时,
SignedCoupon
恰好满足漏洞触发的条件- tuple 的最后一个元素是静态数组且存储在 calldata,数组元素为基本类型
uint
或bytes32
,对应Signature
中的bytes32[2] rs
- tuple 包含至少一个动态元素,如
bytes
或包含动态数组的结构体,即Coupon
中的bytes reason
- 代码使用 ABI coder v2(自 0.8.0 起默认)
- tuple 的最后一个元素是静态数组且存储在 calldata,数组元素为基本类型
- tuple 的 ABI 编码包含两部分,静态编码的 head 以及动态编码的 tail,head 中包含静态元素以及动态元素自编码起的偏移,动态元素实际存储在 tail 中
-
编码后的
scoupon
参数布局如下,底部数字表示编码的顺序 -
当静态数组作为结构体最后一个元素时,其后 tail 的前 \(32\) 字节将被覆盖(实际将被覆盖为 \(0\))。也就是说,当
purchaseWithCoupon()
调用verifyCoupon()
时,实际参与验证的都是orderId
为0
的订单 -
首先再创建一个
TctfNFT
合约并mint()
1 个 token,利用purchaseTest()
转移TctfMarket
的所有余额。随后purchaseOrder(1)
,此时tokenId
为3
的订单下标为1
,接着createOrder()
使得调用purchaseOrder(0)
后orderId
为0
的订单受控,从而能对其进行签名并通过验证
Flag¶
flag{off_by_null_in_the_market_d711fbd6a7c0c015b42d}
参考资料¶
- Head Overflow Bug in Calldata Tuple ABI-Reencoding | Solidity Blog
- Formal Specification of the Encoding