Description
A heap overflow that turns into command injection. A username buffer read with scanf("%s") sits next to a second buffer pre-filled with a harmless command (/bin/pwd) that the program runs via system(). Overflow the username into that command buffer, but mind that scanf stops at whitespace, so your payload must contain no spaces.
Setup
Connect to the server and note that it prints two buffer addresses on startup.
nc <host> <PORT_FROM_INSTANCE>Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the two buffer addresses and the offsetObservationI noticed the program prints both the username buffer address and the shell command buffer address on startup, which suggested that subtracting the two values would give the exact overflow offset instead of guessing or hardcoding a chunk size.The program allocates a username buffer (read with scanf("%s"), about 28 bytes of usable space) and, adjacent to it, a shell command buffer initialized to /bin/pwd that it later runs with system(). It prints both addresses; the difference is the overflow offset from the username to the command buffer.bash# The service prints something like: username @ 0x..., shell @ 0x...bash# offset = shell_addr - username_addr (about 28 bytes)What didn't work first
Tried: Assume the offset is always exactly 32 bytes (one standard malloc chunk) without reading the printed addresses.
Heap allocator chunk sizes vary by platform, libc version, and compile flags. If the actual username buffer is 28 bytes of usable space, a hardcoded 32-byte pad overshoots the command buffer entirely, corrupting memory past it and causing a segfault or silent no-op instead of replacing the command. Reading the two leaked addresses and subtracting gives the exact offset for the running instance.
Tried: Use a debugger or pwntools cyclic pattern locally to find the offset instead of using the server-leaked addresses.
The ASLR and heap layout on the remote server will differ from a local binary unless you have the exact same libc and allocator. Cyclic patterns are useful when no addresses are leaked, but here the program prints both buffer addresses directly, making subtraction more reliable and faster than local reversing that may not match the remote heap layout.
Learn more
Why the leaked addresses matter. You do not have to reverse chunk metadata: the program hands you both buffer addresses, so the padding length is a simple subtraction. The bug is that the username read is not bounded to its buffer, so writing past it reaches the adjacent command buffer that
system()will execute.Step 2
Overflow with a no-whitespace commandObservationI noticed that the input is read by scanf("%s"), which terminates on any whitespace character, which suggested crafting a payload that uses input redirection (cat<flag.txt) or $IFS substitution to express the target command as a single whitespace-free token.Send 'offset' bytes of filler followed by a shell command that contains no whitespace, so scanf reads the whole thing into the username buffer and it overruns into the command buffer. Use shell tricks to avoid spaces: cat<flag.txt (input redirection) or cat${IFS}flag.txt (IFS expands to whitespace).pythonpython3 -c "import sys; sys.stdout.buffer.write(b'A'*28 + b'cat<flag.txt\n')" | nc <host> <PORT_FROM_INSTANCE>bash# or, using the IFS trick for the space:pythonpython3 -c "import sys; sys.stdout.buffer.write(b'A'*28 + b'cat${IFS}flag.txt\n')" | nc <host> <PORT_FROM_INSTANCE>Expected output
picoCTF{0v3rfl0w_t0_c0mm4nd_...}The 28-byte offset is the typical value; use the exact difference of the two leaked addresses for your instance. The no-whitespace requirement is the whole trick:
scanf("%s")would truncate a command containing a literal space.What didn't work first
Tried: Send 'A'*28 + 'cat flag.txt' with a literal space between cat and flag.txt.
scanf("%s") terminates reading at the first whitespace character, so the payload is split into two tokens and only 'cat' (plus the filler) reaches the command buffer. The program then tries to run just 'cat' with no argument, produces no output, and the flag is never read. Replacing the space with the input-redirection operator 'cat<flag.txt' or the IFS trick 'cat${IFS}flag.txt' keeps the entire payload as one whitespace-free token.
Tried: Use printf or echo to build the command on the remote side instead of overflowing the buffer directly.
There is no shell session available to chain commands before the program reads input; the connection goes straight to the scanf call. The only injection point is the username buffer that overflows into the adjacent command buffer, so the entire no-space payload must arrive in that single scanf read before system() is called.
Learn more
Why no-whitespace command injection.
scanf("%s")reads a single whitespace-delimited token, so a space, tab, or newline ends your input early and the command buffer never gets your full payload. Shell features that encode a separator without a literal space, like input redirection<or the${IFS}variable, let you expresscat flag.txtas one whitespace-free token.
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{0v3rfl0w_t0_c0mm4nd_...}
A scanf("%s") username buffer (~28 bytes) overflows into an adjacent command buffer (init /bin/pwd) that system() runs. Pad to the leaked-address difference, then append a no-whitespace command (cat<flag.txt or cat${IFS}flag.txt) since scanf stops at spaces.