RegreSSHion: ejecución remota de código (RCE) en servidores OpenSSH de Linux basados en glibc
(qualys.com)- Una condición de carrera en el manejador de señales del servidor OpenSSH
sshdpuede permitir que un cliente no autenticado provoque ejecución remota de código en servidores con la configuración por defecto, si no se autentica dentro del tiempo límite previo a la autenticaciónLoginGraceTime - Esta vulnerabilidad es una regresión de CVE-2006-5051, y reapareció entre 8.5p1 y antes de 9.8p1 cuando un commit de OpenSSH 8.5p1 de octubre de 2020 eliminó una protección en
sigdie() - En Linux basado en glibc,
syslog()llama funciones async-signal-unsafe comomalloc()yfree(), lo que puede llevar a RCE como root sin autenticación desde código privilegiado y no aislado desshd - Las pruebas se realizaron en máquinas virtuales i386 y en una red estable con alrededor de 10 ms de jitter de paquetes; en Debian 12.5.0 con OpenSSH 9.2p1, obtener una shell root tomó en promedio unas 10,000 ejecuciones y entre 6 y 8 horas con
MaxStartups=100yLoginGraceTime=120 - OpenSSH lo corrigió el 6 de junio de 2024 con el commit
81c1099; si actualizar o recompilar es difícil,LoginGraceTime 0puede bloquear el RCE, aunque sigue existiendo riesgo de DoS por agotamiento de conexiones MaxStartups
Dónde ocurre la vulnerabilidad
- El problema en
sshdde OpenSSH comienza en el manejador SIGALRM que se ejecuta antes de la autenticación- Si el cliente no se autentica dentro de
LoginGraceTime, el manejadorSIGALRMse invoca de forma asíncrona - Ese manejador llama funciones que no son async-signal-safe, como
syslog() - El valor por defecto es
LoginGraceTime=120segundos, y en versiones antiguas de OpenSSH era de 600 segundos
- Si el cliente no se autentica dentro de
- Esta vulnerabilidad es una regresión de CVE-2006-5051
- CVE-2006-5051 fue una condición de carrera en el manejador de señales de OpenSSH anterior a 4.4 reportada por Mark Dowd en 2006
- En octubre de 2020, el commit
752250cde OpenSSH 8.5p1 eliminó por error#ifdef DO_LOG_SAFE_IN_SIGHANDdesigdie()
- El impacto por versión está claramente delimitado
- Antes de OpenSSH 4.4p1: es vulnerable si no se backportearon los parches relacionados con CVE-2006-5051 o CVE-2008-4109
- OpenSSH 4.4p1 y hasta antes de 8.5p1: no es vulnerable a esta carrera porque
sigdie()se cambió por una llamada segura a_exit(1) - OpenSSH 8.5p1 y hasta antes de 9.8p1: vuelve a ser vulnerable por la eliminación de la protección
Entornos afectados y excepciones
- El objetivo de la explotación remota es Linux basado en glibc
- El
syslog()de glibc llama internamente funciones async-signal-unsafe comomalloc()yfree() - El código vulnerable está en código privilegiado de
sshd, sin sandbox y ejecutándose con privilegios completos - Como resultado, es posible ejecutar código remoto como root sin autenticación
- El
- No se investigaron otras libc ni otros sistemas operativos
- OpenBSD no es vulnerable
- El manejador
SIGALRMde OpenBSD llamasyslog_r()en lugar desyslog() syslog_r()es una versión más segura para señales asíncronas creada por OpenBSD en 2001
- El manejador
Supuestos de la investigación de explotación remota
- Para explotar esta condición de carrera en remoto, hay que resolver tres problemas
- Se necesita una ruta de código que deje a
sshden un estado inconsistente siSIGALRMinterrumpe en el momento adecuado - Hay que llegar a esa ruta de código y aumentar la probabilidad de interrumpirla en el momento correcto
- Ese timing debe poder ajustarse incluso en una red remota
- Se necesita una ruta de código que deje a
- La investigación comenzó en un entorno i386 con versiones antiguas de OpenSSH y luego se extendió a versiones recientes, en lugar de enfrentarse de entrada a las mitigaciones más modernas
- Las condiciones experimentales tienen limitaciones claras
- Solo se probaron máquinas virtuales, no servidores bare-metal
- La red era un enlace relativamente estable con unos 10 ms de jitter de paquetes
- Varias partes del exploit todavía podrían mejorarse
- Ya comenzó el trabajo del exploit para amd64, pero es mucho más difícil por un ASLR más fuerte
Pruebas contra OpenSSH antiguo
-
Debian 3.0r6, OpenSSH 3.4p1
- El objetivo fue
SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3, un entorno de 2005 sobre Debian 3.0r6 - Esa versión de Debian fue la primera con privilege separation activado por defecto y ya incluía los principales parches de vulnerabilidades de la época
- La explotación aprovecha la interrupción de
free()y un estado inconsistente del heap- Una llamada a
free()en el código de parsing de claves públicas se interrumpe conSIGALRM - Después se aprovecha el estado inconsistente del heap en otro
free()dentro depacket_close()
- Una llamada a
- glibc 2.2.5 no tenía mitigaciones para la técnica
unlink()de Solar Designer - El ataque sobrescribe
__free_hookpara redirigir el flujo de ejecución hacia la dirección del shellcode en el heap - Esta versión de Debian no tiene ASLR ni NX
- Tras mejorar el timing, se requirieron en promedio unas 10,000 ejecuciones
- Con
MaxStartups=10yLoginGraceTime=600, obtener una shell root remota tomó alrededor de una semana en promedio
- El objetivo fue
-
Ubuntu 6.06.1, OpenSSH 4.2p1
- El objetivo fue
SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3, un entorno de 2006 sobre Ubuntu 6.06.1 - Fue la última versión de Ubuntu que seguía siendo vulnerable a CVE-2006-5051
- glibc 2.3.6 toma un lock obligatorio al entrar a funciones de la familia malloc, así que interrumpir un malloc y luego abusar de otra llamada malloc termina en deadlock
- La ruta final de explotación usa PAM
pam_start()establece el puntero globalsshpam_handledesshd- Si
_pam_add_handler()se interrumpe, puede quedar sin inicializar el camponext - Cuando
pam_end()se llama desde el manejadorSIGALRM, puede pasar un puntero arbitrario afree()
- La antigua técnica
unlink()de glibc ya estaba bloqueada, así que se usó la versión fastbin de House of Mind de Malloc Maleficarum - La fake arena apunta a
.got.pltdesshdy sobrescribe la entrada de_exit()con la dirección del shellcode en el heap - El heap de esta versión de Ubuntu es ejecutable por defecto
- Se requirieron en promedio unas 10,000 ejecuciones
- Con
MaxStartups=10yLoginGraceTime=120, obtener una shell root remota tomó entre 1 y 2 días en promedio - Un atacante con mala suerte puede bloquear por deadlock las 10 conexiones de
MaxStartupsantes de obtener la shell root
- El objetivo fue
Prueba en Debian 12.5.0, OpenSSH 9.2p1
-
syslog()y la ruta malloc de glibc- El objetivo fue
SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2, el entorno stable actual de 2024 en Debian 12.5.0 - Este entorno es vulnerable a la regresión de CVE-2006-5051
- El manejador
SIGALRMen esta versión no llamapacket_close()nipam_end(), sino que termina en la ruta desyslog()grace_alarm_handler()llamasigdie()sigdie()llamasyslog()pasando porsshlogv()ydo_log()
- El
syslog()de Debian glibc 2.36 llama malloc la primera vez que se ejecuta- En la ruta
__localtime64_r()se llama__tzfile_read() fopen()llamamalloc(304)para una estructura FILE- También se llama
malloc(4096)para un buffer interno de lectura
- En la ruta
- Desde octubre de 2017, malloc de glibc ya no toma locks obligatorios en escenarios de un solo hilo
- En procesos de un solo hilo como
sshd, esto abre la posibilidad de explotar contención en malloc
- El objetivo fue
-
Condiciones de ASLR y limitaciones de i386
- El entorno Debian 12.5.0 i386 tiene una debilidad de ASLR
- El PIE de
sshd, el heap, la mayoría de las librerías y la stack suelen estar aleatorizados - Pero glibc siempre se mapea en
0xb7200000o0xb7400000 - Es posible acertar la dirección de glibc con una probabilidad del 50%
- El exploit asume que glibc está mapeada en
0xb7400000 - Esto se debe a que esa dirección era un poco más frecuente que
0xb7200000
-
Inconsistencia del heap y explotación de la estructura FILE
- La ruta malloc elegida es la ruta split que divide un free chunk grande en dos
- Se generan el chunk que se devuelve y un chunk remainder
- Si
SIGALRMinterrumpe después de enlazar el remainder chunk en la unsorted list pero antes de inicializar su campo size, se produce una inconsistencia del heap - El atacante controla el campo size del remainder chunk usando datos residuales de una asignación previa del heap
- Hace que el remainder chunk parezca más grande de lo real y se superponga con otros chunks del heap
- Cuando malloc dentro del manejador
SIGALRMusa ese chunk, la memoria del heap se corrompe - El objetivo es la estructura FILE que
fopen()asigna en el heap dentro de__tzfile_read()- Con corrupción limitada del heap, se sobrescribe 1 byte de
_vtable_offsetde la estructura FILE - Eso hace que las funciones libio de glibc busquen el puntero de vtable en otro offset, no en el habitual
- El atacante controla en esa ubicación un fake vtable pointer y un puntero
_codecvtmediante datos residuales de asignaciones previas del heap
- Con corrupción limitada del heap, se sobrescribe 1 byte de
- En glibc i386, esta técnica permite llamar un puntero de función arbitrario
__fctdurante__fread_unlocked() - Parece que glibc amd64 no usa
_vtable_offset
-
Layout del heap y 27 ventanas de carrera
- Como una sola ventana de carrera pequeña no basta para ganar la condición de carrera, se crean 27 pares de large hole y small hole
- 28 pares superarían
PACKET_MAX_SIZEde 256 KB - El paquete final fuerza la secuencia
malloc(~4KB), malloc(304), malloc(~4KB), malloc(304) - Si el split de cada large hole se interrumpe en el momento exacto, el
fopen()del manejadorSIGALRMasigna el small hole correspondiente como estructura FILE - Para construir el layout del heap se usa el código de parsing de claves públicas de
sshd- Las rutas
cert_parse()ycert_free()ejecutan secuencias demalloc()yfree()casi arbitrarias - Como no se encontró una memory leak, se usaron chunks de tcache como barrier chunk temporal
- Las rutas
- Se envían cinco tipos de paquetes de clave pública
- a/ hacen que asignaciones de heap no controlables entren como chunks de tcache
- b/ crean 27 pares de large/small hole y barrier chunk
- c/ escriben fake header, fake footer, fake vtable y el puntero
_codecvt - d/ asignan y liberan una cadena grande de casi 256 KB para mover los holes desde la unsorted list a cada bin de malloc
- e/ fuerzan la secuencia final de malloc y abren 27 ventanas pequeñas de carrera
-
Estrategia de timing para versiones modernas
- La estrategia de timing basada en feedback usada en versiones antiguas no funcionó contra OpenSSH 9.2p1
- El quinto parsing de clave pública tarda unos 10 ms, así que la ventana de carrera grande se vuelve demasiado amplia
user_specific_delay(), introducida en OpenSSH 7.8p1, retrasa la respuesta hasta unos 9 ms y rompe el feedback anterior- La nueva estrategia compara el tiempo de respuesta de dos tipos de errores intencionales
- Se envía un paquete que provoca un error justo antes del parsing de la clave pública
- Se envía un paquete que provoca un error justo después del parsing de la clave pública
- La diferencia entre ambos tiempos mide el tiempo del último parsing de clave pública
- Con esta estrategia se gana la condición de carrera en promedio tras unas 10,000 ejecuciones
- Con
MaxStartups=100yLoginGraceTime=120, ganar la condición de carrera toma entre 3 y 4 horas en promedio - Por ASLR, obtener la shell root remota requiere entre 6 y 8 horas en promedio
Estado del exploit en amd64
- El objetivo amd64 es Rocky Linux 9
- La imagen objetivo es
Rocky-9.4-x86_64-minimal.iso - OpenSSH 8.7p1 es vulnerable a esta condición de carrera en el manejador de señales
- glibc se mapea en múltiplos de 2 MB por una debilidad de ASLR, lo que vuelve más potente el partial pointer overwrite
- La imagen objetivo es
- El
syslog()de glibc 2.34 en Rocky Linux 9 llama internamente__open_memstream()- Asigna una estructura FILE en el heap con
malloc() - También llama
calloc(),realloc()yfree(), dando más margen de maniobra
- Asigna una estructura FILE en el heap con
- A partir del primitive de corrupción de heap, las dos estructuras FILE asignadas en heap y 21 bits fijos de la dirección de glibc, consideran posible la explotación también en amd64
- Estiman que tardará más que las 6 a 8 horas de i386, pero esperan que sea menos de una semana
- También hay una observación aparte sobre Ubuntu 24.04
- Ubuntu 24.04 no vuelve a aleatorizar el ASLR del proceso hijo de
sshd, sino que lo aleatoriza solo una vez al arrancar - La causa se rastrea al
systemd-socket-activation.patchque desactivarexec_flag - En general es una mala decisión, pero en esta vulnerabilidad impide la explotación porque
syslog()dentro del manejadorSIGALRMno es la primera llamada asyslog(), así que no invoca funciones malloc - Parche relacionado: https://git.launchpad.net/ubuntu/+source/…
- Ubuntu 24.04 no vuelve a aleatorizar el ASLR del proceso hijo de
Parche y mitigaciones
- OpenSSH corrigió esta condición de carrera el 6 de junio de 2024 con el commit
81c1099- 81c1099: agrega una función para que
sshd(8)penalice el comportamiento problemático del cliente - Mueve el código async-signal-unsafe fuera del manejador SIGALRM de
sshdhacia el proceso listener para procesarlo de forma síncrona
- 81c1099: agrega una función para que
- Esta corrección depende del gran commit
81c1099y del aún mayor commit de defense-in-depth03e3de4, así que podría ser difícil de backportear - Si backportearlo es complicado, se puede eliminar o comentar el código async-signal-unsafe en
sshsigdie()para que solo llame_exit(1) - Si no es posible actualizar ni recompilar, se puede poner
LoginGraceTimeen0en el archivo de configuración- Esa configuración bloquea la ejecución remota de código descrita en este advisory
- Pero deja el sistema vulnerable a DoS por agotamiento de todas las conexiones
MaxStartups
Cronograma de divulgación
- 2024-05-19: se contactó a los desarrolladores de OpenSSH y luego siguieron iteraciones de parche y revisión
- 2024-06-20: se contactó a
distros@openwall - 2024-07-01: se publicó en la fecha de lanzamiento coordinada
1 comentarios
Opiniones de Hacker News
Curiosamente, parece que la corrección del RCE se “coló” públicamente hace casi un mes
Cuando PerSourcePenalties está activado, sshd(8) vigila el estado de salida de los procesos hijos de sesión antes de la autenticación y registra condiciones como fallos repetidos de autenticación o caídas de sshd como penalizaciones durante cierto tiempo para la dirección del cliente
https://github.com/openssh/openssh-portable/commit/81c1099d2...
Más que un parche que pueda analizarse al revés y dar pistas al atacante, parece un efecto secundario bastante ingenioso: cambia la estructura del binario, elimina una vulnerabilidad concreta y además mitiga toda esa familia de exploits
El cambio anterior era una función anunciada para manejar conexiones basura, y solo mitiga también esta vulnerabilidad al hacer más difícil ganar la condición de carrera
Discusión previa: https://news.ycombinator.com/item?id=40610621
Me pregunto si la gente solo lee el primer comentario del hilo, lo vota y se va con una impresión equivocada
Un fragmento de las notas de lanzamiento de OpenSSH es interesante
“Se ha demostrado una explotación exitosa en sistemas Linux/glibc de 32 bits con ASLR activado. En condiciones de laboratorio, el ataque requiere mantener conexiones continuas hasta el máximo permitido por el servidor durante un promedio de 6 a 8 horas. Se cree que también es posible en sistemas de 64 bits, pero aún no se ha demostrado. Es probable que estos ataques mejoren.”
https://www.openssh.com/releasenotes.html
Al ver el diff [1] que introdujo el bug, según el análisis el problema es que
sigdie()pasó de estar envuelto en#ifdef DO_LOG_SAFE_IN_SIGHANDa una refactorización en la quesshsigdie()llama directamente asshlogv(), y se perdió el #ifdef¿Qué podría haberlo evitado? ¿Había que revisar más los pull requests? Me sorprende que un software del que depende el mundo entero para conexiones seguras parezca estar mantenido, en la práctica, por dos personas [2]
[1] https://github.com/openssh/openssh-portable/commit/752250caa...
[2] https://github.com/openssh/openssh-portable/graphs/contribut...
En este caso, habría ayudado que hubiera un comentario explicando por qué se necesitaba el
#ifdef. Por ejemplo: “el código aquí debe ser seguro para señales asíncronas, y el estado de los locks puede ser indeterminado”Aunque, para ser honestos,
getrlimittampoco aparece en esta lista: https://man7.org/linux/man-pages/man7/signal-safety.7.htmlAun así, si se hubiera eliminado o modificado código con un comentario sobre seguridad frente a señales asíncronas, probablemente habría llamado la atención en la revisión. En el código citado, solo algo como
SAFE_IN_SIGHANDsugiere que este código debe ser seguro dentro de un manejador de señalessyslogreentrante y segura para señales asíncronas, es posible que quien escribió este código simplemente haya asumido que el cambio era seguroPuede que haya olvidado o no supiera que en otras plataformas que los desarrolladores de OpenBSD ssh en realidad no afirman soportar, todavía se usan funciones que no son seguras para señales asíncronas
No tienes derecho a recibir nada de los desarrolladores open source. Ellos también pueden equivocarse y pueden decidir por sí mismos cuántos mantenedores o revisores tener
https://gist.github.com/richhickey/1563cddea1002958f96e7ba95...
#ifdefsin justificación, como en [1]También vale la pena leer las notas de la versión: https://www.openssh.com/releasenotes.html
En realidad, esto es una variante de un interesante bug de condición de carrera de señales. Según el informe de la vulnerabilidad, “OpenBSD no es especialmente vulnerable porque su manejador de SIGALRM llama a
syslog_r(), una versión desyslog()más segura para señales asíncronas que OpenBSD creó en 2001”.Es decir, una mitigación de seguridad de señales llevó a los desarrolladores de OpenBSD a poner código no trivial dentro de un manejador de señales, y cuando ese código se portó a otros sistemas, dejó de ser seguro. Si se hubiera refactorizado para minimizar el código dentro del manejador de señales, siguiendo la sabiduría general y las convenciones del código Unix, este bug se habría evitado.
Con el tiempo, es demasiado fácil que en algún punto de las llamadas transitivas se mezcle una llamada que no sea segura para señales asíncronas, y tampoco siempre es evidente que esa ruta sea alcanzable desde un contexto de señal.
Después de actualizar mis instancias de OpenSSH, vi que estaban enlazadas contra musl y no contra glibc, así que revisé si
syslog(3)de musl también hace asignaciones y, por lo tanto, si sería fácilmente explotable de la misma manera.A simple vista no parece ser así: https://github.com/bminor/musl/blob/master/src/misc/syslog.c
Todo lo que hay ahí son variables en la pila o variables estáticas protegidas con locks para evitar reentradas. Las llamadas a
{d,sn,vsn}printf()tampoco asignan memoria en musl, aunque en glibc podrían hacerlo. ¿Se me está escapando algo?Aun así, si hay un interbloqueo dentro de
sigalrm, podría impedir la limpieza de conexiones y terminar en una denegación de servicio.Ya salió un parche para FreeBSD.
No está claro si está afectado. La explotación conocida solo era posible en glibc y FreeBSD no usa glibc, pero es mejor ir por lo seguro.
https://www.freebsd.org/security/advisories/FreeBSD-SA-24:04...
Según el informe, si no puedes actualizar o recompilar sshd, basta con establecer
LoginGraceTimeen 0 en el archivo de configuración para corregir esta condición de carrera del manejador de señales.En ese caso, sshd quedaría vulnerable a una denegación de servicio por agotamiento de todas las conexiones de
MaxStartups, pero quedaría a salvo de la ejecución remota de código descrita en este aviso.Por lo tanto, configurar
LoginGraceTime 0ensshd_configparece servir como mitigación.¿Entonces no sería peor?
Ya salió un parche para Debian 12, y Debian 11 no está afectado.
https://security-tracker.debian.org/tracker/CVE-2024-6387
apt updateyupgradeen un servidor Debian 12, y lo único que se actualizó fueron los paquetes de OpenSSH.Un hallazgo realmente muy bueno.
Aunque no estoy en una posición en la que trabaje directamente en esto, a menudo se siente que, en la investigación de seguridad, para “ganar” no basta con encontrar y corregir un problema aislado, o recibir una recompensa por él, sino que hay que encontrar toda la cadena que llegue hasta el acceso remoto.
Me pregunto si no debería ser suficiente encontrar un solo hueco, por ejemplo una corrupción de memoria o un escape de sandbox. Hoy hay tantos problemas pequeños que quizá, para que la gente realmente los tome en serio o pague bug bounties, haya que mostrar el hack completo hasta el final.
Por ejemplo, si una app se crashea cuando recibe una entrada de confianza mal formada, pero por la naturaleza de esa app no está pensada para quedar expuesta a un adversario y, en la práctica, eso tampoco ocurriría, la mayoría lo vería simplemente como un bug, no como un bug de seguridad. Sería bueno corregirlo, pero no está al mismo nivel, y ese tipo de cosas tampoco es tan difícil de encontrar.
Por eso hace falta distinguir entre bugs de seguridad “reales” como este caso y bugs sin impacto de seguridad, y es muy importante demostrar que el problema es explotable.
Como siempre habrá una cantidad interminable de bugs sin impacto de seguridad, no parece que este requisito de demostración vaya a desaparecer pronto.
Es así por diseño, y los usuarios pueden serializar y deserializar cualquier cosa, incluso funciones lambda. Mi biblioteca solo está pensada para procesar datos de fuentes confiables.
Hasta donde sé, nadie usa esta biblioteca para procesar datos no confiables. Una biblioteca popular usa la mía para leer archivos de configuración, pero ellos consideran esos archivos de configuración como datos confiables. Y no es mi trabajo controlar cómo otras personas usan mi biblioteca.
En ese caso, ¿sería correcto registrar un CVE de máxima prioridad diciendo que mi proyecto tiene una vulnerabilidad de ejecución remota de código?
Las recompensas siempre se pagan por la primera categoría. Los reportes de la segunda categoría, si no incluyen una prueba de concepto o una demostración de explotabilidad, incluso pueden dañar la reputación o la señal.
Las debilidades que no se vuelven explotables hasta que se cumplen ciertas condiciones existen casi siempre. Incluso en competencias como Pwn2Own, suele verse cómo se encadenan varias vulnerabilidades hasta tomar control del dispositivo, y cómo algunas quedan sin parchear durante años. Los investigadores a veces se guardan esas debilidades durante mucho tiempo para maximizar el impacto.
Es triste, pero así es la realidad.
Notas de la versión de OpenSSH: https://www.openssh.com/txt/release-9.8
Parche mínimo para quienes no pueden o no quieren actualizar: https://marc.info/?l=oss-security&m=171982317624594&w=2