
Del parche al RCE: exploit construido a mano para CVE-2010-4221 (desbordamiento de pila TELNET IAC en ProFTPD), con el recorrido completo impulsado por fallos documentado
Un laboratorio totalmente reproducible y un exploit escrito a mano con sockets crudos para
CVE-2010-4221 — el desbordamiento de búfer en pila previo a la autenticación en
pr_netio_telnet_gets() de ProFTPD — construido como ejercicio de aprendizaje en
investigación de vulnerabilidades y desarrollo de exploits.
Cada fallo está documentado. El camino feliz es una mentira; los desvíos son la lección.
Este repositorio es un artefacto educativo. Existe para que las personas que no pueden pagar un mentor o una formación puedan aprender cómo nace realmente un exploit de corrupción de memoria: desde un parche, pasando por fallos, hasta una prueba de concepto funcional dentro de un laboratorio que usted posee.
El trabajo de Red Team — la seguridad ofensiva real y profesional — se define por una palabra: autorización. Todo lo que hace un profesional ocurre dentro de un acuerdo escrito: un documento de Reglas de Compromiso firmado que nombra el alcance, los objetivos, las técnicas permitidas, la ventana de tiempo y las personas que lo aprobaron. Sin ese papel, las mismas pulsaciones de teclas no son una profesión — son un delito en prácticamente todas las jurisdicciones de la Tierra.
Así que este es el contrato para este repositorio, no negociable:
El oficio merece la pena aprenderse. El oficio solo vale algo con la disciplina que lo acompaña.
ProFTPD habla secuencias de escape TELNET en el canal de control FTP. En
TELNET, 0xFF (IAC, "Interpret As Command") es el byte de escape; un
0xFF literal se envía como 0xFF 0xFF.
pr_netio_telnet_gets() copia los bytes del cliente en un búfer de pila
(char buf[PR_DEFAULT_CMD_BUFSZ+1] de pr_cmd_read, 4104 bytes con
MAXPATHLEN=4096 de glibc), rastreando el espacio restante en buflen — un
size_t, sin signo.
La ruta vulnerable (1.3.3a, netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // escritura #1
buflen--; // decremento #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // escritura #2
buflen--; // decremento #2 <-- sin comprobación entre ambos
Dos escrituras, dos decrementos, sin comprobación de cero entre ellos. Cuando buflen
es exactamente 1, el par lo decrementa a 0, y luego se desborda a
SIZE_MAX (18 quintillones). El bucle ahora cree que el búfer es infinito
y sigue escribiendo bytes controlados por el atacante hacia arriba en la pila — sobre los
registros guardados, el RBP guardado y la dirección de retorno.
PrevIO a la autenticación. La función se ejecuta antes de que USER/PASS se procesen jamás.
La corrección (commit
3cc69b8388,
"Bug#3521 - Telnet IAC processing stack overflow", publicada en 1.3.3c)
son doce líneas. Toda la frontera de seguridad es:
if (buflen == 0) {
break;
}
Vea patch.diff. Leer el parche le dice dónde estaba
la herida — esa es la habilidad.
El Dockerfile compila ProFTPD 1.3.3a desde el código fuente histórico de la instantánea de Debian, deliberadamente inseguro (así se veía 2010):
-fno-stack-protector — sin canario-z execstack — pila ejecutable (sin NX)-no-pie — direcciones binarias fijasgdb, que desactiva ASLR por defecto → pila deterministadocker 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
Salida esperada:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(El shellcode escribe en los fds 0, 1 y 2 porque no quisimos depender de saber cuál transporta el canal de control — dos de ellos responden.)
"SITE " + trineo NOP + shellcode + [inundación \xff\xff] + relleno + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
el shellcode vive DENTRO del búfer la cola del desbordamiento solo
de comando — la región que nadie toca entrega UNA dirección
buf[4102]
(NUL de truncamiento). Todo lo que está debajo de las variables locales vivas del marco está en calma.buflen al desbordamiento (vea "El viaje"
para el problema de paridad).pr_cmd_read llega a return 0 después de analizar, la
CPU aterriza en el trineo y se desliza hacia el shellcode.Esta arquitectura invertida — payload primero, inundación después, dirección al final —
fue validada contra el módulo canónico de Metasploit
(proftp_telnet_iac), que usa la misma disposición. Sus objetivos tenían NX,
por lo que necesitaban una cadena ROP con un "doble desreferenciado cuádruple" del puntero res;
nuestro laboratorio tiene una pila ejecutable, por lo que un solo retorno directo basta.
El exploit final son 60 líneas. Lo que costó:
Inundación ciega de \xff → nada. El servidor cerró cortésmente la
sesión. Causa raíz: buflen comienza en 4102 (PAR) y cada par IAC
decrementa en 2 — aterriza en 0 limpiamente, nunca en 1. El desbordamiento
necesita romper la paridad. Lección: leer la máquina de estados vence a rociar.
Tamaño de búfer incorrecto. El primer intento calibrado asumió un búfer de 1024 bytes.
El real es MAXPATHLEN+8 = 4104 en Linux/glibc. La inundación
se detuvo 3KB antes del objetivo. Lección: mida el objetivo, no
asuma el objetivo.
Primer SIGSEGV. El patrón cíclico de Bruijn (Aa0Aa1...) colocó la
ranura de retorno en buf+4152, validado de forma cruzada dos veces (matemática del marco + desplazamiento del patrón). Lección: el patrón cíclico es una cinta métrica, no un
exploit.
Control de RIP. Establecer la ranura en 0x4141414141414141 bloqueó la
propia instrucción ret — x86-64 rechaza direcciones no canónicas, y
el fallo ocurre en ret, con nuestro valor esperando en el backtrace.
Lección: un bloqueo en ret con su valor en el marco = control.
Shellcode por encima de la ranura ret → destruido. 8 bytes sobrescritos por un
puntero de heap (0x4d7838 — identificado más tarde como la asignación del pool
cmd_rec). Los marcos de pila por encima de la ranura pertenecen a funciones que aún
están trabajando entre el aterrizaje y el secuestro. Lección: el desbordamiento no es la
última escritura; el programa sigue viviendo en la pila que acaba de vandalizar.
"Zona muerta" debajo de la ranura → también garabateada. Las propias variables
locales de pr_cmd_read (cmd, buflen, cp) viven justo allí y siguen
almacenándose durante el análisis.
Forensica con puntos de interrupción de hardware. watch *(long*)ADDR en gdb convirtió
el misterio en una cámara: cada escritura en la dirección destruida, con
backtrace, en orden. Lección: cuando la pregunta es "¿quién escribió esta
memoria?", la respuesta está a un comando de gdb de distancia.
Lea la referencia, luego entiéndala. El módulo canónico confirmó la arquitectura invertida. Leer otro exploit después de construir su propio modelo mental es estudio; antes, es copiar.
size_t nunca se vuelve negativo — se vuelve gigantesco. El desbordamiento de enteros
en un contador de espacio restante es un desbordamiento de pila con pasos extra.\x0a
(termina la lectura) y sobrevive a \xff (escape TELNET) por diseño.argv[0] (binario del árbol de compilación vs binario instalado) desplazó toda la
pila en 0x40 e invalidó silenciosamente un exploit perfecto.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)Construido por diegslva, aprendiendo en público — desde "nunca escribí un exploit" hasta RCE previo a la autenticación con shellcode hecho a mano, en un día documentado. Si este repositorio le enseñó algo, páselo adelante.