Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/gather/elasticsearch_enum.rb
Views: 11779
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::HttpClient7include Msf::Auxiliary::Report8include Msf::Module::Deprecated910moved_from 'auxiliary/scanner/elasticsearch/indices_enum'1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Elasticsearch Enumeration Utility',17'Description' => %q{18This module enumerates Elasticsearch instances. It uses the REST API19in order to gather information about the server, the cluster, nodes,20in the cluster, indices, and pull data from those indices.21},22'Author' => [23'Silas Cutler <Silas.Cutler[at]BlackListThisDomain.com>', # original indices enum module24'h00die' # generic enum module25],26'References' => [27['URL', 'https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html']28],29'License' => MSF_LICENSE,30'DefaultOptions' => {31'SSL' => true32},33'Notes' => {34'Stability' => [CRASH_SAFE],35'Reliability' => [],36'SideEffects' => [IOC_IN_LOGS]37}38)39)4041register_options(42[43Opt::RPORT(9200),44OptString.new('USERNAME', [false, 'A specific username to authenticate as', '']),45OptString.new('PASSWORD', [false, 'A specific password to authenticate as', '']),46OptInt.new('DOWNLOADROWS', [true, 'Number of beginning and ending rows to download per index', 5])47]48)49end5051def get_results(index)52vprint_status("Downloading #{datastore['DOWNLOADROWS']} rows from index #{index}")53body = { 'query' => { 'query_string' => { 'query' => '*' } }, 'size' => datastore['DOWNLOADROWS'], 'from' => 0, 'sort' => [] }54request = {55'uri' => normalize_uri(target_uri.path, index, '_search/'),56'method' => 'POST',57'headers' => {58'Accept' => 'application/json'59},60'ctype' => 'application/json',61'data' => body.to_json62}63request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']6465res = send_request_cgi(request)66vprint_error('Unable to establish connection') if res.nil?6768if res && res.code == 200 && !res.body.empty?69json_body = res.get_json_document70if json_body.empty?71vprint_error('Unable to parse JSON')72return73end74else75vprint_error('Timeout or unexpected response...')76return77end7879columns = json_body.dig('hits', 'hits')[0]['_source'].keys80elastic_table = Rex::Text::Table.new(81'Header' => "#{index} Data",82'Indent' => 2,83# we know at least 1 row since we wouldn't query an index w/o a row84'Columns' => columns85)86json_body.dig('hits', 'hits').each do |hash|87elastic_table << columns.map { |column| hash['_source'][column] }88end8990l = store_loot('elasticserch.index.data', 'application/csv', rhost, elastic_table.to_csv, "#{index}_data.csv", nil, @service)91print_good("#{index} data stored to #{l}")92end9394def get_indices95vprint_status('Querying indices...')96request = {97'uri' => normalize_uri(target_uri.path, '_cat', 'indices/'),98'method' => 'GET',99'headers' => {100'Accept' => 'application/json'101},102'vars_get' => {103# this is the query https://github.com/cars10/elasticvue uses for the chrome browser extension104'h' => 'index,health,status,uuid,docs.count,store.size',105'bytes' => 'mb'106}107}108request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']109110res = send_request_cgi(request)111vprint_error('Unable to establish connection') if res.nil?112113if res && res.code == 200 && !res.body.empty?114json_body = res.get_json_document115if json_body.empty?116vprint_error('Unable to parse JSON')117return118end119else120vprint_error('Timeout or unexpected response...')121return122end123124elastic_table = Rex::Text::Table.new(125'Header' => 'Indicies Information',126'Indent' => 2,127'Columns' =>128[129'Name',130'Health',131'Status',132'UUID',133'Documents',134'Storage Usage (MB)'135]136)137138indices = []139140json_body.each do |index|141next if datastore['VERBOSE'] == false && index['index'].starts_with?('.fleet')142143indices << index['index'] if index['docs.count'].to_i > 0 # avoid querying something with no data144elastic_table << [145index['index'],146index['health'],147index['status'],148index['uuid'],149index['docs.count'],150"#{index['store.size']}MB"151]152report_note(153host: rhost,154port: rport,155proto: 'tcp',156type: 'elasticsearch.index',157data: index[0],158update: :unique_data159)160end161162print_good(elastic_table.to_s)163indices.each do |index|164get_results(index)165end166end167168def get_cluster_info169vprint_status('Querying cluster information...')170request = {171'uri' => normalize_uri(target_uri.path, '_cluster', 'health'),172'method' => 'GET'173}174request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']175176res = send_request_cgi(request)177178fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?179fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401180181if res.code == 200 && !res.body.empty?182json_body = res.get_json_document183if json_body.empty?184vprint_error('Unable to parse JSON')185return186end187end188189elastic_table = Rex::Text::Table.new(190'Header' => 'Cluster Information',191'Indent' => 2,192'Columns' =>193[194'Cluster Name',195'Status',196'Number of Nodes'197]198)199200elastic_table << [201json_body['cluster_name'],202json_body['status'],203json_body['number_of_nodes']204]205print_good(elastic_table.to_s)206end207208def get_node_info209vprint_status('Querying node information...')210request = {211'uri' => normalize_uri(target_uri.path, '_cat', 'nodes'),212'method' => 'GET',213'headers' => {214'Accept' => 'application/json'215},216'vars_get' => {217'h' => 'ip,port,version,http,uptime,name,heap.current,heap.max,ram.current,ram.max,node.role,master,cpu,disk.used,disk.total'218}219}220request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']221222res = send_request_cgi(request)223224fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?225fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401226227if res.code == 200 && !res.body.empty?228json_body = res.get_json_document229if json_body.empty?230vprint_error('Unable to parse JSON')231return232end233end234235elastic_table = Rex::Text::Table.new(236'Header' => 'Node Information',237'Indent' => 2,238'Columns' =>239[240'IP',241'Transport Port',242'HTTP Port',243'Version',244'Name',245'Uptime',246'Ram Usage',247'Node Role',248'Master',249'CPU Load',250'Disk Usage'251]252)253json_body.each do |node|254report_service(255host: node['ip'],256port: node['port'],257proto: 'tcp',258name: 'elasticsearch'259)260report_service(261host: node['ip'],262port: node['http'].split(':')[1],263proto: 'tcp',264name: 'elasticsearch'265)266elastic_table << [267node['ip'],268node['port'],269node['http'],270node['version'],271node['name'],272node['uptime'],273"#{node['ram.current']}/#{node['ram.max']}",274node['node.role'],275node['master'],276"#{node['cpu']}%",277"#{node['disk.used']}/#{node['disk.total']}"278]279end280print_good(elastic_table.to_s)281end282283def get_version_info284vprint_status('Querying version information...')285request = {286'uri' => normalize_uri(target_uri.path),287'method' => 'GET'288}289request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']290291res = send_request_cgi(request)292293fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?294fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401295296# leaving this here for future travelers, this header was added in 7.14.0 https://www.elastic.co/guide/en/elasticsearch/reference/7.17/release-notes-7.14.0.html297# so it isn't too reliable to check for298# fail_with(Failure::Unreachable, "#{peer} - Elasticsearch not detected in X-elastic-product header") unless res.headers['X-elastic-product'] == 'Elasticsearch'299300if res.code == 200 && !res.body.empty?301json_body = res.get_json_document302if json_body.empty?303vprint_error('Unable to parse JSON')304return305end306end307308fail_with(Failure::Unreachable, "#{peer} - Elasticsearch cluster name not found, likely not Elasticsearch server") unless json_body['cluster_name']309310elastic_table = Rex::Text::Table.new(311'Header' => 'Elastic Information',312'Indent' => 2,313'Columns' =>314[315'Name',316'Cluster Name',317'Version',318'Build Type',319'Lucene Version'320]321)322323elastic_table << [324json_body['name'],325json_body['cluster_name'],326json_body.dig('version', 'number'),327json_body.dig('version', 'build_type'),328json_body.dig('version', 'lucene_version'),329]330print_good(elastic_table.to_s)331332@service = report_service(333host: rhost,334port: rport,335proto: 'tcp',336name: 'elasticsearch'337)338end339340def get_users341vprint_status('Querying user information...')342request = {343'uri' => normalize_uri(target_uri.path, '_security', 'user/'),344'method' => 'GET'345}346request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']347348res = send_request_cgi(request)349350fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?351fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401352353if res.code == 200 && !res.body.empty?354json_body = res.get_json_document355if json_body.empty?356vprint_error('Unable to parse JSON')357return358end359end360361if json_body.nil?362print_bad('Unable to pull user data')363return364end365366elastic_table = Rex::Text::Table.new(367'Header' => 'User Information',368'Indent' => 2,369'Columns' =>370[371'Name',372'Roles',373'Email',374'Metadata',375'Enabled'376]377)378379json_body.each do |username, attributes|380elastic_table << [381username,382attributes['roles'],383attributes['email'],384attributes['metadata'],385attributes['enabled'],386]387end388print_good(elastic_table.to_s)389end390391def run392get_version_info393get_node_info394get_cluster_info395get_indices396get_users397end398end399400401