CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/ldap/auth.rb
Views: 11704
1
require 'net/ldap'
2
require 'net/ldap/dn'
3
4
module Rex
5
module Proto
6
module LDAP
7
class Auth
8
SUPPORTS_SASL = %w[GSS-SPNEGO NTLM]
9
NTLM_CONST = Rex::Proto::NTLM::Constants
10
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
11
MESSAGE = Rex::Proto::NTLM::Message
12
13
#
14
# Initialize the required variables
15
#
16
# @param challenge [String] NTLM Server Challenge
17
# @param domain [String] Domain value used in NTLM
18
# @param server [String] Server value used in NTLM
19
# @param dnsname [String] DNS Name value used in NTLM
20
# @param dnsdomain [String] DNS Domain value used in NTLM
21
def initialize(challenge, domain, server, dnsname, dnsdomain)
22
@domain = domain.nil? ? 'DOMAIN' : domain
23
@server = server.nil? ? 'SERVER' : server
24
@dnsname = dnsname.nil? ? 'server' : dnsname
25
@dnsdomain = dnsdomain.nil? ? 'example.com' : dnsdomain
26
@challenge = [challenge.nil? ? Rex::Text.rand_text_alphanumeric(16) : challenge].pack('H*')
27
end
28
29
#
30
# Process the incoming LDAP login requests from clients
31
#
32
# @param user_login [OpenStruct] User login information
33
#
34
# @return auth_info [Hash] Processed authentication information
35
def process_login_request(user_login)
36
auth_info = {}
37
38
if user_login.name.empty? && user_login.authentication.empty? # Anonymous
39
auth_info = handle_anonymous_request(user_login, auth_info)
40
elsif !user_login.name.empty? # Simple
41
auth_info = handle_simple_request(user_login, auth_info)
42
elsif sasl?(user_login)
43
auth_info = handle_sasl_request(user_login, auth_info)
44
else
45
auth_info = handle_unknown_request(user_login, auth_info)
46
end
47
48
auth_info
49
end
50
51
#
52
# Handle Anonymous authentication requests
53
#
54
# @param user_login [OpenStruct] User login information
55
# @param auth_info [Hash] Processed authentication information
56
#
57
# @return auth_info [Hash] Processed authentication information
58
def handle_anonymous_request(user_login, auth_info = {})
59
if user_login.name.empty? && user_login.authentication.empty?
60
auth_info[:user] = user_login.name
61
auth_info[:pass] = user_login.authentication
62
auth_info[:domain] = nil
63
auth_info[:result_code] = Net::LDAP::ResultCodeSuccess
64
auth_info[:auth_type] = 'Anonymous'
65
end
66
auth_info
67
end
68
69
#
70
# Handle Unknown authentication requests
71
#
72
# @param user_login [OpenStruct] User login information
73
# @param auth_info [Hash] Processed authentication information
74
#
75
# @return auth_info [Hash] Processed authentication information
76
def handle_unknown_request(user_login, auth_info = {})
77
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
78
auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Authentication Format'
79
auth_info
80
end
81
82
#
83
# Handle Simple authentication requests
84
#
85
# @param user_login [OpenStruct] User login information
86
# @param auth_info [Hash] Processed authentication information
87
#
88
# @return auth_info [Hash] Processed authentication information
89
def handle_simple_request(user_login, auth_info = {})
90
domains = []
91
names = []
92
if !user_login.name.empty?
93
if user_login.name =~ /@/
94
pub_info = user_login.name.split('@')
95
if pub_info.length <= 2
96
auth_info[:user], auth_info[:domain] = pub_info
97
else
98
auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
99
auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
100
end
101
elsif user_login.name =~ /,/
102
begin
103
dn = Net::LDAP::DN.new(user_login.name)
104
dn.each_pair do |key, value|
105
if key == 'cn'
106
names << value
107
elsif key == 'dc'
108
domains << value
109
end
110
end
111
auth_info[:user] = names.join('')
112
auth_info[:domain] = domains.empty? ? nil : domains.join('.')
113
rescue Net::LDAP::InvalidDNError => e
114
auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
115
raise e
116
end
117
elsif user_login.name =~ /\\/
118
pub_info = user_login.name.split('\\')
119
if pub_info.length <= 2
120
auth_info[:domain], auth_info[:user] = pub_info
121
else
122
auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
123
auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
124
end
125
else
126
auth_info[:user] = user_login.name
127
auth_info[:domain] = nil
128
auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
129
end
130
auth_info[:private] = user_login.authentication
131
auth_info[:private_type] = :password
132
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil?
133
auth_info[:auth_type] = 'Simple'
134
auth_info
135
end
136
end
137
138
#
139
# Handle SASL authentication requests
140
#
141
# @param user_login [OpenStruct] User login information
142
# @param auth_info [Hash] Processed authentication information
143
#
144
# @return auth_info [Hash] Processed authentication information
145
def handle_sasl_request(user_login, auth_info = {})
146
case user_login.authentication[1]
147
when /NTLMSSP/
148
message = Net::NTLM::Message.parse(user_login.authentication[1])
149
if message.is_a?(::Net::NTLM::Message::Type1)
150
auth_info[:server_creds] = generate_type2_response(message)
151
auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress
152
elsif message.is_a?(::Net::NTLM::Message::Type3)
153
auth_info = handle_type3_message(message, auth_info)
154
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
155
end
156
else
157
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
158
auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unsupported SASL Format'
159
end
160
auth_info[:auth_type] = 'SASL'
161
auth_info
162
end
163
164
private
165
166
#
167
# Determine if the supplied request is formatted for SASL auth
168
#
169
# @param user_login [OpenStruct] User login information
170
#
171
# @return [bool] True if the request can be processed for SASL auth
172
def sasl?(user_login)
173
if user_login.authentication.is_a?(Array) && SUPPORTS_SASL.include?(user_login.authentication[0])
174
return true
175
end
176
177
false
178
end
179
180
#
181
# Generate NTLM Type2 response from NTLM Type1 message
182
#
183
# @param message [Net::NTLM::Message::Type1] NTLM Type1 message
184
#
185
# @return server_hash [String] NTLM Type2 response that is sent as server credentials
186
def generate_type2_response(message)
187
dom = message.domain
188
ws = message.workstation
189
domain = dom.empty? ? @domain : dom
190
server = ws.empty? ? @server : ws
191
server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, domain, server, @dnsname, @dnsdomain)
192
Rex::Text.decode_base64(server_hash)
193
end
194
195
#
196
# Handle NTLM Type3 message
197
#
198
# @param message [Net::NTLM::Message::Type3] NTLM Type3 message
199
# @param auth_info [Hash] Processed authentication information
200
#
201
# @return auth_info [Hash] Processed authentication information
202
def handle_type3_message(message, auth_info = {})
203
arg = {
204
domain: message.domain,
205
user: message.user,
206
host: message.workstation
207
}
208
209
domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(message.encode64)
210
nt_len = ntlm_hash.length
211
212
if nt_len == 48
213
arg[:ntlm_ver] = NTLM_CONST::NTLM_V1_RESPONSE
214
arg[:lm_hash] = lm_hash
215
arg[:nt_hash] = ntlm_hash
216
217
if arg[:lm_hash][16, 32] == '0' * 32
218
arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
219
end
220
elsif nt_len > 48
221
arg[:ntlm_ver] = NTLM_CONST::NTLM_V2_RESPONSE
222
arg[:lm_hash] = lm_hash[0, 32]
223
arg[:lm_cli_challenge] = lm_hash[32, 16]
224
arg[:nt_hash] = ntlm_hash[0, 32]
225
arg[:nt_cli_challenge] = ntlm_hash[32, nt_len - 32]
226
else
227
auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..."
228
end
229
auth_info.merge(process_ntlm_hash(arg)) unless arg.nil?
230
end
231
232
#
233
# Process the NTLM Hash received from NTLM Type3 message
234
#
235
# @param arg [Hash] authentication information received from Type3 message
236
#
237
# @return arg [Hash] Processed NTLM authentication information
238
def process_ntlm_hash(arg = {})
239
ntlm_ver = arg[:ntlm_ver]
240
lm_hash = arg[:lm_hash]
241
nt_hash = arg[:nt_hash]
242
unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
243
lm_cli_challenge = arg[:lm_cli_challenge]
244
nt_cli_challenge = arg[:nt_cli_challenge]
245
end
246
domain = Rex::Text.to_ascii(arg[:domain])
247
user = Rex::Text.to_ascii(arg[:user])
248
host = Rex::Text.to_ascii(arg[:host])
249
250
case ntlm_ver
251
when NTLM_CONST::NTLM_V1_RESPONSE
252
if NTLM_CRYPT.is_hash_from_empty_pwd?({
253
hash: [nt_hash].pack('H*'),
254
srv_challenge: @challenge,
255
ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
256
type: 'ntlm'
257
})
258
arg[:error_msg] = 'NLMv1 Hash correspond to an empty password, ignoring ... '
259
return
260
end
261
if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/
262
lm_hash_message = 'Disabled'
263
elsif NTLM_CRYPT.is_hash_from_empty_pwd?({
264
hash: [lm_hash].pack('H*'),
265
srv_challenge: @challenge,
266
ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
267
type: 'lm'
268
})
269
lm_hash_message = 'Disabled (from empty password)'
270
else
271
lm_hash_message = lm_hash
272
end
273
274
hash = [
275
lm_hash || '0' * 48,
276
nt_hash || '0' * 48
277
].join(':').gsub(/\n/, '\\n')
278
arg[:private] = hash
279
when NTLM_CONST::NTLM_V2_RESPONSE
280
if NTLM_CRYPT.is_hash_from_empty_pwd?({
281
hash: [nt_hash].pack('H*'),
282
srv_challenge: @challenge,
283
cli_challenge: [nt_cli_challenge].pack('H*'),
284
user: user,
285
domain: domain,
286
ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
287
type: 'ntlm'
288
})
289
arg[:error_msg] = 'NTLMv2 Hash correspond to an empty password, ignoring ... '
290
return
291
end
292
if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16)
293
lm_hash_message = 'Disabled'
294
elsif NTLM_CRYPT.is_hash_from_empty_pwd?({
295
hash: [lm_hash].pack('H*'),
296
srv_challenge: @challenge,
297
cli_challenge: [lm_cli_challenge].pack('H*'),
298
user: user,
299
domain: domain,
300
ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
301
type: 'lm'
302
})
303
lm_hash_message = 'Disabled (from empty password)'
304
else
305
lm_hash_message = lm_hash
306
end
307
308
hash = [
309
lm_hash || '0' * 32,
310
nt_hash || '0' * 32
311
].join(':').gsub(/\n/, '\\n')
312
arg[:private] = hash
313
when NTLM_CONST::NTLM_2_SESSION_RESPONSE
314
if NTLM_CRYPT.is_hash_from_empty_pwd?({
315
hash: [nt_hash].pack('H*'),
316
srv_challenge: @challenge,
317
cli_challenge: [lm_hash].pack('H*')[0, 8],
318
ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE,
319
type: 'ntlm'
320
})
321
arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... '
322
return
323
end
324
325
hash = [
326
lm_hash || '0' * 48,
327
nt_hash || '0' * 48
328
].join(':').gsub(/\n/, '\\n')
329
arg[:private] = hash
330
else
331
return
332
end
333
arg[:domain] = domain
334
arg[:user] = user
335
arg[:host] = host
336
arg[:private_type] = :ntlm_hash
337
arg
338
end
339
end
340
end
341
end
342
end
343
344