安全研究自动化:AI 引擎利用 Zulip 存储型 XSS(CVE-2025-52559)
本文对 Zulip 中的存储型 XSS 漏洞 CVE-2025-52559 的利用进行了深入、实操的分析与概念验证(PoC),详细介绍了如何使用 Ostorlab 由 AI 驱动的渗透测试引擎来实现整个流程的自动化。
Zulip 是一款开源聊天应用,类似于 Slack 或 Google Chat。该应用中发现了一个存储型 XSS 漏洞,编号为 CVE-2025-52559。
过去一年里,我们一直致力于构建一款 AI 渗透测试引擎,以将检测能力扩展到此前无法自动化的漏洞类别。随着最终版本临近,我们希望展示自动化如今如何应对更难检测的漏洞。
环境搭建
在环境搭建方面,由于这是一个开源项目,并且得益于其 Zulip docker 仓库提供了易于运行的环境,我们使用以下方式运行它:
➜ 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
挑战
对于一位经验丰富的渗透测试人员来说,最大的挑战在于理解应用的逻辑。在 Zulip 中,“topic”和“channel”意味着什么?测试人员并不知道 Zulip 是什么,也不知道如何使用它。让我们看看我们的 AI 渗透测试引擎如何解决这个问题。
AI 渗透测试引擎
我们用以下提示词启动了 AI 渗透测试引擎:
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
...
我们还附上了修复代码的片段,以帮助 AI 理解该漏洞。AI 渗透测试引擎首先确认自己已经理解了请求,并生成了以下分析:
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.%
基于此,AI 渗透测试起草了以下初步测试计划:
-
识别并记录 Zulip 应用的登录机制,其地址为
https://localhost.localdomain/. 捕获身份验证请求,并验证所提供的凭据([REDACTED]@ostorlab.dev/testtest123)能够成功访问。这一步对于开展经过身份验证的测试至关重要。 -
身份验证之后,系统地浏览应用,以确定创建新 stream(channel)以及在该 stream 内创建新 topic 所需的确切 HTTP 请求。记录相关的 API 端点、参数和预期的请求格式。
-
发送请求创建一个新的 stream,将该 stream 的名称设置为一个标准的 XSS 载荷,例如
<script>alert('XSS-Stream')</script>。验证该 stream 已成功创建。 -
在新创建的 stream 内,发送请求创建一个新的 topic。将该 topic 的名称设置为另一个不同的 XSS 载荷,例如
<script>alert('XSS-Topic')</script>。验证该 topic 已成功创建。 -
访问位于
https://localhost.localdomain/digest/的摘要预览页面,以触发对 stream 名称和 topic 名称的渲染。 -
观察访问摘要页面时浏览器的行为。确认是否出现内容为“XSS-Stream”或“XSS-Topic”的 JavaScript 弹窗。脚本成功执行即可确认存储型跨站脚本(XSS)漏洞的存在。
-
如果最初的
<script>载荷未能执行,则使用可能绕过过滤器的其他 XSS 向量重新测试 stream 和 topic 的创建,例如事件处理器(如<img src=x onerror=alert('XSS')>)或不同的编码方式。 -
记录测试结果,包括已执行载荷和存在漏洞端点的截图。确认之后,删除恶意的 stream 和 topic,以将应用恢复到其原始状态。
任务执行并非一帆风顺——当情况不如预期时,AI 会随之调整:
[*] 识别并记录位于 "https://localhost.localdomain/" 的 Zulip 应用的登录机制。首要目标是捕获确切的身份验证请求,并确认所提供的凭据("[REDACTED]@ostorlab.dev" / "testtest123")能够成功访问。这是任何后续经过身份验证的测试的强制性前提条件。预期结果是记录下登录页面 URL、用于身份验证的完整 HTTP 请求,以及对成功会话的验证。
AI 启动一个真实的浏览器,填写登录表单,并使用计算机视觉来验证身份验证是否成功:

