Reports
BAJA[TLS Y CRIPTOGRAFÍA]#3975408

CVE-2026-87865: libcurl con libssh2 presenta como "nueva" la clave falsa de un host SSH ya fijado en known_hosts

Con un known_hosts de nombres hasheados, libcurl usaba la primera entrada anónima sin comprobar su hash. Una clave hostil para un host ya fijado llegaba al callback como MISSING en vez de MISMATCH.

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 el backend SSH de libcurl basado en libssh2, en la parte que consulta el fichero known_hosts de OpenSSH antes de llamar al callback que la aplicación registra con CURLOPT_SSH_KEYFUNCTION. Ese callback recibe un veredicto sobre la clave del servidor: CURLKHMATCH_OK, CURLKHMATCH_MISMATCH (el host es conocido pero la clave no coincide) o CURLKHMATCH_MISSING (el host no aparece en el fichero).

Cuando las entradas de known_hosts guardan el nombre de host como hash (formato hashed, con |1|...), su nombre público es NULL. El código de lib/vssh/libssh2.c (líneas 653-687 en el commit 68720b4837284335b2d63cb358f8f6ce65f5bc55) tomaba la primera entrada hasheada que encontraba como si fuera la del destino y forzaba su algoritmo de clave, sin comprobar que el hash del nombre de host correspondiera realmente al servidor al que se conecta.

La consecuencia: si delante de la entrada legítima del host hay otra entrada hasheada de un host distinto con otro tipo de clave, el veredicto se tuerce. Un servidor hostil puede hacer que un host ya fijado parezca uno visto por primera vez. Una aplicación que confíe en el primer uso (TOFU, trust on first use) aceptará entonces la clave falsa.

El reporte tiene asignado el CVE-2026-87865. El investigador aclara que es un origen distinto al de CVE-2026-9547, cuyo aviso dice que libssh2 no está afectado.

Pasos de reproducción

El escenario necesita un known_hosts con dos entradas hasheadas en este orden:

  1. Una entrada de un host ajeno con clave RSA.
  2. La entrada del host de destino, fijada con clave Ed25519.

Con eso, el desarrollo es el siguiente:

  1. libcurl coge la primera entrada hasheada (la RSA ajena), la da por buena para el destino y fuerza el algoritmo RSA.
  2. Un servidor hostil que presenta una clave RSA negocia la conexión sin problema.
  3. Al comprobar, libssh2 descarta la entrada Ed25519 del host real porque no es del tipo negociado y devuelve "no encontrado".
  4. curl traduce ese resultado a CURLKHMATCH_MISSING y se lo pasa al callback, cuando lo correcto sería CURLKHMATCH_MISMATCH.

Para reproducirlo sobre curl 8.21.0 (commit 68720b4837284335b2d63cb358f8f6ce65f5bc55), se compila libcurl como biblioteca compartida con soporte libssh2:

cmake -S . -B build -DCURL_USE_LIBSSH2=ON -DBUILD_SHARED_LIBS=ON

Después se extrae el adjunto del reporte y se ejecuta la prueba de concepto:

python3 poc.py --build-dir /absolute/path/to/build --output run.json

Resultado: las dos ejecuciones del ataque terminan con código 0, el callback recibe MISSING, el servidor hostil recibe la contraseña de prueba y libcurl devuelve ATTACKER_SFTP_BODY. En los casos de control, con la entrada del destino en primer lugar o con ambas entradas del mismo tipo, el resultado es MISMATCH y no se envía ninguna contraseña.

Impacto

Un atacante en la red que controle o intercepte el servidor SSH al que se conecta la víctima puede hacer pasar un host ya fijado por uno nuevo ante un callback que confía en el primer uso. Si el callback lo acepta, libcurl se autentica contra el servidor hostil. En la prueba, el servidor recibió la contraseña SSH configurada y libcurl aceptó datos SFTP controlados por el atacante.

Afecta a transferencias SFTP y SCP de aplicaciones que usan libcurl con libssh2, known_hosts hasheado y un callback propio. Según el investigador, ni el callback por defecto ni la herramienta de línea de órdenes curl están afectados. El programa lo clasificó como severidad baja.

Qué aprender de este caso

  • Si una biblioteca recorre known_hosts buscando "la entrada del host", revisa cómo trata las entradas hasheadas (|1|salt|hash): quedarse con la primera cuyo nombre es NULL sin comprobar el HMAC del nombre equivale a usar la clave de cualquier otro host.
  • Al probar validación de claves SSH, el orden de las entradas importa: mezcla en el fichero hosts ajenos con tipos de clave distintos (RSA delante, Ed25519 detrás) y comprueba si el veredicto cambia.
  • Distinguir "host desconocido" de "clave distinta" es la frontera de seguridad en TOFU. Cualquier camino de código que convierta un "no encontrado para este algoritmo" en "host nuevo" debe tratarse como un fallo de verificación, no como un caso normal.
  • En un callback de CURLOPT_SSH_KEYFUNCTION, no aceptes automáticamente CURLKHMATCH_MISSING si tu aplicación ya debería conocer ese host. Mejor fallar y pedir confirmación fuera de banda.