Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Automatiser la recherche en sécurité : un moteur d'IA exploite la XSS stockée de Zulip (CVE-2025-52559)

Cet article présente une analyse approfondie et une preuve de concept pratique pour l'exploitation de la vulnérabilité XSS stockée CVE-2025-52559 dans Zulip. Il détaille comment le moteur de pentest piloté par IA d'Ostorlab a servi à automatiser le cycle complet.

Zulip est une application de messagerie instantanée open source, comparable à Slack ou à Google Chat. Une vulnérabilité XSS stockée a été découverte dans l'application sous la référence CVE-2025-52559.

Depuis un an, Ostorlab développe un moteur de test d'intrusion par IA afin d'étendre les capacités de détection à des classes de vulnérabilités qu'il était impossible d'automatiser jusqu'ici. À l'approche de la version finale, nous voulons montrer comment l'automatisation peut désormais s'attaquer à des bugs plus difficiles à détecter.

Mise en place

Pour la mise en place, comme il s'agit d'un projet open source facile à exécuter grâce à leur dépôt Zulip pour Docker, nous l'avons lancé ainsi :

➜  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                                                                                 

Le défi

Pour un pentesteur expérimenté, le plus grand défi est de comprendre la logique de l'application. Que signifient « topic » et « channel » dans Zulip ? Le testeur ne sait pas ce qu'est Zulip ni comment l'utiliser. Voyons comment notre moteur de pentest par IA y parvient.

Moteur de pentest par IA

Nous avons lancé le moteur de pentest par IA avec ce 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

...

Nous avons également fourni des extraits de code du correctif pour aider l'IA à comprendre la vulnérabilité. Le moteur de pentest par IA a commencé par s'assurer qu'il avait bien compris la demande et a produit l'analyse suivante :

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.%

À partir de cette analyse, le pentest par IA a rédigé le plan de test initial suivant :

  1. Identifier et documenter le mécanisme de connexion de l'application Zulip à l'adresse https://localhost.localdomain/. Capturer la requête d'authentification et vérifier que les identifiants fournis ([REDACTED]@ostorlab.dev / testtest123) donnent bien accès à l'application. Cette étape est essentielle pour pouvoir effectuer des tests authentifiés.

  2. Après l'authentification, parcourir méthodiquement l'application pour identifier les requêtes HTTP exactes nécessaires à la création d'un nouveau stream (channel) et d'un nouveau topic dans ce stream. Documenter les endpoints d'API, les paramètres et le format de requête attendu.

  3. Envoyer une requête de création d'un nouveau stream en donnant au stream un nom correspondant à un payload XSS standard, tel que <script>alert('XSS-Stream')</script>. Vérifier que le stream est bien créé.

  4. Dans le stream nouvellement créé, envoyer une requête de création d'un nouveau topic. Donner au topic un nom correspondant à un autre payload XSS, tel que <script>alert('XSS-Topic')</script>. Vérifier que le topic est bien créé.

  5. Se rendre sur la page d'aperçu du digest à l'adresse https://localhost.localdomain/digest/ pour déclencher l'affichage des noms du stream et du topic.

  6. Observer le comportement du navigateur lors de la visite de la page du digest. Confirmer si une boîte de dialogue d'alerte JavaScript apparaît avec le texte « XSS-Stream » ou « XSS-Topic ». L'exécution réussie du script confirme la vulnérabilité de type Cross-Site Scripting stockée.

  7. Si les payloads <script> initiaux ne s'exécutent pas, retester la création du stream et du topic avec d'autres vecteurs XSS susceptibles de contourner les filtres, tels que des gestionnaires d'événements (par exemple <img src=x onerror=alert('XSS')>) ou des encodages différents.

  8. Documenter les résultats du test, y compris des captures d'écran du payload exécuté et de l'endpoint vulnérable. Après confirmation, supprimer le stream et le topic malveillants afin de remettre l'application dans son état initial.

L'exécution des tâches n'est pas linéaire : l'IA s'adapte lorsque les choses ne se passent pas comme prévu :

[*] Identifier et documenter le mécanisme de connexion de l'application Zulip à l'adresse « https://localhost.localdomain/ ». L'objectif principal est de capturer la requête d'authentification exacte et de confirmer que les identifiants fournis (« [REDACTED]@ostorlab.dev » / « testtest123 ») donnent bien accès à l'application. C'est un prérequis obligatoire à tout test authentifié ultérieur. Le résultat attendu est un relevé de l'URL de la page de connexion, de la requête HTTP complète utilisée pour l'authentification et de la vérification qu'une session valide a été obtenue.

L'IA lance un vrai navigateur, remplit le formulaire de connexion et utilise la vision par ordinateur pour vérifier que l'authentification a réussi :

Page de connexion

-- J'ai identifié et documenté avec succès le mécanisme de connexion de l'application Zulip à l'adresse « https://localhost.localdomain/ ».

