codekingpro/portable-devtools
114k
1"""
2"""
3
4# Created on 2015.04.02
5#
6# Author: Giovanni Cannata
7#
8# Copyright 2015 - 2020 Giovanni Cannata
9#
10# This file is part of ldap3.
11#
12# ldap3 is free software: you can redistribute it and/or modify
13# it under the terms of the GNU Lesser General Public License as published
14# by the Free Software Foundation, either version 3 of the License, or
15# (at your option) any later version.
16#
17# ldap3 is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU Lesser General Public License for more details.
21#
22# You should have received a copy of the GNU Lesser General Public License
23# along with ldap3 in the COPYING and COPYING.LESSER files.
24# If not, see <http://www.gnu.org/licenses/>.
25
26# NTLMv2 authentication as per [MS-NLMP] (https://msdn.microsoft.com/en-us/library/cc236621.aspx)
27
28from struct import pack, unpack
29from platform import system, version
30from socket import gethostname
31from time import time
32import hmac
33import hashlib
34import binascii
35from os import urandom
36
37try:
38 from locale import getpreferredencoding
39 oem_encoding = getpreferredencoding()
40except Exception:
41 oem_encoding = 'utf-8'
42
43from ..protocol.formatters.formatters import format_ad_timestamp
44
45NTLM_SIGNATURE = b'NTLMSSP\x00'
46NTLM_MESSAGE_TYPE_NTLM_NEGOTIATE = 1
47NTLM_MESSAGE_TYPE_NTLM_CHALLENGE = 2
48NTLM_MESSAGE_TYPE_NTLM_AUTHENTICATE = 3
49
50FLAG_NEGOTIATE_56 = 31 # W
51FLAG_NEGOTIATE_KEY_EXCH = 30 # V
52FLAG_NEGOTIATE_128 = 29 # U
53FLAG_NEGOTIATE_VERSION = 25 # T
54FLAG_NEGOTIATE_TARGET_INFO = 23 # S
55FLAG_REQUEST_NOT_NT_SESSION_KEY = 22 # R
56FLAG_NEGOTIATE_IDENTIFY = 20 # Q
57FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY = 19 # P
58FLAG_TARGET_TYPE_SERVER = 17 # O
59FLAG_TARGET_TYPE_DOMAIN = 16 # N
60FLAG_NEGOTIATE_ALWAYS_SIGN = 15 # M
61FLAG_NEGOTIATE_OEM_WORKSTATION_SUPPLIED = 13 # L
62FLAG_NEGOTIATE_OEM_DOMAIN_SUPPLIED = 12 # K
63FLAG_NEGOTIATE_ANONYMOUS = 11 # J
64FLAG_NEGOTIATE_NTLM = 9 # H
65FLAG_NEGOTIATE_LM_KEY = 7 # G
66FLAG_NEGOTIATE_DATAGRAM = 6 # F
67FLAG_NEGOTIATE_SEAL = 5 # E
68FLAG_NEGOTIATE_SIGN = 4 # D
69FLAG_REQUEST_TARGET = 2 # C
70FLAG_NEGOTIATE_OEM = 1 # B
71FLAG_NEGOTIATE_UNICODE = 0 # A
72
73FLAG_TYPES = [FLAG_NEGOTIATE_56,
74 FLAG_NEGOTIATE_KEY_EXCH,
75 FLAG_NEGOTIATE_128,
76 FLAG_NEGOTIATE_VERSION,
77 FLAG_NEGOTIATE_TARGET_INFO,
78 FLAG_REQUEST_NOT_NT_SESSION_KEY,
79 FLAG_NEGOTIATE_IDENTIFY,
80 FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY,
81 FLAG_TARGET_TYPE_SERVER,
82 FLAG_TARGET_TYPE_DOMAIN,
83 FLAG_NEGOTIATE_ALWAYS_SIGN,
84 FLAG_NEGOTIATE_OEM_WORKSTATION_SUPPLIED,
85 FLAG_NEGOTIATE_OEM_DOMAIN_SUPPLIED,
86 FLAG_NEGOTIATE_ANONYMOUS,
87 FLAG_NEGOTIATE_NTLM,
88 FLAG_NEGOTIATE_LM_KEY,
89 FLAG_NEGOTIATE_DATAGRAM,
90 FLAG_NEGOTIATE_SEAL,
91 FLAG_NEGOTIATE_SIGN,
92 FLAG_REQUEST_TARGET,
93 FLAG_NEGOTIATE_OEM,
94 FLAG_NEGOTIATE_UNICODE]
95
96AV_END_OF_LIST = 0
97AV_NETBIOS_COMPUTER_NAME = 1
98AV_NETBIOS_DOMAIN_NAME = 2
99AV_DNS_COMPUTER_NAME = 3
100AV_DNS_DOMAIN_NAME = 4
101AV_DNS_TREE_NAME = 5
102AV_FLAGS = 6
103AV_TIMESTAMP = 7
104AV_SINGLE_HOST_DATA = 8
105AV_TARGET_NAME = 9
106AV_CHANNEL_BINDINGS = 10
107
108AV_TYPES = [AV_END_OF_LIST,
109 AV_NETBIOS_COMPUTER_NAME,
110 AV_NETBIOS_DOMAIN_NAME,
111 AV_DNS_COMPUTER_NAME,
112 AV_DNS_DOMAIN_NAME,
113 AV_DNS_TREE_NAME,
114 AV_FLAGS,
115 AV_TIMESTAMP,
116 AV_SINGLE_HOST_DATA,
117 AV_TARGET_NAME,
118 AV_CHANNEL_BINDINGS]
119
120AV_FLAG_CONSTRAINED = 0
121AV_FLAG_INTEGRITY = 1
122AV_FLAG_TARGET_SPN_UNTRUSTED = 2
123
124AV_FLAG_TYPES = [AV_FLAG_CONSTRAINED,
125 AV_FLAG_INTEGRITY,
126 AV_FLAG_TARGET_SPN_UNTRUSTED]
127
128
129def pack_windows_version(debug=False):
130 if debug:
131 if system().lower() == 'windows':
132 try:
133 major_release, minor_release, build = version().split('.')
134 major_release = int(major_release)
135 minor_release = int(minor_release)
136 build = int(build)
137 except Exception:
138 major_release = 5
139 minor_release = 1
140 build = 2600
141 else:
142 major_release = 5
143 minor_release = 1
144 build = 2600
145 else:
146 major_release = 0
147 minor_release = 0
148 build = 0
149
150 return pack('<B', major_release) + \
151 pack('<B', minor_release) + \
152 pack('<H', build) + \
153 pack('<B', 0) + \
154 pack('<B', 0) + \
155 pack('<B', 0) + \
156 pack('<B', 15)
157
158
159def unpack_windows_version(version_message):
160 if len(version_message) != 8:
161 raise ValueError('version field must be 8 bytes long')
162
163 if str is bytes: # Python 2
164 return (unpack('<B', version_message[0])[0],
165 unpack('<B', version_message[1])[0],
166 unpack('<H', version_message[2:4])[0],
167 unpack('<B', version_message[7])[0])
168 else: # Python 3
169 return (int(version_message[0]),
170 int(version_message[1]),
171 int(unpack('<H', version_message[2:4])[0]),
172 int(version_message[7]))
173
174
175class NtlmClient(object):
176 def __init__(self, domain, user_name, password):
177 self.client_config_flags = 0
178 self.exported_session_key = None
179 self.negotiated_flags = None
180 self.user_name = user_name
181 self.user_domain = domain
182 self.no_lm_response_ntlm_v1 = None
183 self.client_blocked = False
184 self.client_block_exceptions = []
185 self.client_require_128_bit_encryption = None
186 self.max_life_time = None
187 self.client_signing_key = None
188 self.client_sealing_key = None
189 self.sequence_number = None
190 self.server_sealing_key = None
191 self.server_signing_key = None
192 self.integrity = False
193 self.replay_detect = False
194 self.sequence_detect = False
195 self.confidentiality = False
196 self.datagram = False
197 self.identity = False
198 self.client_supplied_target_name = None
199 self.client_channel_binding_unhashed = None
200 self.unverified_target_name = None
201 self._password = password
202 self.server_challenge = None
203 self.server_target_name = None
204 self.server_target_info = None
205 self.server_version = None
206 self.server_av_netbios_computer_name = None
207 self.server_av_netbios_domain_name = None
208 self.server_av_dns_computer_name = None
209 self.server_av_dns_domain_name = None
210 self.server_av_dns_forest_name = None
211 self.server_av_target_name = None
212 self.server_av_flags = None
213 self.server_av_timestamp = None
214 self.server_av_single_host_data = None
215 self.server_av_channel_bindings = None
216 self.server_av_flag_constrained = None
217 self.server_av_flag_integrity = None
218 self.server_av_flag_target_spn_untrusted = None
219 self.current_encoding = None
220 self.client_challenge = None
221 self.server_target_info_raw = None
222
223 def get_client_flag(self, flag):
224 if not self.client_config_flags:
225 return False
226
227 if flag in FLAG_TYPES:
228 return True if self.client_config_flags & (1 << flag) else False
229
230 raise ValueError('invalid flag')
231
232 def get_negotiated_flag(self, flag):
233 if not self.negotiated_flags:
234 return False
235
236 if flag not in FLAG_TYPES:
237 raise ValueError('invalid flag')
238
239 return True if self.negotiated_flags & (1 << flag) else False
240
241 def get_server_av_flag(self, flag):
242 if not self.server_av_flags:
243 return False
244
245 if flag not in AV_FLAG_TYPES:
246 raise ValueError('invalid AV flag')
247
248 return True if self.server_av_flags & (1 << flag) else False
249
250 def set_client_flag(self, flags):
251 if type(flags) == int:
252 flags = [flags]
253 for flag in flags:
254 if flag in FLAG_TYPES:
255 self.client_config_flags |= (1 << flag)
256 else:
257 raise ValueError('invalid flag')
258
259 def reset_client_flags(self):
260 self.client_config_flags = 0
261
262 def unset_client_flag(self, flags):
263 if type(flags) == int:
264 flags = [flags]
265 for flag in flags:
266 if flag in FLAG_TYPES:
267 self.client_config_flags &= ~(1 << flag)
268 else:
269 raise ValueError('invalid flag')
270
271 def create_negotiate_message(self):
272 """
273 Microsoft MS-NLMP 2.2.1.1
274 """
275 self.reset_client_flags()
276 self.set_client_flag([FLAG_REQUEST_TARGET,
277 FLAG_NEGOTIATE_56,
278 FLAG_NEGOTIATE_128,
279 FLAG_NEGOTIATE_NTLM,
280 FLAG_NEGOTIATE_ALWAYS_SIGN,
281 FLAG_NEGOTIATE_OEM,
282 FLAG_NEGOTIATE_UNICODE,
283 FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY])
284
285 message = NTLM_SIGNATURE # 8 bytes
286 message += pack('<I', NTLM_MESSAGE_TYPE_NTLM_NEGOTIATE) # 4 bytes
287 message += pack('<I', self.client_config_flags) # 4 bytes
288 message += self.pack_field('', 40) # domain name field # 8 bytes
289 if self.get_client_flag(FLAG_NEGOTIATE_VERSION): # version 8 bytes - used for debug in ntlm
290 message += pack_windows_version(True)
291 else:
292 message += pack_windows_version(False)
293 return message
294
295 def parse_challenge_message(self, message):
296 """
297 Microsoft MS-NLMP 2.2.1.2
298 """
299 if len(message) < 56: # minimum size of challenge message
300 return False
301
302 if message[0:8] != NTLM_SIGNATURE: # NTLM signature - 8 bytes
303 return False
304
305 if int(unpack('<I', message[8:12])[0]) != NTLM_MESSAGE_TYPE_NTLM_CHALLENGE: # type of message - 4 bytes
306 return False
307
308 target_name_len, _, target_name_offset = self.unpack_field(message[12:20]) # targetNameFields - 8 bytes
309 self.negotiated_flags = unpack('<I', message[20:24])[0] # negotiated flags - 4 bytes
310 self.current_encoding = 'utf-16-le' if self.get_negotiated_flag(
311 FLAG_NEGOTIATE_UNICODE) else oem_encoding # set encoding
312
313 self.server_challenge = message[24:32] # server challenge - 8 bytes
314 target_info_len, _, target_info_offset = self.unpack_field(message[40:48]) # targetInfoFields - 8 bytes
315 self.server_version = unpack_windows_version(message[48:56])
316 if self.get_negotiated_flag(FLAG_REQUEST_TARGET) and target_name_len:
317 self.server_target_name = message[target_name_offset: target_name_offset + target_name_len].decode(
318 self.current_encoding)
319 if self.get_negotiated_flag(FLAG_NEGOTIATE_TARGET_INFO) and target_info_len:
320 self.server_target_info_raw = message[target_info_offset: target_info_offset + target_info_len]
321 self.server_target_info = self.unpack_av_info(self.server_target_info_raw)
322 for attribute, value in self.server_target_info:
323 if attribute == AV_NETBIOS_COMPUTER_NAME:
324 self.server_av_netbios_computer_name = value.decode('utf-16-le') # always unicode
325 elif attribute == AV_NETBIOS_DOMAIN_NAME:
326 self.server_av_netbios_domain_name = value.decode('utf-16-le') # always unicode
327 elif attribute == AV_DNS_COMPUTER_NAME:
328 self.server_av_dns_computer_name = value.decode('utf-16-le') # always unicode
329 elif attribute == AV_DNS_DOMAIN_NAME:
330 self.server_av_dns_domain_name = value.decode('utf-16-le') # always unicode
331 elif attribute == AV_DNS_TREE_NAME:
332 self.server_av_dns_forest_name = value.decode('utf-16-le') # always unicode
333 elif attribute == AV_FLAGS:
334 if self.get_server_av_flag(AV_FLAG_CONSTRAINED):
335 self.server_av_flag_constrained = True
336 if self.get_server_av_flag(AV_FLAG_INTEGRITY):
337 self.server_av_flag_integrity = True
338 if self.get_server_av_flag(AV_FLAG_TARGET_SPN_UNTRUSTED):
339 self.server_av_flag_target_spn_untrusted = True
340 elif attribute == AV_TIMESTAMP:
341 self.server_av_timestamp = format_ad_timestamp(unpack('<Q', value)[0])
342 elif attribute == AV_SINGLE_HOST_DATA:
343 self.server_av_single_host_data = value
344 elif attribute == AV_TARGET_NAME:
345 self.server_av_target_name = value.decode('utf-16-le') # always unicode
346 elif attribute == AV_CHANNEL_BINDINGS:
347 self.server_av_channel_bindings = value
348 else:
349 raise ValueError('unknown AV type')
350
351 def create_authenticate_message(self):
352 """
353 Microsoft MS-NLMP 2.2.1.3
354 """
355 # 3.1.5.2
356 if not self.client_config_flags and not self.negotiated_flags:
357 return False
358
359 # 3.1.5.2
360 if self.get_client_flag(FLAG_NEGOTIATE_128) and not self.get_negotiated_flag(FLAG_NEGOTIATE_128):
361 return False
362
363 # 3.1.5.2
364 if (not self.server_av_netbios_computer_name or not self.server_av_netbios_domain_name) and self.server_av_flag_integrity:
365 return False
366
367 message = NTLM_SIGNATURE # 8 bytes
368 message += pack('<I', NTLM_MESSAGE_TYPE_NTLM_AUTHENTICATE) # 4 bytes
369 pos = 88 # payload starts at 88
370 # 3.1.5.2
371 if self.server_target_info:
372 lm_challenge_response = b''
373 else:
374 # computed LmChallengeResponse - todo
375 lm_challenge_response = b''
376
377 message += self.pack_field(lm_challenge_response, pos) # LmChallengeResponseField field # 8 bytes
378 pos += len(lm_challenge_response)
379 nt_challenge_response = self.compute_nt_response()
380 message += self.pack_field(nt_challenge_response, pos) # NtChallengeResponseField field # 8 bytes
381 pos += len(nt_challenge_response)
382 domain_name = self.user_domain.encode(self.current_encoding)
383 message += self.pack_field(domain_name, pos) # DomainNameField field # 8 bytes
384 pos += len(domain_name)
385 user_name = self.user_name.encode(self.current_encoding)
386 message += self.pack_field(user_name, pos) # UserNameField field # 8 bytes
387 pos += len(user_name)
388 if self.get_negotiated_flag(FLAG_NEGOTIATE_OEM_WORKSTATION_SUPPLIED) or self.get_negotiated_flag(
389 FLAG_NEGOTIATE_VERSION):
390 workstation = gethostname().encode(self.current_encoding)
391 else:
392 workstation = b''
393 message += self.pack_field(workstation, pos) # empty WorkstationField field # 8 bytes
394 pos += len(workstation)
395 encrypted_random_session_key = b''
396 message += self.pack_field(encrypted_random_session_key, pos) # EncryptedRandomSessionKeyField field # 8 bytes
397 pos += len(encrypted_random_session_key)
398 message += pack('<I', self.negotiated_flags) # negotiated flags - 4 bytes
399 if self.get_negotiated_flag(FLAG_NEGOTIATE_VERSION):
400 message += pack_windows_version(True) # windows version - 8 bytes
401 else:
402 message += pack_windows_version() # empty windows version - 8 bytes
403 message += pack('<Q', 0) # mic
404 message += pack('<Q', 0) # mic - total of 16 bytes
405 # payload starts at 88
406 message += lm_challenge_response
407 message += nt_challenge_response
408 message += domain_name
409 message += user_name
410 message += workstation
411 message += encrypted_random_session_key
412
413 return message
414
415 @staticmethod
416 def pack_field(value, offset):
417 return pack('<HHI', len(value), len(value), offset)
418
419 @staticmethod
420 def unpack_field(field_message):
421 if len(field_message) != 8:
422 raise ValueError('ntlm field must be 8 bytes long')
423 return unpack('<H', field_message[0:2])[0], \
424 unpack('<H', field_message[2:4])[0], \
425 unpack('<I', field_message[4:8])[0]
426
427 @staticmethod
428 def unpack_av_info(info):
429 if info:
430 avs = list()
431 done = False
432 pos = 0
433 while not done:
434 av_type = unpack('<H', info[pos: pos + 2])[0]
435 if av_type not in AV_TYPES:
436 raise ValueError('unknown AV type')
437 av_len = unpack('<H', info[pos + 2: pos + 4])[0]
438 av_value = info[pos + 4: pos + 4 + av_len]
439 pos += av_len + 4
440 if av_type == AV_END_OF_LIST:
441 done = True
442 else:
443 avs.append((av_type, av_value))
444 else:
445 return list()
446
447 return avs
448
449 @staticmethod
450 def pack_av_info(avs):
451 # avs is a list of tuples, each tuple is made of av_type and av_value
452 info = b''
453 for av_type, av_value in avs:
454 if av_type(0) == AV_END_OF_LIST:
455 continue
456 info += pack('<H', av_type)
457 info += pack('<H', len(av_value))
458 info += av_value
459
460 # add AV_END_OF_LIST
461 info += pack('<H', AV_END_OF_LIST)
462 info += pack('<H', 0)
463
464 return info
465
466 @staticmethod
467 def pack_windows_timestamp():
468 return pack('<Q', (int(time()) + 11644473600) * 10000000)
469
470 def compute_nt_response(self):
471 if not self.user_name and not self._password: # anonymous authentication
472 return b''
473
474 self.client_challenge = urandom(8)
475 temp = b''
476 temp += pack('<B', 1) # ResponseVersion - 1 byte
477 temp += pack('<B', 1) # HiResponseVersion - 1 byte
478 temp += pack('<H', 0) # Z(2)
479 temp += pack('<I', 0) # Z(4) - total Z(6)
480 temp += self.pack_windows_timestamp() # time - 8 bytes
481 temp += self.client_challenge # random client challenge - 8 bytes
482 temp += pack('<I', 0) # Z(4)
483 temp += self.server_target_info_raw
484 temp += pack('<I', 0) # Z(4)
485 response_key_nt = self.ntowf_v2()
486 nt_proof_str = hmac.new(response_key_nt, self.server_challenge + temp, digestmod=hashlib.md5).digest()
487 nt_challenge_response = nt_proof_str + temp
488 return nt_challenge_response
489
490 def ntowf_v2(self):
491 passparts = self._password.split(':')
492 if len(passparts) == 2 and len(passparts[0]) == 32 and len(passparts[1]) == 32:
493 # The specified password is an LM:NTLM hash
494 password_digest = binascii.unhexlify(passparts[1])
495 else:
496 try:
497 password_digest = hashlib.new('MD4', self._password.encode('utf-16-le')).digest()
498 except ValueError as e:
499 try:
500 from Crypto.Hash import MD4 # try with the Crypto library if present
501 password_digest = MD4.new(self._password.encode('utf-16-le')).digest()
502 except ImportError:
503 raise e # raise original exception
504
505 return hmac.new(password_digest, (self.user_name.upper() + self.user_domain).encode('utf-16-le'), digestmod=hashlib.md5).digest()
506 