# Popping Root on UniFi OS Server: Unauthenticated RCE Chain Detection & Analysis

UniFi OS Server auth gateway compares raw URI while Nginx routes on normalized URI, letting unauthenticated requests reach a package-update shell sink and escalate to root.

- Payout: Research
- Severity: critical
- Program: UniFi OS Server
- Write-up by: Bishop Fox
- Disclosed: 2026-06-05
- CVE: CVE-2026-34908, CVE-2026-34909, CVE-2026-34910
- Write-up quality: 9/10
- Original: https://bishopfox.com/blog/popping-root-on-unifi-os-server-unauthenticated-rce-chain-detection-analysis
- Library page: https://bugbountylibrary.com/r/blog%3Abishopfox-com-blog-popping-root-on-unifi-os-server-unauthenticated-rce-chain-detection-analysis
- Tags: area:cloud domain:infra impact:privesc impact:rce kind:research platform:research sev:critical tech:go tech:nginx technique:reverse-engineering technique:source-review vuln:auth-bypass vuln:command-injection vuln:path-traversal vuln:privilege-escalation

**Target:** Nginx-fronted unifi-core auth gateway and package-update backend (ulp-go / identity-update)
**Root cause:** The auth_request handler prefix-matches the raw percent-encoded x-original-uri for public exemptions, while Nginx selects the upstream location using the normalized $uri; a request can start with the exempt /api/auth/validate-sso/ prefix yet normalize to an authenticated /proxy/<service>/ route. Behind the gate, the package-update handler interpolates a caller-supplied package name into `sudo /usr/bin/uos runnable latest-versions %v` and runs it via sh -c with no validation.
**Steps:**
1. Diff 5.0.6 vs 5.0.8 root filesystems: Nginx config, obfuscated service.js auth handler, Go package-update binaries.
2. Craft a request whose raw URI begins with `/api/auth/validate-sso/` but contains encoded traversal (`..%2f`, `..%2e`, `%2e%2e`) so the normalized `$uri` resolves to `/proxy/<service>/...`; gateway returns 200 and Nginx proxies to the internal backend.
3. Control tests: same traversal behind a non-exempt prefix returns 401; untraversed `/proxy/<service>/` returns 401.
4. Reach `.../ucs/update/latest_package` with a package name containing shell metacharacters plus the option forcing the command path; confirm via time-based oracle (delayed 200 on 5.0.6, fast 400 on 5.0.8).
5. From the ucs-update service account, use passwordless `sudo -n dpkg -i` to install a throwaway .deb whose post-install script reads /etc/shadow, confirming root execution.
**How found:** Started from SAB-064 and a filesystem diff of 5.0.6 vs 5.0.8; the auth logic lived in a 1.2 MB obfuscated service.js requiring string/structure diffing and Go binaries requiring symbol/string diffing plus decompilation. Renaming decompiled identifiers made the chain legible; the key observation was that the exemption check reads raw $request_uri while Nginx routes on normalized $uri.
**Exploitation:** Single unauthenticated HTTP request to the reachable UI (TCP 11443): gateway bypass via raw/normalized URI divergence reaches the package-update route, injected shell metacharacters in the package name execute as ucs-update, then passwordless sudo dpkg installs an attacker .deb whose maintainer script runs as root, yielding a root shell. From root, read jwt.yaml signing key, TLS keys, cloud tokens, PostgreSQL user store, RADIUS/WiFi/VPN/NFC/biometric data, and /etc/shadow.
**Escalation:** Forged HS256 owner session token from the stolen signing key (verification does not pin alg/iss/aud) gives durable admin access that survives patching; forged owner token drives SSH-enable and root-password-set workflow for persistent root login; cloud tokens pivot to other sites under the same Ubiquiti account.
**Insight:** When a reverse proxy enforces auth via auth_request on the raw URI but routes on the normalized URI, hunt for prefix-exempt paths (e.g. /api/auth/validate-sso/) combined with encoded traversal so the two views diverge; also check whether the service account behind the sink has passwordless sudo on dpkg/chmod/systemctl.

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

**Vulnerable authCheck prefix-match bypass code**

```
authCheck = async (req, res) => { 
 let uri = getHeader(req.headers, "x-original-uri"); // the RAW $request_uri 
 ... 
 if (publicRoutes.has(`${method} ${uri}`) || 
 uri?.startsWith("/api/auth/validate-sso/")) // public / auth-exempt prefix 
 return res.statusCode = 200, res.end(); 
 ... 
 return res.statusCode = 401, res.end(); 
};
```

**Command injection sink format string**

```
sudo /usr/bin/uos runnable latest-versions %v

where %v is the caller’s package name, interpolated verbatim.
```

**JWT signing key rotation commands**

```
ssh root@<device> 
openssl rand -hex 32 # new key 
# edit /data/unifi-core/config/jwt.yaml -> secret: <new hex> 
reboot # reload all token consumers
```