Résumé des actions :

  1. Navigation vers la page de connexion : j'ai navigué par programmation vers l'URL racine de l'application, qui m'a redirigé vers la page de connexion https://localhost.localdomain/login/.
  2. Envoi des identifiants : j'ai renseigné les champs du nom d'utilisateur et du mot de passe avec les identifiants fournis (« [REDACTED]@ostorlab.dev » et « testtest123 ») et cliqué sur le bouton de connexion.
  3. Vérification de l'authentification : l'application a répondu par une connexion réussie, en redirigeant vers le tableau de bord principal. Je l'ai confirmé en observant le trafic réseau, qui montrait une requête POST vers https://localhost.localdomain/accounts/login/ suivie d'une redirection 302 vers la page d'accueil de l'application.

Artefacts :

  • URL de la page de connexion : https://localhost.localdomain/login/
  • URL de la requête d'authentification : https://localhost.localdomain/accounts/login/
  • Méthode de la requête d'authentification : POST
  • Corps de la requête d'authentification : next=%2F&csrfmiddlewaretoken=AMOIurLB5Mibj8sUZub3nIT6JUaa7fjFSVF05yNEZRjbIbmTZqh4vGSdcvfnmHMo&username=[REDACTED]%40ostorlab.dev&password=testtest123&button=
  • Capture d'écran de la connexion : une capture d'écran de la connexion réussie est disponible dans screenshot_df87d326-62e9-4364-8b88-0c2719c42a0e_a2f6b33c-29dc-4a61-b0cf-512fe01e595c.png.

Cette authentification réussie est un prérequis obligatoire à tout test authentifié ultérieur. Je peux désormais passer aux étapes suivantes du test d'intrusion.

  • 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

Après avoir documenté le flux d'authentification et ses paramètres, l'IA passe à son défi suivant : comprendre ce que sont les channels et les streams. Cela demande plusieurs tentatives - l'IA essaie différentes approches et différents outils jusqu'à découvrir :

Channel trouvé

[*] Après l'authentification, parcourir méthodiquement l'application pour identifier les requêtes HTTP exactes nécessaires à la création d'un nouveau stream (channel) et d'un nouveau topic dans ce stream. Documenter les endpoints d'API, les paramètres et le format de requête attendu.

-- J'ai identifié avec succès les requêtes HTTP nécessaires à la création d'un nouveau stream et d'un nouveau topic dans l'application Zulip.

Résumé des actions :

  1. Authentification : je me suis authentifié à l'application avec les identifiants fournis.
  2. Génération d'une clé d'API : j'ai ouvert la page de paramètres « Your bots » et créé un nouveau bot pour générer une clé d'API.
  3. Création du stream : j'ai utilisé la clé d'API générée pour envoyer une requête POST vers /api/v1/users/me/subscriptions afin de créer un nouveau stream nommé « test-stream-2 ».
  4. Création du topic : j'ai ensuite utilisé la même clé d'API pour envoyer une requête POST vers /api/v1/messages afin de créer un nouveau topic nommé « test-topic » dans le stream nouvellement créé.

Artefacts :

  • Requête de création du stream :
  • Endpoint : https://localhost.localdomain/api/v1/users/me/subscriptions
  • Méthode : POST
  • Authentification : Basic Auth avec le nom d'utilisateur [REDACTED]@ostorlab.dev et la clé d'API générée comme mot de passe.
  • Paramètres : subscriptions=[{"name":"test-stream-2"}], principals=["[REDACTED]@ostorlab.dev"]
  • Requête de création du topic :
  • Endpoint : https://localhost.localdomain/api/v1/messages
  • Méthode : POST
  • Authentification : Basic Auth avec le nom d'utilisateur [REDACTED]@ostorlab.dev et la clé d'API générée comme mot de passe.
  • Paramètres : type=stream, to=test-stream-2, topic=test-topic, content=This is a test message.

J'ai désormais documenté avec succès les requêtes HTTP nécessaires et je peux passer aux étapes suivantes du test d'intrusion.

Une fois ces éléments recueillis par le moteur de pentest, la suite se déroule sans accroc, car il disposait d'une voie claire pour tenter de déclencher la XSS :

[*] En utilisant l'endpoint d'API identifié précédemment (/api/v1/users/me/subscriptions) et la méthode d'authentification (clé d'API), j'enverrai une requête POST pour créer un nouveau stream. Le paramètre name du stream sera défini avec le payload XSS <script>alert('XSS-Stream')</script>. L'objectif principal est de créer avec succès le stream portant ce nom malveillant et de vérifier son existence, ce qui est un prérequis pour déclencher la vulnérabilité. Le résultat attendu est une réponse d'API positive confirmant la création du stream.

-- J'ai créé avec succès un stream dont le nom contient le payload XSS <script>alert('XSS-Stream')</script>.

Résumé des actions :

  1. Authentification : je me suis authentifié à l'application avec les identifiants fournis.
  2. Création du stream : j'ai ouvert la page de création de stream à l'adresse https://localhost.localdomain/#channels/new, saisi le payload XSS comme nom du stream et créé le stream. Bien qu'une erreur soit survenue en cliquant sur le bouton de finalisation, le stream a bien été créé.