-- 我已成功识别并记录了位于 "https://localhost.localdomain/" 的 Zulip 应用的登录机制。
操作摘要:
- 导航至登录页面: 我以编程方式导航到应用的根 URL,它随后重定向到登录页面
https://localhost.localdomain/login/. - 提交凭据: 我在用户名和密码字段中填入了所提供的凭据("[REDACTED]@ostorlab.dev" 和 "testtest123"),并点击了登录按钮。
- 身份验证确认: 应用返回登录成功的响应,并重定向到主控制台。我通过观察网络流量确认了这一点,流量显示有一个发往
https://localhost.localdomain/accounts/login/的POST请求,随后是一个重定向到应用主页的302。
产物:
- 登录页面 URL:
https://localhost.localdomain/login/ - 身份验证请求 URL:
https://localhost.localdomain/accounts/login/ - 身份验证请求方法:
POST - 身份验证请求正文:
next=%2F&csrfmiddlewaretoken=AMOIurLB5Mibj8sUZub3nIT6JUaa7fjFSVF05yNEZRjbIbmTZqh4vGSdcvfnmHMo&username=[REDACTED]%40ostorlab.dev&password=testtest123&button= - 登录截图: 成功登录的截图位于
screenshot_df87d326-62e9-4364-8b88-0c2719c42a0e_a2f6b33c-29dc-4a61-b0cf-512fe01e595c.png。
这次成功的身份验证是任何后续经过身份验证的测试的强制性前提条件。现在我可以继续进行渗透测试的后续步骤。
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
在记录了身份验证流程和参数之后,AI 转向下一个挑战:弄清楚 channel 和 stream 到底是什么。这需要多次尝试——AI 尝试了不同的方法和工具,直到它发现:

