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 1Read the two buffer addresses and the offset
ObservationThe program prints both the username buffer address and the shell command buffer address at startup. Subtracting one from the other gives the exact overflow offset, with no guessing or hardcoded 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 chunk sizes vary by platform, libc version, and compile flags. If the username buffer really has 28 usable bytes, a hardcoded 32-byte pad overshoots the command buffer entirely, corrupting memory past it and giving a segfault or a silent no-op rather than a replaced command. Subtracting the two leaked addresses gives the exact offset for this instance.
Tried: Use a debugger or pwntools cyclic pattern locally to find the offset instead of using the server-leaked addresses.
ASLR and heap layout on the remote server differ from your local binary unless the libc and allocator match exactly. Cyclic patterns earn their keep when nothing is leaked, but here the program prints both addresses outright, so subtraction is faster and more reliable than local reversing that may not match.
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 2Overflow with a no-whitespace command
ObservationThe input is read by scanf("%s"), which stops at the first whitespace character. So the payload has to be one whitespace-free token, using input redirection (cat<flag.txt) or $IFS substitution.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") stops at the first whitespace, so the payload splits in two and only 'cat' plus the filler reaches the command buffer. The program then runs a bare 'cat' with no argument, prints nothing, and never reads the flag. Use input redirection, 'cat<flag.txt', or the IFS trick, 'cat${IFS}flag.txt', to keep the payload one 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 to chain commands in before the program reads input; the connection goes straight to the scanf call. The only injection point is the username buffer overflowing into its neighbour, so the whole no-space payload has to arrive in that single read, before system() runs.
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.