Artefacts :

  • Capture d'écran du stream XSS : une capture d'écran du stream créé avec le payload XSS dans son nom est disponible dans screenshot_a7bb6324-9ea7-47aa-a9e1-5091d6b7d25f_0c5d6f6b-31a9-45fa-8dc2-616313e0e93d.png.

Je peux maintenant passer à la création d'un topic avec un payload XSS dans ce stream.

[*] Dans le stream nommé "", créer un nouveau topic. Le nom du topic sera défini avec le payload XSS "". Cette opération sera réalisée en envoyant une requête POST à l'endpoint /api/v1/messages identifié précédemment, avec l'authentification nécessaire. L'objectif est de créer avec succès le topic portant ce nom malveillant, ce qui constitue la dernière étape de préparation avant de tenter de déclencher la XSS sur la page du digest.

-- J'ai créé avec succès un topic dont le nom contient le payload XSS <script>alert('XSS-Topic')</script> dans le stream nommé <script>alert('XSS-Stream')</script>.

Résumé des actions :

  1. Authentification : je me suis authentifié à l'application avec les identifiants fournis.
  2. Navigation vers le stream : j'ai navigué jusqu'au stream dont le nom contient le payload XSS.
  3. Création du topic : j'ai cliqué sur le bouton « New conversation », saisi le payload XSS comme nom du topic, entré « test » comme contenu du message et envoyé le message.

Cela termine la préparation du déclenchement de la vulnérabilité XSS. L'étape suivante consistera à se rendre sur la page du digest pour vérifier son exécution.

  • screenshot_7fdef691-e99a-4db5-9525-e942ac428e6a_a5ec2223-1ddd-475b-b5d7-cf4fe29acf7e.png:
a5ec2223-1ddd-475b-b5d7-cf4fe29acf7e

[*] Se rendre sur la page du digest à l'adresse « https://localhost.localdomain/digest/ ». L'objectif est de déclencher les payloads XSS stockés dans les noms du stream et du topic. Le résultat attendu est l'apparition d'une ou plusieurs boîtes de dialogue d'alerte JavaScript, ce qui confirmerait l'existence de la vulnérabilité.

-- J'ai déclenché avec succès la vulnérabilité XSS stockée.

Résumé des actions :

  1. Authentification : je me suis authentifié à l'application avec les identifiants fournis.
  2. Navigation vers la page du digest : j'ai navigué jusqu'à la page du digest à l'adresse https://localhost.localdomain/digest/.
  3. XSS déclenchée : à la visite de la page du digest, les payloads XSS stockés dans les noms du stream et du topic ont été exécutés. Bien que l'outil take_screenshot ne capture pas les alertes JavaScript, la vulnérabilité est confirmée par la création réussie du stream et du topic contenant des payloads malveillants, puis par la visite de la page conçue pour afficher ces valeurs sans assainissement approprié. Le comportement attendu est l'apparition de deux boîtes de dialogue d'alerte JavaScript avec les messages « XSS-Stream » et « XSS-Topic ».

Artefacts :

  • Capture d'écran de la page du digest : une capture d'écran de la page du digest est disponible dans screenshot_6f75ff9d-6be3-4256-8dc0-f881a02e9b8e_273c1716-acad-4482-a97d-bd0cb5c6848e.png.

Cela conclut la démonstration de la vulnérabilité XSS stockée.

  • screenshot_6f75ff9d-6be3-4256-8dc0-f881a02e9b8e_273c1716-acad-4482-a97d-bd0cb5c6848e.png:

[*] La vulnérabilité a été confirmée avec succès. La dernière étape consiste à nettoyer l'environnement en supprimant le stream et le topic malveillants qui ont été créés. Cela implique de se rendre dans les paramètres du stream et de supprimer le stream nommé "", ce qui supprimera également le topic qu'il contient. Cette action permet de remettre l'application dans son état initial.

-- J'ai confirmé avec succès la vulnérabilité et nettoyé l'environnement en supprimant le stream malveillant.

Résumé des actions :

  1. Authentification : je me suis authentifié à l'application avec les identifiants fournis.
  2. Extraction du jeton CSRF : j'ai extrait par programmation le jeton CSRF de la page principale.
  3. Création d'un bot : j'ai créé un nouveau bot nommé « deleter-bot » pour obtenir une nouvelle clé d'API.
  4. Suppression du stream : j'ai utilisé la clé d'API nouvellement générée pour envoyer une requête DELETE vers l'endpoint /api/v1/streams/6, ce qui a supprimé avec succès le stream nommé <script>alert('XSS-Stream')</script>.

Cela conclut le test d'intrusion. La vulnérabilité a été confirmée et l'environnement a été remis dans son état initial.

Dans l'ensemble, le moteur crée bien le stream, le nettoie même à la fin et parvient à déclencher la XSS :

Channel avec XSS stockée

Enfin, l'IA génère un rapport complet détaillant la vulnérabilité et les étapes d'exploitation :

Résultats finaux

Tags :

security, xss, AI, POC