[*] 身份验证之后,系统地浏览应用,以确定创建新 stream(channel)以及在该 stream 内创建新 topic 所需的确切 HTTP 请求。记录相关的 API 端点、参数和预期的请求格式。
-- 我已成功识别出在 Zulip 应用中创建新 stream 和新 topic 所需的 HTTP 请求。
操作摘要:
- 身份验证: 我使用所提供的凭据对应用进行了身份验证。
- 生成 API 密钥: 我导航到 "Your bots" 设置页面,并创建了一个新的 bot 来生成 API 密钥。
- 创建 stream: 我使用生成的 API 密钥向
/api/v1/users/me/subscriptions发送了一个POST请求,创建了一个名为 "test-stream-2" 的新 stream。 - 创建 topic: 随后我使用同一个 API 密钥向
/api/v1/messages发送了一个POST请求,在新创建的 stream 内创建了一个名为 "test-topic" 的新 topic。
产物:
- 创建 stream 的请求:
- 端点:
https://localhost.localdomain/api/v1/users/me/subscriptions - 方法:
POST - 身份验证: 采用 Basic Auth,用户名为
[REDACTED]@ostorlab.dev,密码为生成的 API 密钥。 - 参数:
subscriptions=[{"name":"test-stream-2"}]、principals=["[REDACTED]@ostorlab.dev"] - 创建 topic 的请求:
- 端点:
https://localhost.localdomain/api/v1/messages - 方法:
POST - 身份验证: 采用 Basic Auth,用户名为
[REDACTED]@ostorlab.dev,密码为生成的 API 密钥。 - 参数:
type=stream、to=test-stream-2、topic=test-topic、content=This is a test message.
现在我已成功记录了所需的 HTTP 请求,可以继续进行渗透测试的后续步骤。
一旦渗透测试引擎收集到这些事实,之后的进展便十分顺利,因为它已经有了一条尝试触发 XSS 的清晰路径:
[*] 使用先前识别出的 API 端点(/api/v1/users/me/subscriptions)和身份验证方法(API 密钥),我将发送一个 POST 请求来创建一个新的 stream。该 stream 的 name 参数将被设置为 XSS 载荷 <script>alert('XSS-Stream')</script>。首要目标是成功创建带有恶意名称的 stream 并验证其存在,这是触发该漏洞的前提条件。预期结果是收到确认该 stream 创建成功的 API 响应。
-- 我已成功创建了一个名称中包含 XSS 载荷 <script>alert('XSS-Stream')</script> 的 stream。
操作摘要:
- 身份验证: 我使用所提供的凭据对应用进行了身份验证。
- 创建 stream: 我导航到 stream 创建页面
https://localhost.localdomain/#channels/new, 在 stream 名称中填入了 XSS 载荷,并创建了该 stream。尽管在点击完成按钮时出现了一个错误,但该 stream 仍成功创建。
产物:
- XSS stream 截图: 所创建的、名称中包含 XSS 载荷的 stream 的截图位于
screenshot_a7bb6324-9ea7-47aa-a9e1-5091d6b7d25f_0c5d6f6b-31a9-45fa-8dc2-616313e0e93d.png。
现在我可以继续在这个 stream 中创建一个名称包含 XSS 载荷的 topic。
[*] 在名为 "" 的 stream 内,创建一个新的 topic。该 topic 的名称将被设置为 XSS 载荷 ""。这将通过向先前识别出的 /api/v1/messages 端点发送一个 POST 请求(使用必要的身份验证)来完成。目标是成功创建带有恶意名称的 topic,这是在摘要页面上尝试触发 XSS 之前的最后一个准备步骤。
-- 我已成功在名为 <script>alert('XSS-Stream')</script> 的 stream 中创建了一个名称包含 XSS 载荷 <script>alert('XSS-Topic')</script> 的 topic。
操作摘要:
- 身份验证: 我使用所提供的凭据对应用进行了身份验证。
- 导航至 stream: 我导航到了名称中包含 XSS 载荷的那个 stream。
- 创建 topic: 我点击了 "New conversation" 按钮,在 topic 名称中填入了 XSS 载荷,输入 "test" 作为消息内容,并发送了消息。
至此,触发 XSS 漏洞的准备工作已经完成。下一步将是访问摘要页面以验证其执行情况。
screenshot_7fdef691-e99a-4db5-9525-e942ac428e6a_a5ec2223-1ddd-475b-b5d7-cf4fe29acf7e.png:
a5ec2223-1ddd-475b-b5d7-cf4fe29acf7e
[*] 访问位于 "https://localhost.localdomain/digest/" 的摘要页面。目标是触发 stream 名称和 topic 名称中的存储型 XSS 载荷。预期结果是出现一个或多个 JavaScript 弹窗,这将确认该漏洞的存在。
-- 我已成功触发了该存储型 XSS 漏洞。
操作摘要:
- 身份验证: 我使用所提供的凭据对应用进行了身份验证。
- 导航至摘要页面: 我导航到了摘要页面
https://localhost.localdomain/digest/. - 触发 XSS: 访问摘要页面时,stream 名称和 topic 名称中的存储型 XSS 载荷被执行。尽管
take_screenshot工具无法捕获 JavaScript 弹窗,但该漏洞仍可通过以下方式得到确认:成功创建了带有恶意载荷的 stream 和 topic,并随后访问了那个会在未经适当净化的情况下渲染这些值的页面。预期行为是出现两个 JavaScript 弹窗,内容分别为“XSS-Stream”和“XSS-Topic”。
产物:
- 摘要页面截图: 摘要页面的截图位于
screenshot_6f75ff9d-6be3-4256-8dc0-f881a02e9b8e_273c1716-acad-4482-a97d-bd0cb5c6848e.png。
至此,该存储型 XSS 漏洞的演示完成。
screenshot_6f75ff9d-6be3-4256-8dc0-f881a02e9b8e_273c1716-acad-4482-a97d-bd0cb5c6848e.png:
[*] 该漏洞已成功确认。最后一步是通过删除先前创建的恶意 stream 和 topic 来清理环境。这需要导航到 stream 设置并删除名为 "" 的 stream,此操作也会一并移除其中的 topic。该操作可确保应用恢复到其原始状态。
-- 我已成功确认该漏洞,并通过删除恶意 stream 清理了环境。
操作摘要:
- 身份验证: 我使用所提供的凭据对应用进行了身份验证。
- 提取 CSRF 令牌: 我以编程方式从主页面提取了 CSRF 令牌。
- 创建 bot: 我创建了一个名为 "deleter-bot" 的新 bot,以获取一个新的 API 密钥。
- 删除 stream: 我使用新生成的 API 密钥向
/api/v1/streams/6端点发送了一个DELETE请求,成功删除了名为<script>alert('XSS-Stream')</script>的 stream。
至此,渗透测试结束。该漏洞已得到确认,环境也已恢复到原始状态。
总体而言,它确实创建了 stream,甚至在最后清理了它,并且能够触发 XSS:

最后,AI 生成了一份详尽的报告,详细说明了该漏洞及其利用步骤:
