TELNET: CAZANDO BUGS GEMELOS - Industria Hacker
d
An igniting portfolio theme designed
to help you leave quite a mark.
Back to Top
Image Alt

TELNET: CAZANDO BUGS GEMELOS

TELNET: CAZANDO BUGS GEMELOS

TELNET: CAZANDO BUGS GEMELOS

Los servicios que nadie mira son los favoritos de los atacantes. Nadie cambió la contraseña por defecto, nadie los parcheó, nadie los monitorea, porque nadie se acuerda que existen. Una de las primeras cosas que aprende cualquier consultor es que los appliances olvidados, los routers viejos en armarios y los servicios prendidos por inercia desde hace años son los caminos más cortos a la red interna.

CVE-2026-32746 es el ejemplo perfecto: un overflow pre-autenticación en el telnetd de GNU Inetutils que vivió en el código fuente, abierto en internet, gratis para auditar, desde 1994 hasta marzo de 2026. Apple lo metió en macOS Tahoe; Citrix en NetScaler; FreeBSD, NetBSD, TrueNAS y un montón de embebidos lo distribuyen.

Lo interesante para vos como hacker en formación no es ESE bug puntual, que es bastante de manual. Es el método que lo encontró: variant analysis. Veintiún años antes, el lado cliente de telnet tuvo el mismo bug (CVE-2005-0469) y se parcheó. A nadie se le ocurrió revisar si el mismo bug existía del lado servidor. Cuando alguien finalmente miró, el CVE apareció en horas. Esa pregunta (dónde más estará este bug?) es probablemente la más rentable que vas a aprender en tu carrera de hacker.

En este artículo te muestro tres cosas: el código vulnerable real, la cadena conceptual que convierte el overflow en control del proceso, y cómo el método de variant analysis lo descubrió después de tres décadas dormido.

ANATOMÍA DEL BUG

Telnet es un protocolo de 1983 que abre sesiones de terminal remota a otra máquina por TCP. Lo usabas antes de que existiera SSH. Hoy nadie lo recomendaría, porque todo lo que mandás viaja en texto plano, contraseñas incluidas. Y aun así sigue corriendo en miles de routers viejos, NAS hogareños, equipos industriales y appliances que nadie tocó en años. Por inercia, no por necesidad.

Cuando un cliente se conecta al servidor de telnet (telnetd), antes de pedirte usuario y contraseña, los dos negocian opciones: tipo de terminal, tamaño de ventana, modo de procesamiento de input. Una de esas opciones es LINEMODE, y dentro de LINEMODE hay una sub-opción llamada SLC (Set Local Characters) donde el cliente le dice al servidor qué caracteres tienen significado especial (Ctrl+C, Ctrl+D, Backspace, etc.).

El cliente manda esa info en tripletas de tres bytes: (función, flag, valor). El servidor las guarda en un buffer en memoria de tamaño fijo: 108 bytes. Acá está el bug. El servidor no chequea si todavía queda espacio antes de escribir la próxima tripleta. Si el cliente manda más tripletas de las que entran, el servidor las sigue escribiendo, una atrás de otra, pisando lo que esté al lado en memoria. Y todo esto pasa antes del login.

Si querés ver dónde vive el buffer en términos de memoria del proceso: el buffer (slcbuf) está declarado como variable global en el segmento BSS del binario (es la zona donde el sistema pone las variables globales no inicializadas, separada del stack que es donde van las variables locales de las funciones). Importa porque algunas defensas de seguridad solo aplican al stack, no a BSS. Volvemos a esto en un momento.

El código vulnerable, simplificado:

/* slc.c - buffer estatico de 108 bytes en BSS */
static unsigned char slcbuf[108];
static unsigned char *slcptr = slcbuf;

void
add_slc (char func, char flag, cc_t val)
{
  /* sin bounds check: el cliente decide cuanto escribir */
  if ((*slcptr++ = (unsigned char) func) == 0xff)
    *slcptr++ = 0xff;
  if ((*slcptr++ = flag) == 0xff)
    *slcptr++ = 0xff;
  if ((*slcptr++ = val)  == 0xff)
    *slcptr++ = 0xff;
}

Tres patterns que delatan el bug en treinta segundos de code review:

  • Buffer estático de tamaño fijo. 108 bytes, decididos en compilación. Sin redimensionamiento dinámico.
  • Puntero global de escritura (slcptr) que se incrementa con *slcptr++ sin compararlo nunca contra el final del buffer.
  • Cantidad de escrituras controlada por el cliente: depende del campo de longitud que viaja en el paquete TELNET y que el atacante elige.

