Resumen
libcurl guarda las conexiones abiertas en un pool para reutilizarlas. Cuando una conexión ya está autenticada con un esquema ligado a la conexión (NTLM o Negotiate/Kerberos), reutilizarla para otro origen equivale a hablar con el segundo servidor usando la identidad conseguida ante el primero. Para evitarlo, lib/url.c tiene varias funciones que deciden si una conexión existente sirve para una transferencia nueva.
El investigador hadd0ck encontró que el commit bd10924b4, parte de la iniciativa "Patch the Planet", rompió una de esas comprobaciones. El commit cerraba otro problema (#22040) añadiendo el campo conn->creds_origin y comparándolo con data->state.origin, y eso estaba bien. El fallo está en lo que eliminó: en la versión anterior (c2b050e4e496), la rama de url_match_auth_ntlm que trataba las credenciales distintas solo marcaba una conexión como candidata si tanto el estado NTLM como el de Negotiate estaban en reposo:
#ifdef USE_SPNEGO
if((conn->http_ntlm_state == NTLMSTATE_NONE) &&
(conn->http_negotiate_state == GSS_AUTHNONE)) {
#else
if(conn->http_ntlm_state == NTLMSTATE_NONE) {
#endif
m->found = conn;
}
bd10924b4 sustituyó toda esa rama por un único return temprano que depende de conn->creds, y la condición sobre GSS_AUTHNONE desapareció.
El problema aparece en el caso más habitual de inicio de sesión único con Kerberos, --negotiate sin -u:
Curl_http_output_authno aplica a Negotiate la salida rápida por "no hay credenciales", así que la autenticación se hace con la caché de tickets ydata->state.credsqueda enNULL.Curl_input_negotiateasociaconn->creds_original origen, peroCurl_creds_link(&conn->creds, NULL)dejaconn->credsenNULL. Por tanto, la nueva comprobación nunca se activa.- Una transferencia posterior que acepte NTLM (por ejemplo con
--anyauth, porqueCURLAUTH_ANYsolo excluyeCURLAUTH_DIGEST_IEyCURLAUTH_HTTPSIG) superaurl_match_auth: en HTTPneedle->credsesNULLyCurl_creds_same(NULL, NULL)devuelveTRUE. - Llega a la rama de estado NONE (
url.c:937), donde la comparación de orígenes también depende deconn->credsy no se ejecuta. Sigue hastaurl.c:962, que asignam->found = conny devuelveFALSEporque el estado NTLM está en reposo. - Como
url_match_conndevuelveFALSE, nunca se llega aurl_match_auth_nego, que es el único comparador que contrasta los dos orígenes. Al final,url_match_resultusamatch->foundsi no ha encontrado nada mejor.
Hay una condición más: los dos orígenes tienen que estar detrás del mismo proxy de reenvío. lib/proxy.c pone como conn->origin el proxy, de modo que todos los orígenes que pasan por él acaban en el mismo grupo del pool de conexiones.
Versión afectada según el reporte: curl 8.21.0-DEV (en desarrollo, sin publicar), compilada con GSS-API, Kerberos, NTLM y SPNEGO.
Pasos de reproducción
Hace falta git, cmake, un compilador de C, OpenSSL, MIT Kerberos y uv.
- Compilar curl en el commit afectado:
git clone https://github.com/curl/curl.git curl-src
cd curl-src && git checkout bd10924b47c8bf94127eaee8c56e7b5e5418da46
cmake -S . -B bld \
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_CURL_EXE=ON \
-DCURL_USE_OPENSSL=ON -DCURL_USE_GSSAPI=ON -DCURL_ENABLE_NTLM=ON \
-DCURL_USE_LIBPSL=OFF -DUSE_LIBIDN2=OFF -DUSE_NGHTTP2=OFF \
-DCURL_ZLIB=OFF -DCURL_BROTLI=OFF -DCURL_ZSTD=OFF -DENABLE_CURL_MANUAL=OFF
cmake --build bld -j8 --target curl
- Comprobar que
./bld/src/curl -VmuestraSPNEGOyNTLMenFeatures.
- Descomprimir el archivo con la prueba de concepto que adjuntó el investigador (no está disponible públicamente), indicarle el binario y ejecutarla:
unzip curl-22040-ntlm-connection-reuse.zip
cd curl-22040-ntlm-connection-reuse/pocs/ntlm-cross-origin-reuse
export CURL_BIN=/path/to/curl-src/bld/src/curl
./run.sh
- Resultado en
bd10924b4:
conn origin Authorization status
1 a.internal NTLM type1 401
1 b.internal NTLM type3 200
RESULT: REUSED ACROSS ORIGINS
La misma conexión (la número 1) envía el mensaje NTLM type1 para a.internal, recibe el desafío de ese origen y después envía el type3 en una petición para b.internal, que responde 200. Aunque el texto del reporte habla de Negotiate, la salida de la prueba muestra el intercambio con mensajes NTLM.
Impacto
Una petición destinada al host B sale por una conexión que ya se autenticó para el host A, así que B ve la sesión autenticada de A y las credenciales propias de la transferencia no se usan. La petición a B queda asociada a la identidad que el usuario tenía en A sin que se haya hecho ninguna comprobación de credenciales en ese camino de reutilización.
El escenario realista que describe el investigador es una aplicación que hace inicio de sesión único con Kerberos contra varios hosts internos a través de un proxy corporativo. HackerOne lo clasifica como autenticación incorrecta de severidad baja, con el identificador CVE-2026-87861. El reporte no detalla cómo se corrigió.
Qué aprender de este caso
- Cuando una corrección reescribe una condición, compara la versión anterior con la nueva término a término: aquí el arreglo de #22040 era correcto, pero se perdió por el camino la comprobación
GSS_AUTHNONE, que protegía otro caso. - En un pool de conexiones, revisa qué pasa cuando las credenciales son
NULLen ambos lados. SiNULL == NULLse considera "mismas credenciales", la autenticación implícita (caché de Kerberos, SSO) puede saltarse comparaciones pensadas para usuario y contraseña explícitos. - Las comprobaciones de origen no deberían depender de que exista un objeto de credenciales: si la conexión está autenticada con un esquema ligado a la conexión, su origen tiene que coincidir siempre, haya
credso no. - Los proxies que agrupan varios orígenes en un mismo grupo de conexiones son el sitio donde buscar este tipo de fallos: pon a prueba la reutilización con dos hosts distintos detrás del mismo proxy y NTLM o Negotiate activos.