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