
From patch to RCE: hand-built exploit for CVE-2010-4221 (ProFTPD TELNET IAC stack overflow), with the full failure-driven journey documented
A fully reproducible lab and a hand-written, raw-socket exploit for
CVE-2010-4221 — the pre-authentication stack buffer overflow in
ProFTPD's pr_netio_telnet_gets() — built as a learning exercise in
vulnerability research and exploit development.
Every failure is documented. The happy path is a lie; the detours are the lesson.
This repository is an educational artifact. It exists so that people who cannot afford a mentor or a training can learn how a memory-corruption exploit is actually born: from a patch, through failures, to a working proof of concept inside a laboratory you own.
Red Team work — real, professional offensive security — is defined by one word: authorization. Everything a professional does happens inside a written agreement: a signed Rules of Engagement document that names the scope, the targets, the techniques allowed, the time window, and the people who approved it. Without that paper, the exact same keystrokes are not a profession — they are a crime in essentially every jurisdiction on Earth.
So here is the contract for this repo, non-negotiable:
The craft is worth learning. The craft is only worth something with the discipline that comes with it.
ProFTPD speaks TELNET escape sequences on the FTP control channel. In
TELNET, 0xFF (IAC, "Interpret As Command") is the escape byte; a
literal 0xFF is sent as 0xFF 0xFF.
pr_netio_telnet_gets() copies client bytes into a stack buffer
(pr_cmd_read's char buf[PR_DEFAULT_CMD_BUFSZ+1], 4104 bytes with
glibc's MAXPATHLEN=4096), tracking remaining space in buflen — a
size_t, unsigned.
The vulnerable path (1.3.3a, netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // write #1
buflen--; // decrement #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // write #2
buflen--; // decrement #2 <-- no check in between
Two writes, two decrements, no zero check between them. When buflen
is exactly 1, the pair decrements it to 0, then underflows to
SIZE_MAX (18 quintillion). The loop now believes the buffer is infinite
and keeps writing attacker-controlled bytes up the stack — over saved
registers, saved RBP, and the return address.
Pre-auth. The function runs before USER/PASS are ever processed.
The fix (commit
3cc69b8388,
"Bug#3521 - Telnet IAC processing stack overflow", released in 1.3.3c)
is twelve lines. The entire security boundary is:
if (buflen == 0) {
break;
}
See patch.diff. Reading the patch tells you where the
wound was — that is the skill.
The Dockerfile compiles ProFTPD 1.3.3a from the historical Debian snapshot source, deliberately insecure (this is how 2010 looked):
-fno-stack-protector — no canary-z execstack — executable stack (no NX)-no-pie — fixed binary addressesgdb, which disables ASLR by default → deterministic stackdocker build -t proftpd-133a .
docker rm -f lab133 2>/dev/null
docker run -d --name lab133 --cap-add SYS_PTRACE \
--security-opt seccomp=unconfined -p 127.0.0.1:2122:21 \
proftpd-133a sh -c 'gdb -batch -ex "set follow-fork-mode child" \
-ex "run" -ex "continue" --args /usr/local/sbin/proftpd -n -d1 \
> /tmp/gdb.txt 2>&1; sleep 600'
python3 exploit.py
Expected output:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(The shellcode writes to fds 0, 1 and 2 because we did not want to depend on knowing which one carries the control channel — two of them answer.)
"SITE " + NOP sled + shellcode + [\xff\xff flood] + padding + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
shellcode lives INSIDE the command the overflow tail only
buffer — the region nobody touches delivers ONE address
buf[4102] is touched
(truncation NUL). Everything below the frame's live locals is calm.buflen to the underflow (see "The journey"
for the parity problem).pr_cmd_read hits return 0 after parsing, the
CPU lands in the sled and slides into the shellcode.This inverted architecture — payload first, flood second, address last —
was validated against the canonical Metasploit module
(proftp_telnet_iac), which uses the same layout. Their targets had NX,
so they needed a ROP chain with a "quadruple deref" of the res pointer;
our lab has an executable stack, so a single direct return suffices.
The final exploit is 60 lines. What it cost:
Blind \xff flood → nothing. The server politely closed the
session. Root cause: buflen starts at 4102 (EVEN) and each IAC pair
decrements by 2 — it lands on 0 cleanly, never on 1. The underflow
needs parity broken. Lesson: reading the state machine beats spraying.
Wrong buffer size. First calibrated attempt assumed a 1024-byte
buffer. The real one is MAXPATHLEN+8 = 4104 on Linux/glibc. The flood
stopped 3KB short of the target. Lesson: measure the target, don't
assume the target.
First SIGSEGV. Cyclic de Bruijn pattern (Aa0Aa1...) placed the
return slot at buf+4152, cross-validated twice (frame math + pattern
offset). Lesson: the cyclic pattern is a measuring tape, not an
exploit.
RIP control. Setting the slot to 0x4141414141414141 crashed the
ret instruction itself — x86-64 refuses non-canonical addresses, and
the fault lands on ret, with our value waiting in the backtrace.
Lesson: a crash on ret with your value in the frame = control.
Shellcode above the ret slot → clobbered. 8 bytes overwritten by a
heap pointer (0x4d7838 — later identified as the cmd_rec pool
allocation). The stack frames above the slot belong to functions still
working between landing and hijack. Lesson: the overflow is not the
last write; the program keeps living on the stack you just vandalized.
"Dead zone" below the slot → also scribbled. pr_cmd_read's own
locals (cmd, buflen, cp) live right there and keep being stored
during parsing.
Hardware watchpoint forensics. watch *(long*)ADDR in gdb turned
the mystery into a camera: every write to the clobbered address, with
backtrace, in order. Lesson: when the question is "who wrote this
memory?", the answer is one gdb command away.
Read the reference, then understand it. The canonical module confirmed the inverted architecture. Reading another exploit after building your own mental model is study; before, it is copying.
size_t never goes negative — it goes gigantic. Integer underflow
in a remaining-space counter is a stack overflow with extra steps.\x0a
(ends the read) and survives \xff (TELNET escape) by design.argv[0] (build-tree binary vs installed binary) shifted the whole
stack by 0x40 and silently invalidated a perfect exploit.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)Built by diegslva, learning in public — from "never wrote an exploit" to pre-auth RCE with hand-rolled shellcode, in one documented day. If this repo taught you something, pay it forward.