Reports
BAJA[TLS Y CRIPTOGRAFÍA]#4112709

curl con libssh2 compara por prefijo las entradas [host]:puerto de known_hosts y fuerza el tipo de clave de otro host

Una comparación con strncmp hace que `[10.0.0.1]:2222` también valga para 10.0.0.12. curl fuerza entonces el tipo de clave de otro host y un atacante en medio puede convertir un MISMATCH en MISSING.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

El fallo está en ssh_force_knownhost_key_type(), en lib/vssh/libssh2.c, la función de curl que elige el tipo de clave de host que se va a exigir en la conexión SSH a partir del fichero known_hosts. Cuando el servidor usa un puerto distinto del predeterminado, ssh escribe la entrada con la forma [nombre]:puerto, y curl la compara así:

strncmp(store->name + 1, conn->origin->hostname, kh_name_size)

kh_name_size es la longitud del nombre que aparece en la entrada, no la del host al que se conecta curl. Por eso la comparación solo comprueba que la entrada sea un prefijo del host, no que coincidan los dos nombres:

  • [10.0.0.1]:2222 también coincide con 10.0.0.12.
  • [git.example.co]:2222 también coincide con git.example.com.

La consecuencia encadena tres pasos. Si una entrada de otro host queda en el fichero antes que la entrada propia y tiene un tipo de clave distinto, curl fuerza ese otro tipo para la conexión. Un servidor atacante solo tiene que ofrecer ese tipo. Después, la comprobación de ssh_knownhost() no encuentra ninguna entrada de ese tipo para el host, porque con libssh2 1.11.1 libssh2_knownhost_checkp() solo informa de MISMATCH si la clave es del mismo tipo. El resultado es que el callback CURLOPT_SSH_KEYFUNCTION recibe CURLKHMATCH_MISSING en lugar de CURLKHMATCH_MISMATCH.

Es el mismo desenlace de CVE-2026-87865, pero alcanzado con entradas en claro en vez de entradas con hash. El código procede del commit 272282a054, incluido en curl 7.69.0.

El investigador lo comprobó sobre master en el commit 9a6d4ffc1 (13 de octubre de 2026), con una compilación de depuración en Debian forky/sid x86_64 y libssh2 1.11.1:

curl 8.23.0-DEV (x86_64-pc-linux-gnu) libcurl/8.23.0-DEV OpenSSL/3.6.5 zlib/1.3.2 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libssh2/1.11.1 nghttp2/1.70.0 OpenLDAP/2.6.14
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt mqtts pop3 pop3s rtsp scp sftp smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli Debug HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz SSL threadsafe UnixSockets zstd

Los datos del reporte: enviado el 14 de octubre de 2026, divulgado el 26 de octubre y marcado como resuelto, con severidad baja. El autor avisa de que lo escribió con ayuda de IA y de que revisó el texto final varias veces.

Pasos de reproducción

La prueba se apoya en dos casos de la propia batería de tests de curl, adjuntos al reporte original.

  1. Copia test3990.pl en tests/libtest/.
  2. Copia test3990 y test3991 en tests/data/.
  3. El caso test3990 se ejecuta contra el servidor SFTP de la batería de tests, que por defecto tiene una clave de host RSA, usando este known_hosts:
[127.0.0]:PORT ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAqmXRY8k+W9GtpWxYru6ELAXYHdXqjf33ziITGTiJUN
[127.0.0.1]:PORT ssh-rsa <the server's real key>
  1. Ejecuta los dos casos:
cd tests && ./runtests.pl 3990 3991
  1. test3990 falla, y la traza muestra que curl ha usado la entrada [127.0.0] para conectar a 127.0.0.1:
SSH: found host '127.0.0.1' in 'log/known3990'
SSH: set 'ssh-ed25519' as hostkey type
Failure establishing ssh session: -5, Unable to exchange encryption keys

La negociación falla únicamente porque el servidor de pruebas no tiene clave ed25519; un servidor intermedio malicioso ofrecería una.

  1. test3991 sirve de control. Su primera entrada es para [127.0.0.1.2]:PORT, que no es prefijo del host, así que curl fuerza ssh-rsa y la transferencia funciona.

Impacto

El ataque exige poder interceptar conexiones y que el host objetivo tenga un nombre que prolongue el de una entrada [nombre]:puerto anterior con el mismo puerto. Lo más probable es que ocurra con direcciones IP, como 10.0.0.1 y 10.0.0.12.

Si además la aplicación tiene un callback CURLOPT_SSH_KEYFUNCTION que confía en los hosts nuevos, aceptará la clave del atacante en vez de rechazar la discrepancia, y se autenticará contra él. El resto de condiciones son las de CVE-2026-87865: backend libssh2 y callback de clave aportado por la aplicación.

Sin atacante también hay un fallo funcional: una conexión legítima a ese host se rompe, bien en el intercambio de claves, bien porque su clave se da por ausente, como en la prueba anterior.

Qué aprender de este caso

  • Desconfía de cualquier strncmp cuya longitud salga del valor guardado y no de los dos valores comparados: es una comparación de prefijo. Para exigir igualdad hay que comprobar también que las longitudes coinciden.
  • Al revisar parsers de formatos con delimitadores, como [host]:puerto, mira qué pasa justo después del nombre extraído. Probar valores que prolongan uno ya guardado (10.0.0.1 frente a 10.0.0.12, example.co frente a example.com) destapa este error enseguida.
  • Vigila los códigos que distinguen "clave cambiada" de "host desconocido". Si un atacante puede pasar del primero al segundo, cualquier política de confiar en hosts nuevos se convierte en una vía para saltarse la verificación.
  • Cuando se corrige un CVE, busca otras rutas que lleven al mismo resultado: aquí el desenlace de CVE-2026-87865 se alcanzaba con entradas en claro en lugar de con hash.