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:
- Una entrada de un host ajeno con clave RSA.
- La entrada del host de destino, fijada con clave Ed25519.
Con eso, el desarrollo es el siguiente:
- libcurl coge la primera entrada hasheada (la RSA ajena), la da por buena para el destino y fuerza el algoritmo RSA.
- Un servidor hostil que presenta una clave RSA negocia la conexión sin problema.
- Al comprobar, libssh2 descarta la entrada Ed25519 del host real porque no es del tipo negociado y devuelve "no encontrado".
- curl traduce ese resultado a
CURLKHMATCH_MISSINGy se lo pasa al callback, cuando lo correcto seríaCURLKHMATCH_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_hostsbuscando "la entrada del host", revisa cómo trata las entradas hasheadas (|1|salt|hash): quedarse con la primera cuyo nombre esNULLsin 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áticamenteCURLKHMATCH_MISSINGsi tu aplicación ya debería conocer ese host. Mejor fallar y pedir confirmación fuera de banda.