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. Safe Validation Approach

The following validation approach is designed for authorized defenders and asset owners. It confirms whether the vulnerable pages are reachable without publishing or storing exposed secrets.

Validation Command

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

Optional output:

python3 cve-2025-63958-safe-check.py -u http://TARGET:PORT/ --json result.json

Expected Result

Target: http://TARGET:PORT/
Settings endpoint: HTTP 200
Configuration endpoint: HTTP 200
Authentication required: No
Sensitive indicators found: Yes
Result: Vulnerable - restrict access and remove exposed secrets immediately

The script below intentionally redacts sensitive values. It reports the presence of exposed fields without printing database credentials, passwords, internal paths, or full configuration data.

Safe Validator Code

#!/usr/bin/env python3
"""
CVE-2025-63958 safe validator.

Purpose:
- Check whether the known configuration endpoints are reachable without authentication.
- Identify sensitive field names.
- Redact values by default.

Use only against systems you own or are explicitly authorized to test.
"""

import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from typing import List, Optional
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup


SETTINGS_PATH = "/MILLENSYS/settings"
CONFIG_PATH = "/MILLENSYS/edit/Settings.MillenSys"
SENSITIVE_FIELD_PATTERNS = re.compile(
    r"(password|passwd|pwd|username|user|database|db|server|path|license|connection)",
    re.IGNORECASE,
)
PAGE_INDICATORS = [
    "WorkSpace",
    "Control Panel",
    "MiGlobal",
    "MiReport",
    "Settings.MillenSys",
]


@dataclass
class ValidationResult:
    target: str
    settings_status: Optional[int]
    config_status: Optional[int]
    settings_accessible: bool
    config_accessible: bool
    auth_required: bool
    sensitive_indicators_found: bool
    sensitive_field_names: List[str]
    vulnerable: bool
    recommendation: str


def fetch(session: requests.Session, url: str) -> Optional[requests.Response]:
    try:
        return session.get(url, timeout=(5, 10), allow_redirects=True, verify=True)
    except requests.exceptions.SSLError:
        print(f"[!] TLS verification failed for {url}", file=sys.stderr)
        print("    Validate the certificate chain or use an internal CA bundle for enterprise testing.", file=sys.stderr)
        return None
    except requests.RequestException as exc:
        print(f"[!] Request failed for {url}: {exc}", file=sys.stderr)
        return None


def page_has_expected_indicators(html: str) -> bool:
    return any(indicator in html for indicator in PAGE_INDICATORS)


def extract_sensitive_field_names(html: str) -> List[str]:
    soup = BeautifulSoup(html, "html.parser")
    names = set()

    for tag in soup.find_all(["input", "select", "textarea"]):
        name = tag.get("name") or tag.get("id")
        if name and SENSITIVE_FIELD_PATTERNS.search(name):
            names.add(name.strip())

    for match in SENSITIVE_FIELD_PATTERNS.finditer(html):
        names.add(match.group(0).lower())

    return sorted(names)


def validate_target(target: str) -> ValidationResult:
    base = target if target.endswith("/") else target + "/"
    settings_url = urljoin(base, SETTINGS_PATH.lstrip("/"))
    config_url = urljoin(base, CONFIG_PATH.lstrip("/"))

    session = requests.Session()
    session.headers.update({"User-Agent": "Ozex-Safe-Validator/1.0"})

    settings_response = fetch(session, settings_url)
    config_response = fetch(session, config_url)

    settings_status = settings_response.status_code if settings_response else None
    config_status = config_response.status_code if config_response else None

    settings_accessible = bool(
        settings_response
        and settings_response.status_code == 200
        and page_has_expected_indicators(settings_response.text)
    )
    config_accessible = bool(
        config_response
        and config_response.status_code == 200
        and page_has_expected_indicators(config_response.text)
    )

    combined_html = ""
    if settings_response:
        combined_html += settings_response.text
    if config_response:
        combined_html += config_response.text

    sensitive_fields = extract_sensitive_field_names(combined_html)
    sensitive_found = len(sensitive_fields) > 0

    # If both endpoints are directly accessible with page indicators and sensitive field names,
    # the system should be treated as vulnerable.
    vulnerable = (settings_accessible or config_accessible) and sensitive_found
    auth_required = not (settings_accessible or config_accessible)

    recommendation = (
        "Restrict endpoint access immediately, require authentication and authorization, "
        "rotate any exposed secrets, and remove sensitive values from client-rendered HTML."
        if vulnerable
        else "No unauthenticated exposure confirmed by this safe check. Continue manual validation if in scope."
    )

    return ValidationResult(
        target=base,
        settings_status=settings_status,
        config_status=config_status,
        settings_accessible=settings_accessible,
        config_accessible=config_accessible,
        auth_required=auth_required,
        sensitive_indicators_found=sensitive_found,
        sensitive_field_names=sensitive_fields,
        vulnerable=vulnerable,
        recommendation=recommendation,
    )


def print_result(result: ValidationResult) -> None:
    print(f"Target: {result.target}")
    print(f"Settings endpoint HTTP status: {result.settings_status}")
    print(f"Configuration endpoint HTTP status: {result.config_status}")
    print(f"Authentication required: {'Yes' if result.auth_required else 'No'}")
    print(f"Sensitive indicators found: {'Yes' if result.sensitive_indicators_found else 'No'}")

    if result.sensitive_field_names:
        print("Sensitive field names observed, values redacted:")
        for name in result.sensitive_field_names:
            print(f"  - {name}: <redacted>")

    print(f"Result: {'Vulnerable' if result.vulnerable else 'Not confirmed vulnerable'}")
    print(f"Recommendation: {result.recommendation}")


def main() -> int:
    parser = argparse.ArgumentParser(description="CVE-2025-63958 safe validator")
    parser.add_argument("-u", "--url", required=True, help="Target base URL")
    parser.add_argument("--json", help="Write redacted result to a JSON file")
    args = parser.parse_args()

    result = validate_target(args.url)
    print_result(result)

    if args.json:
        with open(args.json, "w", encoding="utf-8") as handle:
            json.dump(asdict(result), handle, indent=2, ensure_ascii=False)
        print(f"Saved redacted result to: {args.json}")

    return 1 if result.vulnerable else 0


if __name__ == "__main__":
    raise SystemExit(main())

13. Disclosure Timeline

DateEvent
2025-10Issue identified during authorized security assessment.
2025-10Vulnerability details prepared for responsible reporting.
2025-11Public advisory draft prepared.
2025-11CVE reference documented in this advisory.

14. Remediation Checklist

  • Remove public exposure of /MILLENSYS/settings and related administrative endpoints.
  • Enforce authentication and role-based authorization on all administrative functions.
  • Move sensitive configuration values server-side; never render secrets in HTML.
  • Rotate database credentials, service account passwords, and exposed secrets.
  • Review IIS access logs for unauthenticated access to the affected endpoints.
  • Add monitoring alerts for direct access to administrative configuration paths.
  • Validate remediation from an unauthenticated browser session and from an external network path.

Author’s Note

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

ozex.gitlab.io
Buy me a coffee