Automatización de la investigación de seguridad: un motor de IA explota un XSS almacenado en Zulip (CVE-2025-52559)
Este artículo presenta un análisis detallado y práctico, junto con una prueba de concepto, de la explotación de la vulnerabilidad XSS almacenado CVE-2025-52559 en Zulip. Describe cómo se utilizó el motor de pentesting con IA de Ostorlab para automatizar el ciclo completo.
Zulip es una aplicación de chat de código abierto, similar a Slack o Google Chat. Se encontró una vulnerabilidad XSS almacenado en la aplicación, registrada con el identificador CVE-2025-52559.
Ostorlab lleva el último año trabajando en un motor de pruebas de penetración con IA para extender las capacidades de detección a clases de vulnerabilidades que hasta ahora era imposible automatizar. A medida que nos acercamos a la versión final, queremos mostrar cómo la automatización ya puede abordar errores más difíciles de detectar.
Configuración del entorno
Para la configuración, al tratarse de un proyecto de código abierto con entornos fáciles de ejecutar gracias a su repositorio de Zulip para Docker, lo ejecutamos con:
➜ docker-zulip git:(main) ls
certbot-deploy-hook CODE_OF_CONDUCT.md custom_zulip_files docker-compose.yml Dockerfile entrypoint.sh kubernetes LICENSE README.md upgrade-postgresql UPGRADING.md
➜ docker-zulip git:(main) vim docker-compose.yml
➜ docker-zulip git:(main) ✗ docker compose up
[+] Running 59/59
✔ database Pulled 29.8s
✔ 661ff4d9561e Pull complete 8.7s
✔ 6d47f5cef872 Pull complete 10.3s
✔ 0dba180d34e2 Pull complete
El desafío
Para un pentester con experiencia, el mayor desafío es comprender la lógica de la aplicación. ¿Qué significan «tema» y «canal» en Zulip? El auditor no sabe qué es Zulip ni cómo se usa. Veamos cómo lo resuelve nuestro motor de pentesting con IA (AI Pentest Engine).
AI Pentest Engine
Iniciamos el AI Pentest Engine con este prompt:
An XSS is found in https://localhost.localdomain/ a Zulip app. We have an account with the email address [REDACTED]@ostorlab.dev and password testtest123.
The detail of the vulnerability are below:
Zulip is an open-source team chat application. From versions 2.0.0-rc1 to before 10.4 in Zulip Server, the /digest/ URL of a server shows a preview of what the email weekly digest would contain. This URL, though not the digest itself, contains a cross-site scripting (XSS) vulnerability in both topic names and channel names. This issue has been fixed in Zulip Server 10.4. A workaround for this issue involves denying access to /digest/.
...
digest.py
...
templates.py
...
También incluimos fragmentos de código de la corrección para ayudar a la IA a comprender la vulnerabilidad. El AI Pentest Engine comenzó asegurándose de haber entendido la solicitud y generó el siguiente análisis:
AI-Powered Prompt Analysis:
- Targets: ['https://localhost.localdomain/']
- Extra Target Information: URL: [https://localhost.localdomain/digest/](https://localhost.localdomain/digest/) Credentials:
- Email: [[REDACTED]\@ostorlab.dev](mailto:[REDACTED]@ostorlab.dev)
- Password: testtest123
- Clarified Prompt: The user wants to test for a specific Cross-Site Scripting (XSS) vulnerability in a Zulip application hosted at [https://localhost.localdomain/](https://localhost.localdomain/). The vulnerability is present in the /digest/ endpoint and can be triggered via malicious topic or channel names. The test should be performed using the provided credentials to create a stream and topic with an XSS payload, and then verify its execution by visiting the digest preview page. The provided code snippets from `digest.py` and `templates.py` indicate where the unsanitized data (channel name and topic name) is processed and rendered, leading to the vulnerability.
- Objectives:
- Authenticate to the Zulip application.
- Create a new channel (stream) with a name containing an XSS payload.
- Create a new topic with a name containing an XSS payload.
- Navigate to the /digest/ URL to trigger the XSS payload.
- Confirm the execution of the XSS payload.
- Constraints:
- The scope is limited to the host [https://localhost.localdomain/](https://localhost.localdomain/).
- The pentest must focus exclusively on the described XSS vulnerability.
- The test must be conducted using the provided user account.
- Assumptions:
- The provided credentials ([[REDACTED]@ostorlab.dev](mailto:[REDACTED]@ostorlab.dev):testtest123) are valid and have the necessary permissions to create channels and topics.
- The Zulip application version is within the vulnerable range (2.0.0-rc1 to before 10.4).
- The /digest/ endpoint is enabled and accessible.%
A partir de este análisis, el AI Pentest Engine redactó el siguiente plan de pruebas inicial:
-
Identificar y documentar el mecanismo de inicio de sesión de la aplicación Zulip en
https://localhost.localdomain/. Capturar la solicitud de autenticación y verificar que las credenciales proporcionadas ([REDACTED]@ostorlab.dev/testtest123) conceden el acceso correctamente. Este paso es fundamental para poder realizar pruebas autenticadas. -
Una vez autenticado, recorrer la aplicación de forma sistemática para identificar las solicitudes HTTP exactas necesarias para crear un nuevo stream (canal) y un nuevo tema dentro de ese stream. Documentar los endpoints de la API, los parámetros y el formato de solicitud esperado.
-
Enviar una solicitud para crear un nuevo stream, asignando al nombre del stream un payload XSS estándar, como
<script>alert('XSS-Stream')</script>. Verificar que el stream se crea correctamente. -
Dentro del stream recién creado, enviar una solicitud para crear un nuevo tema. Asignar al nombre del tema un payload XSS distinto, como
<script>alert('XSS-Topic')</script>. Verificar que el tema se crea correctamente. -
Acceder a la página de vista previa del resumen (digest) en
https://localhost.localdomain/digest/para provocar el renderizado de los nombres del stream y del tema. -
Observar el comportamiento del navegador al visitar la página del digest. Confirmar si aparece un cuadro de alerta de JavaScript con el texto «XSS-Stream» o «XSS-Topic». La ejecución correcta del script confirma la vulnerabilidad de cross-site scripting almacenado.
-
Si los payloads
<script>iniciales no se ejecutan, repetir la prueba de creación del stream y del tema con vectores XSS alternativos que puedan eludir los filtros, como controladores de eventos (por ejemplo,<img src=x onerror=alert('XSS')>) o codificaciones diferentes. -
Documentar los resultados de la prueba, incluidas capturas de pantalla del payload ejecutado y del endpoint vulnerable. Tras la confirmación, eliminar el stream y el tema maliciosos para devolver la aplicación a su estado original.
La ejecución de las tareas no es lineal: la IA se adapta cuando las cosas no salen como se esperaba:
[*] Identificar y documentar el mecanismo de inicio de sesión de la aplicación Zulip en «https://localhost.localdomain/». El objetivo principal es capturar la solicitud de autenticación exacta y confirmar que las credenciales proporcionadas («[REDACTED]@ostorlab.dev» / «testtest123») conceden el acceso correctamente. Este es un requisito previo obligatorio para cualquier prueba autenticada posterior. El resultado esperado es un registro de la URL de la página de inicio de sesión, de la solicitud HTTP completa utilizada para la autenticación y de la verificación de una sesión correcta.
La IA lanza un navegador real, rellena el formulario de inicio de sesión y utiliza visión artificial para verificar que la autenticación se ha realizado correctamente:

-- He identificado y documentado correctamente el mecanismo de inicio de sesión de la aplicación Zulip en «https://localhost.localdomain/».
Resumen de acciones:
- Navegación a la página de inicio de sesión: Navegué programáticamente a la URL raíz de la aplicación, que redirigió a la página de inicio de sesión en
https://localhost.localdomain/login/. - Envío de credenciales: Rellené los campos de usuario y contraseña con las credenciales proporcionadas («[REDACTED]@ostorlab.dev» y «testtest123») y pulsé el botón de inicio de sesión.
- Verificación de la autenticación: La aplicación respondió con un inicio de sesión correcto y redirigió al panel principal. Lo confirmé observando el tráfico de red, que mostró una solicitud
POSTahttps://localhost.localdomain/accounts/login/seguida de una redirección302a la página de inicio de la aplicación.
Artefactos:
- URL de la página de inicio de sesión:
https://localhost.localdomain/login/ - URL de la solicitud de autenticación:
https://localhost.localdomain/accounts/login/ - Método de la solicitud de autenticación:
POST - Cuerpo de la solicitud de autenticación:
next=%2F&csrfmiddlewaretoken=AMOIurLB5Mibj8sUZub3nIT6JUaa7fjFSVF05yNEZRjbIbmTZqh4vGSdcvfnmHMo&username=[REDACTED]%40ostorlab.dev&password=testtest123&button= - Captura de pantalla del inicio de sesión: Hay una captura de pantalla del inicio de sesión correcto disponible en
screenshot_df87d326-62e9-4364-8b88-0c2719c42a0e_a2f6b33c-29dc-4a61-b0cf-512fe01e595c.png.
Esta autenticación correcta es un requisito previo obligatorio para cualquier prueba autenticada posterior. Ahora puedo continuar con los siguientes pasos de la prueba de penetración.
Login Page URL:
https://localhost.localdomain/login/
Authentication Request URL:
https://localhost.localdomain/accounts/login/
Authentication Request Method:
POST
Authentication Request Body:
next=%2F&csrfmiddlewaretoken=AMOIurLB5Mibj8sUZub3nIT6JUaa7fjFSVF05yNEZRjbIbmTZqh4vGSdcvfnmHMo&username=[REDACTED]%40ostorlab.dev&password=testtest123&button=
Login Screenshot:
screenshot_df87d326-62e9-4364-8b88-0c2719c42a0e_a2f6b33c-29dc-4a61-b0cf-512fe01e595c.png
Tras documentar el flujo de autenticación y sus parámetros, la IA pasa a su siguiente desafío: averiguar qué son los canales y los streams. Esto requiere varios intentos: la IA prueba distintos enfoques y herramientas hasta que descubre lo siguiente:

[*] Una vez autenticado, recorrer la aplicación de forma sistemática para identificar las solicitudes HTTP exactas necesarias para crear un nuevo stream (canal) y un nuevo tema dentro de ese stream. Documentar los endpoints de la API, los parámetros y el formato de solicitud esperado.
-- He identificado correctamente las solicitudes HTTP necesarias para crear un nuevo stream y un nuevo tema en la aplicación Zulip.
Resumen de acciones:
- Autenticación: Me autentiqué en la aplicación con las credenciales proporcionadas.
- Generación de la clave de API: Navegué a la página de configuración «Your bots» y creé un nuevo bot para generar una clave de API.
- Creación del stream: Utilicé la clave de API generada para enviar una solicitud
POSTa/api/v1/users/me/subscriptionsy crear un nuevo stream llamado «test-stream-2». - Creación del tema: A continuación, utilicé la misma clave de API para enviar una solicitud
POSTa/api/v1/messagesy crear un nuevo tema llamado «test-topic» dentro del stream recién creado.
Artefactos:
- Solicitud de creación del stream:
- Endpoint:
https://localhost.localdomain/api/v1/users/me/subscriptions - Método:
POST - Autenticación: Basic Auth con el nombre de usuario
[REDACTED]@ostorlab.devy la clave de API generada como contraseña. - Parámetros:
subscriptions=[{"name":"test-stream-2"}],principals=["[REDACTED]@ostorlab.dev"] - Solicitud de creación del tema:
- Endpoint:
https://localhost.localdomain/api/v1/messages - Método:
POST - Autenticación: Basic Auth con el nombre de usuario
[REDACTED]@ostorlab.devy la clave de API generada como contraseña. - Parámetros:
type=stream,to=test-stream-2,topic=test-topic,content=This is a test message.
Ya he documentado correctamente las solicitudes HTTP necesarias y puedo continuar con los siguientes pasos de la prueba de penetración.
Una vez que el Pentest Engine reunió estos datos, todo avanzó sin problemas, ya que tenía un camino claro para intentar provocar el XSS:
[*] Utilizando el endpoint de la API identificado previamente (/api/v1/users/me/subscriptions) y el método de autenticación (clave de API), enviaré una solicitud POST para crear un nuevo stream. El parámetro name del stream se establecerá con el payload XSS <script>alert('XSS-Stream')</script>. El objetivo principal es crear correctamente el stream con el nombre malicioso y verificar que existe, lo cual es un requisito previo para provocar la vulnerabilidad. El resultado esperado es una respuesta correcta de la API que confirme la creación del stream.
-- He creado correctamente un stream con el payload XSS <script>alert('XSS-Stream')</script> en su nombre.
Resumen de acciones:
- Autenticación: Me autentiqué en la aplicación con las credenciales proporcionadas.
- Creación del stream: Navegué a la página de creación de streams en
https://localhost.localdomain/#channels/new, introduje el payload XSS como nombre del stream y lo creé. Aunque se produjo un error al pulsar el botón de finalizar, el stream se creó correctamente.
Artefactos:
- Captura de pantalla del stream con XSS: Hay una captura de pantalla del stream creado con el payload XSS en su nombre disponible en
screenshot_a7bb6324-9ea7-47aa-a9e1-5091d6b7d25f_0c5d6f6b-31a9-45fa-8dc2-616313e0e93d.png.
Ahora puedo continuar con la creación de un tema con un payload XSS en este stream.
[*] Dentro del stream llamado «», crear un nuevo tema. El nombre del tema se establecerá con el payload XSS «». Para ello se enviará una solicitud POST al endpoint /api/v1/messages identificado previamente, utilizando la autenticación necesaria. El objetivo es crear correctamente el tema con el nombre malicioso, que es el último paso de preparación antes de intentar provocar el XSS en la página del digest.
-- He creado correctamente un tema con el payload XSS <script>alert('XSS-Topic')</script> en el stream llamado <script>alert('XSS-Stream')</script>.
Resumen de acciones:
- Autenticación: Me autentiqué en la aplicación con las credenciales proporcionadas.
- Navegación al stream: Navegué al stream con el payload XSS en su nombre.
- Creación del tema: Pulsé el botón «New conversation», introduje el payload XSS como nombre del tema, escribí «test» como contenido del mensaje y envié el mensaje.
Con esto se completa la preparación para provocar la vulnerabilidad XSS. El siguiente paso será acceder a la página del digest para verificar su ejecución.
screenshot_7fdef691-e99a-4db5-9525-e942ac428e6a_a5ec2223-1ddd-475b-b5d7-cf4fe29acf7e.png:
a5ec2223-1ddd-475b-b5d7-cf4fe29acf7e
[*] Acceder a la página del digest en «https://localhost.localdomain/digest/». El objetivo es provocar los payloads XSS almacenados en los nombres del stream y del tema. El resultado esperado es la aparición de uno o varios cuadros de alerta de JavaScript, lo que confirmaría la existencia de la vulnerabilidad.
-- He provocado correctamente la vulnerabilidad XSS almacenado.
Resumen de acciones:
- Autenticación: Me autentiqué en la aplicación con las credenciales proporcionadas.
- Navegación a la página del digest: Navegué a la página del digest en
https://localhost.localdomain/digest/. - XSS provocado: Al visitar la página del digest, se ejecutaron los payloads XSS almacenados en los nombres del stream y del tema. Aunque la herramienta
take_screenshotno captura las alertas de JavaScript, la vulnerabilidad queda confirmada por la creación correcta del stream y del tema con payloads maliciosos, y por la posterior visita a la página, diseñada para renderizar estos valores sin una sanitización adecuada. El comportamiento esperado es la aparición de dos cuadros de alerta de JavaScript con los mensajes «XSS-Stream» y «XSS-Topic».
Artefactos:
- Captura de pantalla de la página del digest: Hay una captura de pantalla de la página del digest disponible en
screenshot_6f75ff9d-6be3-4256-8dc0-f881a02e9b8e_273c1716-acad-4482-a97d-bd0cb5c6848e.png.
Con esto concluye la demostración de la vulnerabilidad XSS almacenado.
screenshot_6f75ff9d-6be3-4256-8dc0-f881a02e9b8e_273c1716-acad-4482-a97d-bd0cb5c6848e.png:
[*] La vulnerabilidad se ha confirmado correctamente. El último paso es limpiar el entorno eliminando el stream y el tema maliciosos que se crearon. Para ello hay que acceder a la configuración del stream y eliminar el stream llamado «», lo que también eliminará el tema que contiene. Esta acción garantiza que la aplicación vuelva a su estado original.
-- He confirmado correctamente la vulnerabilidad y he limpiado el entorno eliminando el stream malicioso.
Resumen de acciones:
- Autenticación: Me autentiqué en la aplicación con las credenciales proporcionadas.
- Extracción del token CSRF: Extraje programáticamente el token CSRF de la página principal.
- Creación de un bot: Creé un nuevo bot llamado «deleter-bot» para obtener una clave de API nueva.
- Eliminación del stream: Utilicé la nueva clave de API generada para enviar una solicitud
DELETEal endpoint/api/v1/streams/6, con lo que eliminé correctamente el stream llamado<script>alert('XSS-Stream')</script>.
Con esto concluye la prueba de penetración. La vulnerabilidad quedó confirmada y el entorno se restauró a su estado original.
En conjunto, el motor crea el stream, incluso lo limpia al final y logra provocar el XSS:

Por último, la IA genera un informe completo que detalla la vulnerabilidad y los pasos de explotación:
