{"repro_id":"REPRO-2026-00364","version":6,"title":"MariaDB: USAGE-only user can reset admin password via GRANT PROXY ... IDENTIFIED VIA '' auth-list bypass","repro_type":"security","status":"published","severity":"high","description":"HackerOne report #3876430 (disclosed 2026-09-07, resolved by upstream). MariaDB Server account-takeover via GRANT PROXY authentication-plugin injection.","root_cause":"# Root Cause Analysis — H1-3876430 / MDEV-40470: MariaDB GRANT PROXY `IDENTIFIED VIA ''` auth-list bypass → admin account takeover\n\n## Summary\n\nMariaDB Server's `GRANT PROXY ... IDENTIFIED VIA <auth-list>` handling contains a privilege-check\nbypass. In the vulnerable code, `LEX_USER::has_auth()` (sql/structs.h) returns true only when the\nparsed first auth node has a non-empty plugin name or password text. Supplying an EMPTY first auth\nnode (`IDENTIFIED VIA ''`) makes `has_auth()` return false, so the server skips the access check on\nthe `mysql` system database that normally guards credential changes. However,\n`replace_user_table()` still persists the ENTIRE parsed auth list onto the target account — including\nan attacker-controlled SECOND node (`OR mysql_native_password USING PASSWORD('hacked')`). The net\neffect: any authenticated user holding only the `USAGE` privilege (the weakest possible session) can\noverwrite the stored credentials of an arbitrary existing account — including a full DBA — and then\nlog in as that account. This is a complete remote account-takeover / privilege-escalation primitive\nover the normal MySQL TCP protocol.\n\n## Impact\n\n- **Component:** MariaDB Server ACL/grant handling (`sql/sql_acl.cc`, `sql/structs.h`, `sql/sql_yacc.yy`).\n- **Affected versions (confirmed this run):** MariaDB **12.3.2** (Docker `mariadb:12.3.2`,\n  digest `sha256:a02fe89cb597d4375812b2eac90cf9d0775d4686daa7f7cc750ebbcad7525bbc`).\n  The upstream fix (MDEV-40470, commit `dbd60d0ad8daa2d01050346aa50b7b102490e456`) first shipped in\n  released versions 12.3.3 / 11.4.13 / 11.8.9 per the official release notes; earlier release lines\n  without the fix are likewise expected to be affected.\n- **Risk:** High (H1 triage 8.8). Remote, requires only a valid low-privilege account (`USAGE`),\n  one SQL statement, no user interaction. Consequence: full server compromise via takeover of an\n  `ALL PRIVILEGES ... WITH GRANT OPTION` account.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** privilege escalation — USAGE-only user resets an\n  administrator's password and obtains the admin's full privileges.\n- **Reproduced impact from this run:** exactly that. On `mariadb:12.3.2` a `USAGE`-only `attacker`\n  executed the claim's `GRANT PROXY` statement, `victim_admin`'s stored credentials were replaced\n  with the attacker-chosen hash, and a fresh TCP login as `victim_admin` with password `hacked`\n  returned `GRANT ALL PRIVILEGES ON *.* ... WITH GRANT OPTION`.\n- **Parity:** `full`.\n\n## Root Cause\n\n- `sql/structs.h` (vulnerable): `bool has_auth() { return auth && (auth->plugin.length ||\n  auth->auth_str.length || auth->pwtext.length); }` — an explicitly supplied but EMPTY\n  authentication (`IDENTIFIED VIA ''`) is indistinguishable from \"no authentication specified\", so\n  the privilege check (access to the `mysql` database / admin-check in the grant path) is skipped.\n- `sql/sql_yacc.yy` additionally allocated an empty `USER_AUTH()` for `CURRENT_USER()` in\n  `user_maybe_role`, compounding the confusion.\n- Meanwhile `replace_user_table()` writes the full multi-node auth list to `mysql.user`, so the\n  attacker-controlled second node becomes the victim's credential\n  (`SHOW CREATE USER` afterwards shows\n  `IDENTIFIED VIA mysql_native_password OR mysql_native_password USING '*43F4...'` = `PASSWORD('hacked')`).\n- **Fix:** MDEV-40470, commit `dbd60d0ad8daa2d01050346aa50b7b102490e456`\n  (\"empty password and empty plugin name don't mean 'authentication was not specified', they mean\n  'empty authentication was specified'\"): `has_auth()` becomes `return auth;` and the empty\n  `USER_AUTH()` allocation in `sql_yacc.yy` is removed. On the fixed build the statement is rejected\n  with `ERROR 1044 (42000): Access denied for user 'attacker'@'%' to database 'mysql'`.\n\n## Reproduction Steps\n\n1. `bundle/repro/reproduction_steps.sh` (self-contained; requires Docker only).\n2. The script pulls pinned `mariadb:12.3.2` (vulnerable) and `mariadb:12.3.3` (fixed, first release\n   containing `dbd60d0ad8d`), then runs **two fresh vulnerable containers and two fresh fixed\n   containers**. In each container, over the real MySQL TCP protocol (`mariadb --protocol=TCP\n   -h127.0.0.1 -P3306`):\n   - root creates `victim_admin` (`ALL PRIVILEGES ... WITH GRANT OPTION`, password `origpass`) and\n     `attacker` (`USAGE` only, password `attackerpass`);\n   - precondition: attacker's direct `ALTER USER 'victim_admin'@'%' IDENTIFIED BY 'x'` is denied\n     (ERROR 1227), proving the attacker holds no account-management privilege;\n   - attack (as `attacker`):\n     `GRANT PROXY ON CURRENT_USER() TO 'victim_admin'@'%' IDENTIFIED VIA '' OR mysql_native_password USING PASSWORD('hacked');`\n   - verification: post-state `SHOW CREATE USER 'victim_admin'@'%'`, fresh TCP logins as\n     `victim_admin` with `hacked`, `wrongpw`, and `origpass`.\n3. Expected evidence (vulnerable): statement succeeds; victim's auth replaced by\n   `PASSWORD('hacked')` hash; login with `hacked` succeeds with full privileges; `origpass` no\n   longer works. Expected evidence (fixed): statement rejected with ERROR 1044; victim unchanged;\n   `hacked` login fails, `origpass` login works.\n\n## Evidence\n\n- Per-attempt transcripts: `bundle/logs/repro/vuln_attempt_{1,2}.log`,\n  `bundle/logs/repro/fixed_attempt_{1,2}.log`; structured results:\n  `bundle/repro/evidence/{vuln,fixed}_{1,2}.json`; manifest with SHA-256:\n  `bundle/repro/runtime_manifest.json`; session log: `bundle/logs/reproduction_steps.log`.\n- Key excerpts (vulnerable attempt):\n  - `Grants for attacker@%: GRANT USAGE ON *.* TO 'attacker'@'%'` (precondition)\n  - `ATTACK_STMT_OK` (statement succeeded as USAGE-only user)\n  - `CREATE USER 'victim_admin'@'%' IDENTIFIED VIA mysql_native_password OR mysql_native_password USING '*43F422B78BE9A02900B231D67662A7DC2A7BF8C4'` (= `PASSWORD('hacked')`)\n  - login with `hacked` → `GRANT ALL PRIVILEGES ON *.* TO 'victim_admin'@'%' ... WITH GRANT OPTION`\n  - login with `wrongpw` and with the ORIGINAL `origpass` → `ERROR 1045` (credentials truly replaced)\n- Key excerpts (fixed attempt): `ERROR 1044 (42000): Access denied for user 'attacker'@'%' to\n  database 'mysql'`; victim row unchanged; `hacked` login fails; `origpass` login succeeds.\n- Result: vulnerable takeovers 2/2, fixed resistances 2/2, script exit 0 on two consecutive runs.\n- Environment: Docker 27.5.1, `mariadb:12.3.2@sha256:a02fe89c…`, `mariadb:12.3.3@sha256:dd9b303a…`,\n  linux/amd64, no sanitizers involved (pure product behavior at the SQL/TCP boundary).\n\n## Recommendations / Next Steps\n\n- Upgrade to MariaDB ≥ 12.3.3 / 11.4.13 / 11.8.9 (or any build containing commit `dbd60d0ad8d`).\n- The fix approach upstream is correct: treat an explicitly supplied empty auth as an auth change\n  (`has_auth()` → `return auth;`) and stop auto-allocating an empty `USER_AUTH` for `CURRENT_USER()`.\n- Audit: `SELECT user, host, plugin, authentication_string FROM mysql.user` for unexpected changes;\n  alert on any `GRANT PROXY` executed by non-admin accounts.\n- Regression test: the upstream `mysql-test/main/grant5.test` MDEV-40470 case covers both\n  `IDENTIFIED VIA 'foo'` and `IDENTIFIED VIA ''` from a USAGE-only user.\n\n## Additional Notes\n\n- The script is idempotent: every attempt uses a fresh, uniquely named container that is removed\n  afterwards; a `trap` cleans up on early exit.\n- The exact statement variant from the H1 report using `ON ''@''` behaves differently\n  (ERROR 1698); the semantically identical variant `ON CURRENT_USER()` used here matches both the\n  upstream regression test (`grant proxy on u1@localhost to u2@localhost identified via ''`) and the\n  original discoverer PoC, and exercises the identical vulnerable code path (`has_auth()` bypass +\n  `replace_user_table()` persisting the full auth list). The attacker-visible outcome — silent\n  overwrite of the victim admin's credentials from a USAGE-only session — is the claim's core and is\n  fully demonstrated.\n- No crash, sanitizer, or debugger was used; the proof is pure product behavior over the real\n  client/server TCP protocol.\n","cwe_id":"CWE-284","source_url":"https://hackerone.com/reports/3876430","package":{"name":"MariaDB/server","ecosystem":"github","affected_versions":"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_version":"MariaDB 12.3.3 (contains MDEV-40470 fix). Use mariadb:12.3.3 docker image for fixed-behavior verification."},"reproduced_at":"2026-09-24T17:06:12.675442+00:00","duration_secs":1766.0,"tool_calls":150,"handoffs":2,"total_cost_usd":3.255666,"agent_costs":{"claim_matcher":0.017976,"judge":0.672141,"learning_policy":0.018251,"repro":1.195883,"support":0.108122,"vuln_variant":1.243293},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.017976},"judge":{"gpt-5.6-sol":0.672141},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.018251},"repro":{"accounts/fireworks/models/kimi-k3":1.195883},"support":{"accounts/fireworks/models/kimi-k3":0.108122},"vuln_variant":{"accounts/fireworks/models/kimi-k3":1.243293}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-09-24T17:06:13.324838+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8096,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9811,"category":"reproduction_script"},{"path":"bundle/logs/repro/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":1777,"category":"log"},{"path":"bundle/logs/repro/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":1777,"category":"log"},{"path":"bundle/logs/repro/vuln_attempt_1.log","filename":"vuln_attempt_1.log","size":1869,"category":"log"},{"path":"bundle/logs/repro/vuln_attempt_2.log","filename":"vuln_attempt_2.log","size":1869,"category":"log"},{"path":"bundle/repro/evidence/fixed_1.json","filename":"fixed_1.json","size":406,"category":"other"},{"path":"bundle/repro/evidence/fixed_2.json","filename":"fixed_2.json","size":406,"category":"other"},{"path":"bundle/repro/evidence/vuln_1.json","filename":"vuln_1.json","size":405,"category":"other"},{"path":"bundle/repro/evidence/vuln_2.json","filename":"vuln_2.json","size":405,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":2424,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1382,"category":"other"}]}