##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  # Exploit mixins should be called first
  include Msf::Exploit::Remote::HttpClient
  # Scanner mixin should be near last
  include Msf::Auxiliary::Scanner

  def initialize
    super(
      'Name'        => 'HTTP HTML Title Tag Content Grabber',
      'Description' => %q{
        Generates a GET request to the provided webservers and returns the server header,
        HTML title attribute and location header (if set). This is useful for rapidly identifying
        interesting web applications en mass.
      },
      'Author'       => 'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>',
      'License'     => MSF_LICENSE,
    )

    register_options(
      [
        OptBool.new('STORE_NOTES', [ true, 'Store the captured information in notes. Use "notes -t http.title" to view', true ]),
        OptBool.new('SHOW_TITLES', [ true, 'Show the titles on the console as they are grabbed', true ]),
        OptString.new('TARGETURI', [true, 'The base path', '/'])
      ])

    register_advanced_options(
      [
        OptString.new('HttpQueryString', [ false, 'The HTTP query string', nil ]),
        OptBool.new('FollowRedirect', [ false, 'Follow a HTTP redirect', false ]),
        OptInt.new('FollowRedirectDepth', [false, 'Follow HTTP redirect depth', 1]),
      ]
    )
  end

  def run
    if !datastore['STORE_NOTES'] && !datastore['SHOW_TITLES']
      print_error("Notes storage is false and titles are not being shown on the console. There isn't much point in running this module.")
    else
      super
    end
  end

  def run_host(target_host)
    begin
      http_opts = {
        'uri' => normalize_uri(target_uri.path),
        'query' => datastore['HttpQueryString']
      }

      # Send a normal GET request
      if datastore['FollowRedirect']
        res = send_request_cgi!(http_opts, datastore['HttpClientTimeout'] || 20, datastore['FollowRedirectDepth'])
      else
        res = send_request_cgi(http_opts)
      end

      # If no response, quit now
      if res.nil?
        vprint_error("[#{target_host}:#{rport}] No response")
        return
      end

      # Retrieve the headers to capture the Location and Server header
      # Note that they are case-insensitive but stored in a hash
      server_header = nil
      location_header = nil
      if !res.headers.nil?
        res.headers.each do |key, val|
          location_header = val if key.downcase == 'location'
          server_header  = val if key.downcase == 'server'
        end
      else
        vprint_error("[#{target_host}:#{rport}] No HTTP headers")
      end

      # If the body is blank, just stop now as there is no chance of a title
      if res.body.nil?
        vprint_error("[#{target_host}:#{rport}] No webpage body")
        return
      end

      # Very basic, just match the first title tag we come to. If the match fails,
      # there is no chance that we will have a title
      rx = %r{<title>[\n\t\s]*(?<title>.+?)[\s\n\t]*</title>}im.match(res.body.to_s)
      unless rx
        vprint_error("[#{target_host}:#{rport}] No webpage title")
        return
      end

      # Last bit of logic to capture the title
      rx[:title].strip!
      if rx[:title] != ''
        rx_title = Rex::Text.html_decode(rx[:title])
        if datastore['SHOW_TITLES']
          print_good("[#{target_host}:#{rport}] [C:#{res.code}] [R:#{location_header}] [S:#{server_header}] #{rx_title}")
        end
        if datastore['STORE_NOTES']
          notedata = { code: res.code, port: rport, server: server_header, title: rx_title, redirect: location_header, uri: datastore['TARGETURI'] }
          report_note(host: target_host, port: rport, type: "http.title", data: notedata, update: :unique_data)
        end
      else
        vprint_error("[#{target_host}:#{rport}] No webpage title")
      end
    end

    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
    rescue ::Timeout::Error, ::Errno::EPIPE
  end
end
