
Esta es una prueba de concepto que escribí para CVE-2024-6387.
Qualys Security Advisory
regreSSHion: RCE in OpenSSH's server, on glibc-based Linux systems (CVE-2024-6387)
Resumen SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, de 2005)
Todo lo que se necesita es un acto de fe
-- The Interrupters, "Leap of Faith"
Nota preliminar: OpenSSH es uno de los programas más seguros del mundo; esta vulnerabilidad es un desliz en una implementación por lo demás casi impecable. Su diseño y código de defensa en profundidad son un modelo y una inspiración, y agradecemos a los desarrolladores de OpenSSH su ejemplar trabajo.
Descubrimos una vulnerabilidad (una condición de carrera en el manejador de señales) en el servidor de OpenSSH (sshd): si un cliente no se autentica dentro de LoginGraceTime segundos (120 por defecto, 600 en versiones antiguas de OpenSSH), entonces el manejador SIGALRM de sshd se invoca asincrónicamente, pero este manejador de señales llama a varias funciones que no son seguras para señales asíncronas (por ejemplo, syslog()). Esta condición de carrera afecta a sshd en su configuración por defecto.
Al investigar, nos dimos cuenta de que esta vulnerabilidad es en realidad una regresión de CVE-2006-5051 ("Signal handler race condition in OpenSSH before 4.4 allows remote attackers to cause a denial of service (crash), and possibly execute arbitrary code"), reportada en 2006 por Mark Dowd.
Esta regresión se introdujo en octubre de 2020 (OpenSSH 8.5p1) mediante el commit 752250c ("revised log infrastructure for OpenSSH"), que eliminó accidentalmente un "#ifdef DO_LOG_SAFE_IN_SIGHAND" de sigdie(), una función llamada directamente por el manejador SIGALRM de sshd. En otras palabras:
OpenSSH < 4.4p1 es vulnerable a esta condición de carrera en el manejador de señales, si no tiene un backport del parche contra CVE-2006-5051, o no está parcheado contra CVE-2008-4109, que fue una corrección incorrecta para CVE-2006-5051;
4.4p1 <= OpenSSH < 8.5p1 no es vulnerable a esta condición de carrera en el manejador de señales (porque el "#ifdef DO_LOG_SAFE_IN_SIGHAND" que se añadió a sigdie() mediante el parche para CVE-2006-5051 transformó esta función insegura en una llamada segura a _exit(1));
8.5p1 <= OpenSSH < 9.8p1 vuelve a ser vulnerable a esta condición de carrera en el manejador de señales (porque el "#ifdef DO_LOG_SAFE_IN_SIGHAND" se eliminó accidentalmente de sigdie()).
Esta vulnerabilidad es explotable remotamente en sistemas Linux basados en glibc, donde syslog() a su vez llama a funciones no seguras para señales asíncronas (por ejemplo, malloc() y free()): una ejecución remota de código no autenticada como root, porque afecta al código privilegiado de sshd, que no está en sandbox y se ejecuta con privilegios completos. No hemos investigado ninguna otra libc ni sistema operativo; pero OpenBSD no es vulnerable notablemente, porque su manejador SIGALRM llama a syslog_r(), una versión más segura para señales asíncronas de syslog() que fue inventada por OpenBSD en 2001.
Para explotar esta vulnerabilidad remotamente (hasta donde sabemos, CVE-2006-5051 nunca se había explotado con éxito antes), nos inspiramos en un artículo visionario, "Delivering Signals for Fun and Profit", publicado en 2001 por Michal Zalewski:
https://lcamtuf.coredump.cx/signals.txt
Sin embargo, inmediatamente nos enfrentamos a tres problemas principales:
Desde un punto de vista teórico, debemos encontrar una ruta de código útil que, si es interrumpida en el momento adecuado por SIGALRM, deje a sshd en un estado inconsistente, y luego debemos explotar ese estado inconsistente dentro del manejador SIGALRM.
Desde un punto de vista práctico, debemos encontrar una forma de alcanzar esta ruta de código útil en sshd, y maximizar nuestras posibilidades de interrumpirla en el momento adecuado.
Desde un punto de vista de temporización, debemos encontrar una forma de aumentar aún más nuestras posibilidades de interrumpir esta ruta de código útil en el momento adecuado, de forma remota.
Para centrarnos en estos tres problemas sin tener que luchar de inmediato contra todas las protecciones de los sistemas operativos modernos (en particular, ASLR y NX), decidimos explotar primero versiones antiguas de OpenSSH, en i386, y luego, basándonos en esta experiencia, versiones recientes:
Primero, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", de "debian-30r6-dvd-i386-binary-1_NONUS.iso": esta es la primera versión de Debian que tiene la separación de privilegios habilitada por defecto y que está parcheada contra todas las vulnerabilidades críticas de esa época (en particular, CVE-2003-0693 y CVE-2002-0640).
Para explotar esta versión remotamente, interrumpimos una llamada a free() con SIGALRM (dentro del código de análisis de claves públicas de sshd), dejamos el heap en un estado inconsistente, y explotamos este estado inconsistente durante otra llamada a free(), dentro del manejador SIGALRM.
En nuestros experimentos, se necesitan ~10.000 intentos en promedio para ganar esta condición de carrera; es decir, con 10 conexiones (MaxStartups) aceptadas por cada 600 segundos (LoginGraceTime), se necesitan ~1 semana en promedio para obtener una shell root remota.
Segundo, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", de "ubuntu-6.06.1-server-i386.iso": esta es la última versión de Ubuntu que sigue siendo vulnerable a CVE-2006-5051 ("Signal handler race condition in OpenSSH before 4.4").
Para explotar esta versión remotamente, interrumpimos una llamada a pam_start() con SIGALRM, dejamos una de las estructuras de PAM en un estado inconsistente, y explotamos este estado inconsistente durante una llamada a pam_end(), dentro del manejador SIGALRM.
En nuestros experimentos, se necesitan ~10.000 intentos en promedio para ganar esta condición de carrera; es decir, con 10 conexiones (MaxStartups) aceptadas por cada 120 segundos (LoginGraceTime), se necesitan ~1-2 días en promedio para obtener una shell root remota.
Finalmente, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", de "debian-12.5.0-i386-DVD-1.iso": esta es la versión estable actual de Debian, y es vulnerable a la regresión de CVE-2006-5051.
Para explotar esta versión remotamente, interrumpimos una llamada a malloc() con SIGALRM (dentro del código de análisis de claves públicas de sshd), dejamos el heap en un estado inconsistente, y explotamos este estado inconsistente durante otra llamada a malloc(), dentro del manejador SIGALRM (más precisamente, dentro de syslog()).
En nuestros experimentos, se necesitan ~10.000 intentos en promedio para ganar esta condición de carrera, así que ~3-4 horas con 100 conexiones (MaxStartups) aceptadas por cada 120 segundos (LoginGraceTime). En última instancia, se necesitan ~6-8 horas en promedio para obtener una shell root remota, porque solo podemos adivinar correctamente la dirección de la glibc la mitad de las veces (debido a ASLR).
Esta investigación sigue siendo un trabajo en progreso:
solo nos hemos dirigido a máquinas virtuales, no a servidores de metal desnudo, en un enlace de red mayormente estable (~10 ms de fluctuación de paquetes);
estamos convencidos de que varios aspectos de nuestros exploits pueden mejorarse enormemente;