DevOps 4 min de lectura 833 palabras

sshhole: entrar por SSH en Alpine con cualquier contraseña de más de 256 bytes

EN
sshhole: entrar por SSH en Alpine con cualquier contraseña de más de 256 bytes

Ozgun Kultekin, de Wakefield, ha publicado sshhole, un fallo de OpenSSH en sistemas Linux que usan musl como biblioteca C en vez de glibc. En esos sistemas, y bajo ciertas condiciones, sshd acepta como válida cualquier contraseña de más de 256 bytes para las cuentas que, en teoría, no admiten contraseña. Entre ellas, root en Alpine.

Lo que más me ha gustado del write-up es que no hay un único culpable. Es la suma de cuatro decisiones razonables tomadas a lo largo de 46 años, cada una en un proyecto distinto, que juntas abren la puerta.

A quién afecta

Hace falta que se cumpla todo esto a la vez:

  • sshd compilado contra musl y sin PAM.
  • Login por contraseña activado, que es el valor por defecto.
  • Una cuenta cuyo campo de contraseña sea *: usuarios que sólo entran con clave, usuarios de LDAP o el propio root en Alpine.
  • Otra cuenta en la misma máquina con una contraseña real.

Eso apunta a Alpine, OpenWrt, Buildroot, Yocto y muchas imágenes Docker basadas en Alpine. No afecta a distribuciones con glibc (Debian, Ubuntu, Fedora), a instalaciones que usan PAM, a Dropbear ni a OpenBSD.

El caso más grave que señala el autor son las imágenes oficiales de Alpine para AWS y Azure: traen un usuario alpine con doas sin contraseña, así que entrar con esa cuenta equivale a ser root.

Cómo se llega hasta aquí

La historia empieza en 1980, cuando BSD usaba * en /etc/passwd para cuentas como daemon. La idea es sencilla: sshd (y cualquier programa que compruebe contraseñas) calcula el hash de lo que escribe el usuario con la sal guardada y lo compara con el campo almacenado. Ningún hash válido es *, así que nada puede coincidir.

AñoQué pasa
1980BSD usa * para marcar cuentas que nunca aceptan contraseña
2012musl hace que crypt() devuelva "*" cuando falla, en vez de NULL como pide POSIX, para evitar que los programas que no comprueban el NULL se caigan
2012musl añade SHA-256 y SHA-512 crypt con un límite de 256 bytes de contraseña, para evitar ataques de denegación de servicio; si se pasa del límite, falla y devuelve "*"
2016Para corregir la enumeración de usuarios (CVE-2016-6210), OpenSSH empieza a calcular el hash con la sal de otra cuenta real cuando el usuario no tiene una utilizable, para que todas las respuestas tarden lo mismo
2022Alpine 3.16 cambia la contraseña de root de ! a * para que root pueda entrar con clave sin PAM

Juntando las piezas, tal y como lo describe el autor: alguien intenta entrar como root con una contraseña de 300 bytes. Como el campo de root es *, sshd toma prestada la sal SHA-512 de otra cuenta y llama a crypt(). musl ve que la contraseña pasa de 256 bytes, falla y devuelve "*". sshd compara "*" con el campo guardado, que también es "*", y la contraseña es correcta.

flowchart LR
    A["Contraseña de 300 bytes"] --> B["sshd usa la sal de otra cuenta"]
    B --> C["crypt de musl falla · devuelve *"]
    C --> D["Compara * con el campo de root · *"]
    D --> E["Acceso concedido"]

El detalle de glibc es casi anecdótico: en 2013 alguien pidió en Debian que glibc copiara el comportamiento de musl. Debian acabó pasándose a libxcrypt, que en caso de fallo devuelve "*0". Un carácter de diferencia, y por eso las distribuciones con glibc no están afectadas.

Qué hacer

La solución es actualizar OpenSSH a la 10.6p2 o posterior. En Alpine:

apk update && apk upgrade openssh
rc-service sshd restart

Si no puedes actualizar todavía, desactiva el login por contraseña, que en un servidor expuesto debería estar desactivado de todas formas:

echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/00-no-passwords.conf
rc-service sshd restart

Para comprobar una máquina, el autor ha publicado una herramienta en github.com/oz9un/sshole. Si en su salida aparece una línea como root@tu-maquina, una cuenta sin contraseña ha aceptado un login por contraseña, y esa máquina está expuesta.

En contenedores, lo habitual es que no haya sshd, y entonces no hay nada que hacer. Pero si tienes imágenes basadas en Alpine que sí lo incluyen (para depurar, para acceso de mantenimiento, para algún servicio antiguo), conviene revisarlas y reconstruirlas.

Lo que me llevo

Hace una semana escribía sobre el incidente de Alpine con GitLab y Ruby, donde actualizar no bastaba por cómo interactuaban dos piezas. Este caso tiene algo parecido: musl hizo bien en no devolver NULL, el límite de 256 bytes tiene sentido, la sal prestada de OpenSSH arreglaba un problema real y Alpine cambió root a * por una buena razón. Ninguna de esas decisiones es un error si se mira sola, y el fallo sólo aparece cuando coinciden las cuatro.

Es un buen recordatorio de que el valor que devuelve una función cuando falla también forma parte de su contrato. Y de que PasswordAuthentication no sigue siendo una de las líneas más rentables que se pueden poner en la configuración de un servidor.

Fuente: sshhole, Ozgun Kultekin, Wakefield.