1# Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com> 2# MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 4import hashlib 5import hmac 6import os 7import struct 8 9from ntlm_auth.compute_response import ComputeResponse 10from ntlm_auth.constants import AvId, AvFlags, MessageTypes, NegotiateFlags, \ 11 NTLM_SIGNATURE 12from ntlm_auth.rc4 import ARC4 13 14try: 15 from collections import OrderedDict 16except ImportError: # pragma: no cover 17 from ordereddict import OrderedDict 18 19 20class TargetInfo(object): 21 22 def __init__(self): 23 self.fields = OrderedDict() 24 25 def __setitem__(self, key, value): 26 self.fields[key] = value 27 28 def __getitem__(self, key): 29 return self.fields.get(key, None) 30 31 def __delitem__(self, key): 32 del self.fields[key] 33 34 def pack(self): 35 if AvId.MSV_AV_EOL in self.fields: 36 del self[AvId.MSV_AV_EOL] 37 38 data = b'' 39 for attribute_type, attribute_value in self.fields.items(): 40 data += struct.pack("<HH", attribute_type, len(attribute_value)) 41 data += attribute_value 42 43 # end with a NTLMSSP_AV_EOL 44 data += struct.pack('<HH', AvId.MSV_AV_EOL, 0) 45 return data 46 47 def unpack(self, data): 48 attribute_type = None 49 while attribute_type != AvId.MSV_AV_EOL: 50 attribute_type = struct.unpack("<H", data[:2])[0] 51 attribute_length = struct.unpack("<H", data[2:4])[0] 52 self[attribute_type] = data[4:attribute_length + 4] 53 data = data[4 + attribute_length:] 54 55 56class NegotiateMessage(object): 57 EXPECTED_BODY_LENGTH = 40 58 59 def __init__(self, negotiate_flags, domain_name, workstation): 60 """ 61 [MS-NLMP] v28.0 2016-07-14 62 63 2.2.1.1 NEGOTIATE_MESSAGE 64 The NEGOTIATE_MESSAGE defines an NTLM Negotiate message that is sent 65 from the client to the server. This message allows the client to 66 specify its supported NTLM options to the server. 67 68 :param negotiate_flags: A NEGOTIATE structure that contains a set of 69 bit flags. These flags are the options the client supports 70 :param domain_name: The domain name of the user to authenticate with, 71 default is None 72 :param workstation: The worksation of the client machine, default is 73 None 74 75 Attributes: 76 signature: An 8-byte character array that MUST contain the ASCII 77 string 'NTLMSSP\0' 78 message_type: A 32-bit unsigned integer that indicates the message 79 type. This field must be set to 0x00000001 80 negotiate_flags: A NEGOTIATE structure that contains a set of bit 81 flags. These flags are the options the client supports 82 version: Contains the windows version info of the client. It is 83 used only debugging purposes and are only set when 84 NTLMSSP_NEGOTIATE_VERSION flag is set 85 domain_name: A byte-array that contains the name of the client 86 authentication domain that MUST Be encoded in the negotiated 87 character set 88 workstation: A byte-array that contains the name of the client 89 machine that MUST Be encoded in the negotiated character set 90 """ 91 92 self.signature = NTLM_SIGNATURE 93 self.message_type = struct.pack('<L', MessageTypes.NTLM_NEGOTIATE) 94 95 # Check if the domain_name value is set, if it is, make sure the 96 # negotiate_flag is also set 97 if domain_name is None: 98 self.domain_name = '' 99 else: 100 self.domain_name = domain_name 101 negotiate_flags |= \ 102 NegotiateFlags.NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED 103 104 # Check if the workstation value is set, if it is, make sure the 105 # negotiate_flag is also set 106 if workstation is None: 107 self.workstation = '' 108 else: 109 self.workstation = workstation 110 negotiate_flags |= \ 111 NegotiateFlags.NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED 112 113 # Set the encoding flag to use UNICODE, remove OEM if set. 114 negotiate_flags |= NegotiateFlags.NTLMSSP_NEGOTIATE_UNICODE 115 negotiate_flags &= ~NegotiateFlags.NTLMSSP_NEGOTIATE_OEM 116 117 # The domain name and workstation are always OEM encoded. 118 self.domain_name = self.domain_name.encode('ascii') 119 self.workstation = self.workstation.encode('ascii') 120 121 self.version = get_version(negotiate_flags) 122 123 self.negotiate_flags = struct.pack('<I', negotiate_flags) 124 125 def get_data(self): 126 payload_offset = self.EXPECTED_BODY_LENGTH 127 128 # DomainNameFields - 8 bytes 129 domain_name_len = struct.pack('<H', len(self.domain_name)) 130 domain_name_max_len = struct.pack('<H', len(self.domain_name)) 131 domain_name_buffer_offset = struct.pack('<I', payload_offset) 132 payload_offset += len(self.domain_name) 133 134 # WorkstationFields - 8 bytes 135 workstation_len = struct.pack('<H', len(self.workstation)) 136 workstation_max_len = struct.pack('<H', len(self.workstation)) 137 workstation_buffer_offset = struct.pack('<I', payload_offset) 138 payload_offset += len(self.workstation) 139 140 # Payload - variable length 141 payload = self.domain_name 142 payload += self.workstation 143 144 # Bring the header values together into 1 message 145 msg1 = self.signature 146 msg1 += self.message_type 147 msg1 += self.negotiate_flags 148 msg1 += domain_name_len 149 msg1 += domain_name_max_len 150 msg1 += domain_name_buffer_offset 151 msg1 += workstation_len 152 msg1 += workstation_max_len 153 msg1 += workstation_buffer_offset 154 msg1 += self.version 155 156 assert self.EXPECTED_BODY_LENGTH == len(msg1),\ 157 "BODY_LENGTH: %d != msg1: %d"\ 158 % (self.EXPECTED_BODY_LENGTH, len(msg1)) 159 160 # Adding the payload data to the message 161 msg1 += payload 162 return msg1 163 164 165class ChallengeMessage(object): 166 167 def __init__(self, msg2): 168 """ 169 [MS-NLMP] v28.0 2016-07-14 170 171 2.2.1.2 CHALLENGE_MESSAGE 172 The CHALLENGE_MESSAGE defines an NTLM challenge message that is sent 173 from the server to the client. The CHALLENGE_MESSAGE is used by the 174 server to challenge the client to prove its identity, For 175 connection-oriented requests, the CHALLENGE_MESSAGE generated by the 176 server is in response to the NEGOTIATE_MESSAGE from the client. 177 178 :param msg2: The CHALLENGE_MESSAGE received from the server after 179 sending our NEGOTIATE_MESSAGE. This has been decoded from a base64 180 string 181 182 Attributes 183 signature: An 8-byte character array that MUST contain the ASCII 184 string 'NTLMSSP\0' 185 message_type: A 32-bit unsigned integer that indicates the message 186 type. This field must be set to 0x00000002 187 negotiate_flags: A NEGOTIATE strucutre that contains a set of bit 188 flags. The server sets flags to indicate options it supports 189 server_challenge: A 64-bit value that contains the NTLM challenge. 190 The challenge is a 64-bit nonce. Used in the 191 AuthenticateMessage message 192 reserved: An 8-byte array whose elements MUST be zero when sent and 193 MUST be ignored on receipt 194 version: When NTLMSSP_NEGOTIATE_VERSION flag is set in 195 negotiate_flags field which contains the windows version info. 196 Used only for debugging purposes 197 target_name: When NTLMSSP_REQUEST_TARGET is set is a byte array 198 that contains the name of the server authentication realm. In a 199 domain environment this is the domain name not server name 200 target_info: When NTLMSSP_NEGOTIATE_TARGET_INFO is set is a byte 201 array that contains a sequence of AV_PAIR structures 202 """ 203 204 self.data = msg2 205 # Setting the object values from the raw_challenge_message 206 self.signature = msg2[0:8] 207 self.message_type = struct.unpack("<I", msg2[8:12])[0] 208 self.negotiate_flags = struct.unpack("<I", msg2[20:24])[0] 209 self.server_challenge = msg2[24:32] 210 self.reserved = msg2[32:40] 211 212 if self.negotiate_flags & \ 213 NegotiateFlags.NTLMSSP_NEGOTIATE_VERSION and \ 214 self.negotiate_flags & \ 215 NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY and \ 216 len(msg2) > 48: 217 self.version = struct.unpack("<q", msg2[48:56])[0] 218 else: 219 self.version = None 220 221 if self.negotiate_flags & NegotiateFlags.NTLMSSP_REQUEST_TARGET: 222 target_name_len = struct.unpack("<H", msg2[12:14])[0] 223 target_name_max_len = struct.unpack("<H", msg2[14:16])[0] 224 target_name_offset_mix = struct.unpack("<I", msg2[16:20])[0] 225 target_offset_max = target_name_offset_mix + target_name_len 226 self.target_name = msg2[target_name_offset_mix:target_offset_max] 227 else: 228 self.target_name = None 229 230 if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_TARGET_INFO: 231 target_info_len = struct.unpack("<H", msg2[40:42])[0] 232 target_info_max_len = struct.unpack("<H", msg2[42:44])[0] 233 target_info_offset_min = struct.unpack("<I", msg2[44:48])[0] 234 target_info_offset_max = target_info_offset_min + target_info_len 235 236 target_info_raw = \ 237 msg2[target_info_offset_min:target_info_offset_max] 238 self.target_info = TargetInfo() 239 self.target_info.unpack(target_info_raw) 240 else: 241 self.target_info = None 242 243 # Verify initial integrity of the message, it matches what should be 244 # there 245 assert self.signature == NTLM_SIGNATURE 246 assert self.message_type == MessageTypes.NTLM_CHALLENGE 247 248 def get_data(self): 249 return self.data 250 251 252class AuthenticateMessage(object): 253 EXPECTED_BODY_LENGTH = 72 254 EXPECTED_BODY_LENGTH_WITH_MIC = 88 255 256 def __init__(self, user_name, password, domain_name, workstation, 257 challenge_message, ntlm_compatibility, 258 server_certificate_hash=None, cbt_data=None): 259 """ 260 [MS-NLMP] v28.0 2016-07-14 261 262 2.2.1.3 AUTHENTICATE_MESSAGE 263 The AUTHENTICATE_MESSAGE defines an NTLM authenticate message that is 264 sent from the client to the server after the CHALLENGE_MESSAGE is 265 processed by the client. 266 267 :param user_name: The user name of the user we are trying to 268 authenticate with 269 :param password: The password of the user we are trying to authenticate 270 with 271 :param domain_name: The domain name of the user account we are 272 authenticated with, default is None 273 :param workstation: The workstation we are using to authenticate with, 274 default is None 275 :param challenge_message: A ChallengeMessage object that was received 276 from the server after the negotiate_message 277 :param ntlm_compatibility: The Lan Manager Compatibility Level, used to 278 determine what NTLM auth version to use, see Ntlm in ntlm.py for 279 more details 280 :param server_certificate_hash: Deprecated, used cbt_data instead 281 :param cbt_data: The GssChannelBindingsStruct that contains the CBT 282 data to bind in the auth response 283 284 Message Attributes (Attributes used to compute the message structure): 285 signature: An 8-byte character array that MUST contain the ASCII 286 string 'NTLMSSP\0' 287 message_type: A 32-bit unsigned integer that indicates the message 288 type. This field must be set to 0x00000003 289 negotiate_flags: A NEGOTIATE strucutre that contains a set of bit 290 flags. These flags are the choices the client has made from the 291 CHALLENGE_MESSAGE options 292 version: Contains the windows version info of the client. It is 293 used only debugging purposes and are only set when 294 NTLMSSP_NEGOTIATE_VERSION flag is set 295 mic: The message integrity for the NEGOTIATE_MESSAGE, 296 CHALLENGE_MESSAGE and AUTHENTICATE_MESSAGE 297 lm_challenge_response: An LM_RESPONSE of LMv2_RESPONSE structure 298 that contains the computed LM response to the challenge 299 nt_challenge_response: An NTLM_RESPONSE or NTLMv2_RESPONSE 300 structure that contains the computed NT response to the 301 challenge 302 domain_name: The domain or computer name hosting the user account, 303 MUST be encoded in the negotiated character set 304 user_name: The name of the user to be authenticated, MUST be 305 encoded in the negotiated character set 306 workstation: The name of the computer to which the user is logged 307 on, MUST Be encoded in the negotiated character set 308 encrypted_random_session_key: The client's encrypted random session 309 key 310 311 Non-Message Attributes (Attributes not used in auth message): 312 exported_session_key: A randomly generated session key based on 313 other keys, used to derive the SIGNKEY and SEALKEY 314 target_info: The AV_PAIR structure used in the nt response 315 calculation 316 """ 317 self.signature = NTLM_SIGNATURE 318 self.message_type = struct.pack('<L', MessageTypes.NTLM_AUTHENTICATE) 319 self.negotiate_flags = challenge_message.negotiate_flags 320 self.version = get_version(self.negotiate_flags) 321 self.mic = None 322 323 if domain_name is None: 324 self.domain_name = '' 325 else: 326 self.domain_name = domain_name 327 328 if workstation is None: 329 self.workstation = '' 330 else: 331 self.workstation = workstation 332 333 if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_UNICODE: 334 self.negotiate_flags &= ~NegotiateFlags.NTLMSSP_NEGOTIATE_OEM 335 encoding_value = 'utf-16-le' 336 else: 337 encoding_value = 'ascii' 338 339 self.domain_name = self.domain_name.encode(encoding_value) 340 self.user_name = user_name.encode(encoding_value) 341 self.workstation = self.workstation.encode(encoding_value) 342 343 compute_response = ComputeResponse(user_name, password, domain_name, 344 challenge_message, 345 ntlm_compatibility) 346 347 self.lm_challenge_response = \ 348 compute_response.get_lm_challenge_response() 349 self.nt_challenge_response, key_exchange_key, target_info = \ 350 compute_response.get_nt_challenge_response( 351 self.lm_challenge_response, server_certificate_hash, cbt_data) 352 self.target_info = target_info 353 354 if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_KEY_EXCH: 355 self.exported_session_key = get_random_export_session_key() 356 357 rc4_handle = ARC4(key_exchange_key) 358 self.encrypted_random_session_key = \ 359 rc4_handle.update(self.exported_session_key) 360 else: 361 self.exported_session_key = key_exchange_key 362 self.encrypted_random_session_key = b'' 363 364 self.negotiate_flags = struct.pack('<I', self.negotiate_flags) 365 366 def get_data(self): 367 if self.mic is None: 368 mic = b'' 369 expected_body_length = self.EXPECTED_BODY_LENGTH 370 else: 371 mic = self.mic 372 expected_body_length = self.EXPECTED_BODY_LENGTH_WITH_MIC 373 374 payload_offset = expected_body_length 375 376 # DomainNameFields - 8 bytes 377 domain_name_len = struct.pack('<H', len(self.domain_name)) 378 domain_name_max_len = struct.pack('<H', len(self.domain_name)) 379 domain_name_buffer_offset = struct.pack('<I', payload_offset) 380 payload_offset += len(self.domain_name) 381 382 # UserNameFields - 8 bytes 383 user_name_len = struct.pack('<H', len(self.user_name)) 384 user_name_max_len = struct.pack('<H', len(self.user_name)) 385 user_name_buffer_offset = struct.pack('<I', payload_offset) 386 payload_offset += len(self.user_name) 387 388 # WorkstatonFields - 8 bytes 389 workstation_len = struct.pack('<H', len(self.workstation)) 390 workstation_max_len = struct.pack('<H', len(self.workstation)) 391 workstation_buffer_offset = struct.pack('<I', payload_offset) 392 payload_offset += len(self.workstation) 393 394 # LmChallengeResponseFields - 8 bytes 395 lm_challenge_response_len = \ 396 struct.pack('<H', len(self.lm_challenge_response)) 397 lm_challenge_response_max_len = \ 398 struct.pack('<H', len(self.lm_challenge_response)) 399 lm_challenge_response_buffer_offset = struct.pack('<I', payload_offset) 400 payload_offset += len(self.lm_challenge_response) 401 402 # NtChallengeResponseFields - 8 bytes 403 nt_challenge_response_len = \ 404 struct.pack('<H', len(self.nt_challenge_response)) 405 nt_challenge_response_max_len = \ 406 struct.pack('<H', len(self.nt_challenge_response)) 407 nt_challenge_response_buffer_offset = struct.pack('<I', payload_offset) 408 payload_offset += len(self.nt_challenge_response) 409 410 # EncryptedRandomSessionKeyFields - 8 bytes 411 encrypted_random_session_key_len = \ 412 struct.pack('<H', len(self.encrypted_random_session_key)) 413 encrypted_random_session_key_max_len = \ 414 struct.pack('<H', len(self.encrypted_random_session_key)) 415 encrypted_random_session_key_buffer_offset = \ 416 struct.pack('<I', payload_offset) 417 payload_offset += len(self.encrypted_random_session_key) 418 419 # Payload - variable length 420 payload = self.domain_name 421 payload += self.user_name 422 payload += self.workstation 423 payload += self.lm_challenge_response 424 payload += self.nt_challenge_response 425 payload += self.encrypted_random_session_key 426 427 msg3 = self.signature 428 msg3 += self.message_type 429 msg3 += lm_challenge_response_len 430 msg3 += lm_challenge_response_max_len 431 msg3 += lm_challenge_response_buffer_offset 432 msg3 += nt_challenge_response_len 433 msg3 += nt_challenge_response_max_len 434 msg3 += nt_challenge_response_buffer_offset 435 msg3 += domain_name_len 436 msg3 += domain_name_max_len 437 msg3 += domain_name_buffer_offset 438 msg3 += user_name_len 439 msg3 += user_name_max_len 440 msg3 += user_name_buffer_offset 441 msg3 += workstation_len 442 msg3 += workstation_max_len 443 msg3 += workstation_buffer_offset 444 msg3 += encrypted_random_session_key_len 445 msg3 += encrypted_random_session_key_max_len 446 msg3 += encrypted_random_session_key_buffer_offset 447 msg3 += self.negotiate_flags 448 msg3 += self.version 449 msg3 += mic 450 451 # Adding the payload data to the message 452 msg3 += payload 453 454 return msg3 455 456 def add_mic(self, negotiate_message, challenge_message): 457 if self.target_info is not None: 458 av_flags = self.target_info[AvId.MSV_AV_FLAGS] 459 460 if av_flags is not None and av_flags == \ 461 struct.pack("<L", AvFlags.MIC_PROVIDED): 462 self.mic = struct.pack("<IIII", 0, 0, 0, 0) 463 negotiate_data = negotiate_message.get_data() 464 challenge_data = challenge_message.get_data() 465 authenticate_data = self.get_data() 466 467 hmac_data = negotiate_data + challenge_data + authenticate_data 468 mic = hmac.new(self.exported_session_key, hmac_data, 469 digestmod=hashlib.md5).digest() 470 self.mic = mic 471 472 473def get_version(negotiate_flags): 474 # Check the negotiate_flag version is set, if it is make sure the version 475 # info is added to the data 476 if negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_VERSION: 477 # TODO: Get the major and minor version of Windows instead of using 478 # default values 479 product_major_version = struct.pack('<B', 6) 480 product_minor_version = struct.pack('<B', 1) 481 product_build = struct.pack('<H', 7601) 482 version_reserved = b'\x00' * 3 483 ntlm_revision_current = struct.pack('<B', 15) 484 version = product_major_version 485 version += product_minor_version 486 version += product_build 487 version += version_reserved 488 version += ntlm_revision_current 489 else: 490 version = b'\x00' * 8 491 492 return version 493 494 495def get_random_export_session_key(): 496 return os.urandom(16) 497