# REPRO-2026-00330: MariaDB 13.0.1-rc RCE chain: F-09 GRANT PROXY priv-esc (MDEV-40470) + /proc/self/maps ASLR leak + F-05 SYS_REFCURSOR heap UAF → JOP to system() as uid 999(mysql), pure SQL from a low-priv account ## Summary Status: published Severity: critical CVSS: Unknown CWE: CWE-266 Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00330 ## Package Name: mariadb/server Ecosystem: github Affected: F-09 affects every released MariaDB version (confirmed 13.0.1 through 10.6.27). F-05 unfixed at HEAD as of 2026-08-03. Fixed: Unknown ## Root Cause # RCA Report — MDEV-40470-F05-RCE-CHAIN: MariaDB 13.0.1-rc pure-SQL RCE chain ## Summary A USAGE-only MariaDB account with TCP reachability to port 3306 achieves remote code execution as `uid=999(mysql)` on the **stock, unmodified** MariaDB 13.0.1-rc Docker image (`mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9`) using nothing but SQL statements. The chain combines: (F-09) a `GRANT PROXY ... IDENTIFIED VIA ''` privilege escalation that hijacks the root account with an empty password (MDEV-40470); a server-side `LOAD DATA INFILE '/proc/self/maps'` read that discloses the live PIE and libc base addresses (ASLR defeat); a 128 MiB user-variable buffer whose address is discovered by diffing `/proc/self/maps` from SQL; and (F-05) a `SYS_REFCURSOR` use-after-free in `sp_cursor_array::get_cursor_by_ref()` whose freed 1792-byte array chunk is reclaimed by an exact-fit heap spray, redirecting a virtual dispatch into a two-gadget JOP chain (D2 → D1) that calls libc `system()` with an attacker-chosen command string. ## Impact - **Package/component:** `mariadb/server` — server core (`sql/sql_acl.cc` GRANT PROXY handling; `sql/sp_cursor.{cc,h}` cursor array). - **Affected versions:** reproduced on 13.0.1-MariaDB-ubu2604 (pinned image). Per the advisory, F-09's fix (commit `dbd60d0ad8d`) exists only on dev branches and is absent from every released version 13.0.1 → 10.6.27; F-05 is unfixed upstream (no commits to `sql/sp_cursor.{cc,h}` since the 13.0.1 tag). - **Risk level:** Critical. Any authenticated low-privilege database user gains full OS command execution as the `mysql` service account (uid 999), i.e. complete database-server compromise, remotely, over the normal SQL protocol. ## Impact Parity - **Disclosed/claimed maximum impact:** code execution as uid 999 (mysql) from a USAGE-only account via pure SQL over TCP/3306. - **Reproduced impact from this run:** identical — attacker-chosen shell commands executed as `uid=999(mysql)` on the stock pinned image in 6/6 fresh processes (5 scripted + 1 manual calibration run), each with fresh ASLR. - **Parity:** `full`. ## Root Cause 1. **F-09 — GRANT PROXY privilege escalation (MDEV-40470).** `GRANT PROXY ON CURRENT_USER() TO 'root'@'%' IDENTIFIED VIA '';` passes an *empty* authentication clause. `LEX_USER::has_auth()` returns false, so the privilege check in `check_alter_user()` is skipped, while `replace_user_table()` still applies the (empty) password — replacing root's credentials with an empty password. One statement, any authenticated user. Fix: commit `dbd60d0ad8d` (dev branches only; no released version has it). Runtime proof: after the statement, login as `root` with `--skip-password` succeeds (`CURRENT_USER()=root@%`), and the pre-escalation negative control shows `ERROR 1045` on `LOAD DATA INFILE` and `ERROR 1227` on `SET GLOBAL`. 2. **ASLR defeat.** With the FILE privilege obtained in step 1 and `secure_file_priv` unset (stock), `LOAD DATA INFILE '/proc/self/maps'` loads mariadbd's own memory map into a table; plain SELECTs return the PIE base (first `r--p 00000000` mapping of `/usr/sbin/mariadbd`) and the libc base. Fresh values every process (observed 0x63abb3250000, 0x57b3cea2c000, 0x55b611d36000, 0x76955fffe000-area, 0x731cf3fff000-area). 3. **F-05 — SYS_REFCURSOR use-after-free (unfixed 0day).** `sp_cursor_array::get_cursor_by_ref()` returns an interior pointer into a `Dynamic_array`. When a cursor's `open()` executes SQL that opens more cursors (`grow5()` opens 16 cursors, then `OPEN p FOR SELECT spray128()`), the array grows, `my_realloc` frees the old 1792-byte storage (16×112 B), and the caller's cached pointer dangles. A heap spray of 128 session user variables of exactly 1784 bytes (`SET @e3sNNN=@e3pad`, glibc exact-fit for the 1792-byte chunk) reclaims the freed storage with attacker bytes, placing a controlled pointer `V` at offset 0x20 (the `result` member of `sp_cursor`). `Materialized_cursor::open()` then dispatches virtually: `mov rax,[result]; call [rax+0x20]`. 4. **JOP to `system()`.** The fake vtable `V` lives in a 128 MiB user-variable buffer whose address is learned via the maps diff (glibc gives such large allocations a dedicated mmap; the slot is reused across free+realloc, so the self-referential pointer baked by SQL stays valid; buffer data begins at region+0x30 — verified this run via gdb on the live process). Layout: `V+0x20=D2`, `V+0xa0=system`, `V+0xa8=V+0x140` (cmd ptr), `V+0x100=D1`, `V+0x140="sh -c ''"`. Dispatch: `call [rax+0x20]` → D2 (`call *0x100(%rax)`, PIE+0x80da77) → D1 (`mov rdi,[rax+0xa8]; call [rax+0xa0]`, PIE+0xe3075b) → `system()` (libc+0x5c560). All three gadget offsets were re-verified with `objdump` against the binaries extracted from the pinned image in this run (`logs/repro/gadget_check.log`). ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (run twice consecutively — both runs exited 0). 2. The script: installs `mariadb-client`/`binutils` if missing; pulls the pinned image; verifies the D2/D1/system gadget offsets in the extracted binaries; then performs **two** independent attempts, each against a freshly created container (fresh datadir, root password `labpass`, USAGE + `appdb.*`-only `lowpriv` account, fresh ASLR). Each attempt runs `bundle/repro/exploit_mdev40470.py`, a pure-SQL driver (mariadb client over TCP/3306) that executes the whole chain and finally reads the marker file `id > /tmp/mdev40470_pwned_attemptN` the payload wrote inside the container. 3. Expected evidence: per attempt — `F-09 done - root login with EMPTY password`, leaked PIE/libc bases, stable 128 MiB slot, session death on `CALL uaf5()`, and marker content `uid=999(mysql) gid=999(mysql) groups=999(mysql)`; script exits 0 only when both attempts confirm. Note on the exploit driver: it is the published PoC (github.com/dinosn/mariadb-13-rce-lab @ 6ac868e1) with *one* robustness fix in the `/proc/self/maps` region discovery — on this kernel the fresh 128 MiB mmap is sometimes VMA-merged with an adjacent pre-existing anonymous region (observed merged sizes 0x8022000 and 0x10002000), which the stock exact-size filter missed. The patched logic tracks all anonymous rw-p regions and handles both new-region and grew-region cases. Gadget offsets, sizes, DATA_OFF (0x30) and the whole chain are unchanged. ## Evidence - `bundle/logs/reproduction_steps.log` — full scripted run (exit 0, 2/2). - `bundle/logs/repro/version_check.log` — `13.0.1-MariaDB-ubu2604`. - `bundle/logs/repro/gadget_check.log` — objdump verification of D2/D1/system. - `bundle/logs/repro/vuln_attempt_1.log`, `vuln_attempt_2.log` — per-attempt chain transcripts (distinct ASLR bases). - `bundle/logs/repro/marker_attempt_1.log`, `marker_attempt_2.log` — `uid=999(mysql) gid=999(mysql) groups=999(mysql)`. - `bundle/logs/repro/vuln_attempt_token.log` + `marker_attempt_token.log` — additional run writing the unique token `MDEV40470_CHAIN_EXEC_1785762660`. - `bundle/logs/repro/negative_control_lowpriv.log` + `bundle/repro/negative_control_1.json` — identical procedure without F-09: `ERROR 1045` (FILE), `ERROR 1227` (SUPER), no marker, no crash; the escalation is the linchpin. - `bundle/repro/runtime_manifest.json` — tcp_peer entrypoint, service/health/ target-path flags, pinned-image target identity. - Environment: Docker 27.5.1 daemon, host kernel 6.8, container glibc 2.43; ASLR enabled (`/proc/sys/kernel/randomize_va_space` default); PIE binary. ## Recommendations / Next Steps - **F-09 (MDEV-40470):** backport commit `dbd60d0ad8d` to all maintained release branches; reject empty `IDENTIFIED VIA ''` authentication clauses in GRANT PROXY / treat them as authenticated changes requiring `check_alter_user()` privileges. - **F-05:** fix `sp_cursor_array::get_cursor_by_ref()` callers to re-validate the cursor reference after `open()` (or pin the array storage / use index-based lookup) so growth during a nested open cannot dangle the cached pointer. - **Defense in depth:** set `secure_file_priv` to a dedicated directory in the stock image; consider `local_infile=0` and restricting FILE privilege. - Until patched releases exist, any authenticated account must be treated as equivalent to full OS code execution as the mysql user. ## Additional Notes - **Idempotency:** `reproduction_steps.sh` was run twice consecutively; both runs passed (4/4 scripted attempts + 1 manual + 1 token run = 6/6 total). Each attempt recreates the container from the pinned image, so no state carries over. - **No fixed negative control exists:** the F-09 fix is not in any released version and F-05 is unfixed upstream, so no released image can serve as a fixed build. The in-band negative control (same procedure without the F-09 escalation) demonstrates the privilege gate; the gadget-offset verification binds the result to the exact pinned binaries. - **Kernel-dependent VMA merging** was the only environmental deviation from the published PoC (its exact-size maps filter). The patched discovery was verified against gdb ground truth (marker bytes `ABCD` found at region+0x30, confirming `DATA_OFF=0x30`; chunk header `0x8000ff2` IS_MMAPPED at region+8). - The server process crashes after `system()` returns (mariadbd is PID 1, so the container exits); marker files persist in the container layer and are read after `docker start`. This post-exploit housekeeping is the only non-SQL step and is not part of the exploitation. ## Reproduction Details Reproduced: 2026-08-23T15:38:53.860Z Duration: 3693 seconds Tool calls: 217 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00330 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00330&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00330/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://github.com/dinosn/mariadb-13-rce-lab ## Artifacts - bundle/repro/rca_report.md (analysis, 9731 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 7765 bytes) - bundle/logs/repro/dockerlogs_attempt_1.log (log, 15355 bytes) - bundle/logs/repro/dockerlogs_attempt_2.log (log, 15355 bytes) - bundle/logs/repro/marker_attempt_2.log (log, 48 bytes) - bundle/logs/repro/negative_control_lowpriv.log (log, 905 bytes) - bundle/logs/repro/version_check.log (log, 33 bytes) - bundle/repro/exploit_mdev40470.py (script, 14662 bytes) - bundle/repro/runtime_manifest.json (other, 1280 bytes) - bundle/repro/validation_verdict.json (other, 1194 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00330 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00330/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00330 ## 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