Las tres juntas son la firma de un overflow. Memorizá ese patrón: cuando lo veas en code review, vas a saber al toque que ahí hay algo para escarbar. Es el corazón que vamos a aplicar después.

EL PARCHE Y POR QUÉ LAS DEFENSAS MODERNAS NO APLICAN

El fix completo son tres líneas. PR #17 en Codeberg:

@@ -162,6 +162,9 @@ get_slc_defaults (void)
 void
 add_slc (char func, char flag, cc_t val)
 {
+  /* Do nothing if the entire triplet cannot fit in the buffer.  */
+  if (slcbuf + sizeof slcbuf - slcptr <= 6)
+    return;

   if ((*slcptr++ = (unsigned char) func) == 0xff)
     *slcptr++ = 0xff;

Una observación de consultor sobre el chequeo: contra 6 bytes, no contra 3. La razón es la protocolización IAC (Interpret As Command, el byte 0xff que TELNET reserva como prefijo de comando). Cuando alguno de los tres bytes de la tripleta es 0xff, el protocolo lo duplica en el buffer para distinguirlo de un comando real. Una tripleta puede ocupar entre 3 y 6 bytes según cuántos 0xff traiga. El check pesimista mira el peor caso. Un check ingenuo del tipo slcptr - slcbuf < 108 tampoco hubiera resuelto el bug. Si solo leés la firma de la función sin entender el formato real de los bytes en la red, no llegás a esa conclusión. Para auditar este bug hace falta entender el protocolo, no solo el lenguaje.

Ahora las defensas modernas. Ninguna aplica a este caso. Entender por qué te da la intuición para identificar bugs explotables versus bugs ya neutralizados por el sistema operativo o el compilador.

Stack canaries (los valores aleatorios que el compilador inserta entre las variables locales y el return address de cada función, como detector de overflow): solo protegen el stack. Este buffer vive en BSS, una zona de memoria distinta. No hay canary que romper porque no hay stack involucrado.

FORTIFY_SOURCE (la macro de glibc que reemplaza llamadas a strcpy, memcpy y otras funciones de copia por versiones que chequean tamaño en compile time): no se activa porque el código no usa esas funciones. Escribe byte por byte con un puntero crudo. FORTIFY_SOURCE no ve nada que enganchar.

ASLR (Address Space Layout Randomization, la aleatorización de las direcciones donde el binario y sus librerías cargan en memoria): aplica al binario, pero no rompe la mecánica del bug por sí solo. En arquitecturas de 64 bits sí complica reescribir un puntero válido completo: los punteros tienen múltiples bytes nulos que el contenido de las tripletas no puede representar arbitrariamente. En 32 bits o sistemas embebidos sin esa protección natural, la cosa se relaja.

NX bit (Non-Executable, el bit de página que impide ejecutar código desde memoria de datos como stack o heap): irrelevante. El bug no necesita ejecutar código desde el buffer; solo corromper datos adyacentes.

La regla operacional para vos como atacante: las mitigaciones modernas son por clase de bug, no universales. Cuando auditás código y encontrás un overflow, el primer ejercicio mental es ubicar dónde vive el buffer (stack / heap / BSS / data) y qué mitigaciones se desactivan en esa zona.

DE LA PRIMITIVA AL CONTROL: LA CADENA OFENSIVA

Hasta acá tenemos un overflow controlado en BSS. Eso por sí solo no es RCE. La parte entretenida del trabajo empieza acá: convertir una primitiva (escribir más bytes de los que entran) en algo que valga la pena para un atacante. Te muestro cómo se piensa el exploit. La idea es que entiendas cómo se piensa el camino, no copiar y pegar un exploit.

Paso 1: corromper símbolos adyacentes en BSS. Cuando slcptr sale de los 108 bytes de slcbuf, las próximas escrituras caen sobre lo que el linker haya puesto al lado. En el binario de telnetd compilado de Inetutils 2.7, lo que hay justo después es def_slcbuf y otras estructuras del propio handler de SLC. Acordate: el orden de símbolos en BSS lo decide el linker, así que nm telnetd y mirar el orden es el primer paso.

Paso 2: inducir un controlled write vía end_slc(). Después de que add_slc() termina de meter las tripletas, el handler llama a end_slc(), que escribe un byte terminator usando slcptr. Si vos lograste corromper alguna estructura de la que end_slc() deriva un puntero, ese terminator termina escribiéndose a una dirección que vos elegiste. Ese es el clásico paso de “overflow lineal en BSS” a “escritura arbitraria”.

Paso 3: convertir escritura arbitraria en hijack de control flow. Acá las opciones dependen del binario. En Linux moderno con full RELRO, la GOT (Global Offset Table, donde se resuelven las direcciones de funciones de librería en runtime) es read-only después del loader. Si el binario fue compilado sin RELRO completa, podés sobrescribir un slot de la GOT para apuntar a una función que vos elegiste. Típicamente system() via libc, o algún gadget. Si la GOT está blindada, el target se mueve a otras estructuras vivas: function pointers en variables globales, vtables, callbacks registrados.

Paso 4: la diferencia entre 32 y 64 bits. En 32 bits la cadena es directa: los punteros caben en 4 bytes y el atacante puede formarlos sin restricciones de bytes nulos. En 64 bits los punteros tienen al menos 2-3 bytes nulos en la parte alta. Como la stream de tripletas SLC no puede expresar bytes nulos arbitrarios donde quiera (el byte 0 es válido pero la posición la decide el escape de IAC), reescribir un puntero válido completo es estructuralmente difícil en 64 bits sin info leak previo. Por eso la versión x86_64 del bug se queda muchas veces en denial of service o info leak; la versión 32-bit (típica en routers viejos y appliances industriales con MIPS o ARM32) escala a RCE con menos esfuerzo.

Paso 5: info leak vía SLC reply. Hay una primitiva que vale aprender porque aparece en muchos bugs de protocolos legacy: el servidor responde al cliente con una estructura que incluye partes del buffer corrupto. Si controlás el offset de lectura, podés filtrar bytes adyacentes al buffer hacia el atacante: punteros válidos, valores de heap, todo lo que ayude a romper ASLR. Eso te da el primer leak para construir la versión 64-bit del exploit.

La moraleja operacional, que vale para todo CVE de buffer overflow que mires en tu vida: el bug es solo el primer paso de una cadena. Aprender a pensar la cadena (qué primitivas te da, cómo se compone con otras, qué mitigaciones se interponen) es lo que separa a alguien que reporta un crash de alguien que escribe un exploit funcional.

VARIANT ANALYSIS: CAZA DE BUGS GEMELOS

Este bug lo encontraron con una técnica que se llama variant analysis, y es probablemente la más adictiva de todas las que vas a aprender. La idea en una frase: cuando aparece un bug en un proyecto, no parar ahí. Buscar gemelos del mismo bug en código parecido del mismo proyecto, en proyectos hermanos, en forks. Casi siempre hay más. Cada vez que la aplicás bien, te vas con 3 o 4 hallazgos en lugar de uno; esa es la sensación que te tira para volver a abrir el editor a las dos de la mañana.

Pensalo así. Si un atacante encuentra una cerradura mala en una puerta de un edificio, un buen atacante no se va con esa cerradura. Va a probar todas las otras puertas del mismo edificio que parecen tener la misma marca de cerradura. Y después va al edificio de al lado del mismo arquitecto. La cerradura mala raramente está sola.

En código pasa lo mismo. Un bug es una firma: un patrón sintáctico (un loop sin chequeo, una llamada sin validar input, un puntero crudo) que, cuando se repite, repite el bug. Project Zero formalizó esta idea alrededor de 2014, y GitHub la convirtió en herramienta con CodeQL, un motor que recorre la estructura del código (no busca strings, entiende el lenguaje) para encontrar ocurrencias del patrón.

Para este caso puntual, el flow correcto era bastante mecánico: leés CVE-2005-0469 (cliente vulnerable a SLC overflow, parchado en 2005), identificás que el patrón es “add_to_buffer sin bounds check via puntero global“, buscás funciones similares en el lado servidor de GNU Inetutils, encontrás add_slc() en telnetd/slc.c con la misma firma, escribís el PoC. Veintiún años más tarde, el mismo bug, otro lado del protocolo.

La pregunta entonces es muy concreta: cuando aparezca un CVE en algún proyecto que te interese, en lugar de quedarte con el caso puntual, preguntate dónde más vive el patrón. Otras suboptions del mismo protocolo. Otros productos que copiaron la lógica. Forks downstream que nunca recibieron el parche. Esa pregunta es probablemente la más rentable que puede hacerse un hacker.

RECURSOS

Si te resultó útil esta información, te invito a ver el video relacionado y dejar tu comentario. Tu participación ayuda a que este contenido llegue a más personas y siga creciendo esta comunidad.