1# Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com> 2# MIT License (see LICENSE or https://opensource.org/licenses/MIT) 3 4import binascii 5import hashlib 6import hmac 7import struct 8 9import ntlm_auth.compute_keys as compkeys 10 11from ntlm_auth.constants import NegotiateFlags, SignSealConstants 12from ntlm_auth.rc4 import ARC4 13 14 15class _NtlmMessageSignature1(object): 16 EXPECTED_BODY_LENGTH = 16 17 18 def __init__(self, random_pad, checksum, seq_num): 19 """ 20 [MS-NLMP] v28.0 2016-07-14 21 22 2.2.2.9.1 NTLMSSP_MESSAGE_SIGNATURE 23 This version of the NTLMSSP_MESSAGE_SIGNATURE structure MUST be used 24 when the NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is not 25 negotiated. 26 27 :param random_pad: A 4-byte array that contains the random pad for the 28 message 29 :param checksum: A 4-byte array that contains the checksum for the 30 message 31 :param seq_num: A 32-bit unsigned integer that contains the NTLM 32 sequence number for this application message 33 """ 34 self.version = b"\x01\x00\x00\x00" 35 self.random_pad = random_pad 36 self.checksum = checksum 37 self.seq_num = seq_num 38 39 def get_data(self): 40 signature = self.version 41 signature += self.random_pad 42 signature += self.checksum 43 signature += self.seq_num 44 45 assert self.EXPECTED_BODY_LENGTH == len(signature), \ 46 "BODY_LENGTH: %d != signature: %d" \ 47 % (self.EXPECTED_BODY_LENGTH, len(signature)) 48 49 return signature 50 51 52class _NtlmMessageSignature2(object): 53 EXPECTED_BODY_LENGTH = 16 54 55 def __init__(self, checksum, seq_num): 56 """ 57 [MS-NLMP] v28.0 2016-07-14 58 59 2.2.2.9.2 NTLMSSP_MESSAGE_SIGNATURE for Extended Session Security 60 This version of the NTLMSSP_MESSAGE_SIGNATURE structure MUST be used 61 when the NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is negotiated 62 63 :param checksum: An 8-byte array that contains the checksum for the 64 message 65 :param seq_num: A 32-bit unsigned integer that contains the NTLM 66 sequence number for this application message 67 """ 68 self.version = b"\x01\x00\x00\x00" 69 self.checksum = checksum 70 self.seq_num = seq_num 71 72 def get_data(self): 73 signature = self.version 74 signature += self.checksum 75 signature += self.seq_num 76 77 assert self.EXPECTED_BODY_LENGTH == len(signature),\ 78 "BODY_LENGTH: %d != signature: %d"\ 79 % (self.EXPECTED_BODY_LENGTH, len(signature)) 80 81 return signature 82 83 84class SessionSecurity(object): 85 86 def __init__(self, negotiate_flags, exported_session_key, source="client"): 87 """ 88 Initialises a security session context that can be used by libraries 89 that call ntlm-auth to sign and seal messages send to the server as 90 well as verify and unseal messages that have been received from the 91 server. This is similar to the GSS_Wrap functions specified in the 92 MS-NLMP document which does the same task. 93 94 :param negotiate_flags: The negotiate flag structure that has been 95 negotiated with the server 96 :param exported_session_key: A 128-bit session key used to derive 97 signing and sealing keys 98 :param source: The source of the message, only used in test scenarios 99 when testing out a server sealing and unsealing 100 """ 101 self.negotiate_flags = negotiate_flags 102 self.exported_session_key = exported_session_key 103 self.outgoing_seq_num = 0 104 self.incoming_seq_num = 0 105 self._source = source 106 self._client_sealing_key = compkeys.get_seal_key(self.negotiate_flags, exported_session_key, 107 SignSealConstants.CLIENT_SEALING) 108 self._server_sealing_key = compkeys.get_seal_key(self.negotiate_flags, exported_session_key, 109 SignSealConstants.SERVER_SEALING) 110 111 self.outgoing_handle = None 112 self.incoming_handle = None 113 self.reset_rc4_state(True) 114 self.reset_rc4_state(False) 115 116 if source == "client": 117 self.outgoing_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.CLIENT_SIGNING) 118 self.incoming_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.SERVER_SIGNING) 119 elif source == "server": 120 self.outgoing_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.SERVER_SIGNING) 121 self.incoming_signing_key = compkeys.get_sign_key(exported_session_key, SignSealConstants.CLIENT_SIGNING) 122 else: 123 raise ValueError("Invalid source parameter %s, must be client " 124 "or server" % source) 125 126 def reset_rc4_state(self, outgoing=True): 127 csk = self._client_sealing_key 128 ssk = self._server_sealing_key 129 if outgoing: 130 self.outgoing_handle = ARC4(csk if self._source == 'client' else ssk) 131 else: 132 self.incoming_handle = ARC4(ssk if self._source == 'client' else csk) 133 134 def wrap(self, message): 135 """ 136 [MS-NLMP] v28.0 2016-07-14 137 138 3.4.6 GSS_WrapEx() 139 Emulates the GSS_Wrap() implementation to sign and seal messages if the 140 correct flags are set. 141 142 :param message: The message data that will be wrapped 143 :return message: The message that has been sealed if flags are set 144 :return signature: The signature of the message, None if flags are not 145 set 146 """ 147 if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL: 148 encrypted_message = self._seal_message(message) 149 signature = self.get_signature(message) 150 message = encrypted_message 151 152 elif self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN: 153 signature = self.get_signature(message) 154 else: 155 signature = None 156 157 return message, signature 158 159 def unwrap(self, message, signature): 160 """ 161 [MS-NLMP] v28.0 2016-07-14 162 163 3.4.7 GSS_UnwrapEx() 164 Emulates the GSS_Unwrap() implementation to unseal messages and verify 165 the signature sent matches what has been computed locally. Will throw 166 an Exception if the signature doesn't match 167 168 :param message: The message data received from the server 169 :param signature: The signature of the message 170 :return message: The message that has been unsealed if flags are set 171 """ 172 if self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SEAL: 173 message = self._unseal_message(message) 174 self.verify_signature(message, signature) 175 176 elif self.negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_SIGN: 177 self.verify_signature(message, signature) 178 179 return message 180 181 def _seal_message(self, message): 182 """ 183 [MS-NLMP] v28.0 2016-07-14 184 185 3.4.3 Message Confidentiality 186 Will generate an encrypted message using RC4 based on the 187 ClientSealingKey 188 189 :param message: The message to be sealed (encrypted) 190 :return encrypted_message: The encrypted message 191 """ 192 encrypted_message = self.outgoing_handle.update(message) 193 return encrypted_message 194 195 def _unseal_message(self, message): 196 """ 197 [MS-NLMP] v28.0 2016-07-14 198 199 3.4.3 Message Confidentiality 200 Will generate a dencrypted message using RC4 based on the 201 ServerSealingKey 202 203 :param message: The message to be unsealed (dencrypted) 204 :return decrypted_message: The decrypted message 205 """ 206 decrypted_message = self.incoming_handle.update(message) 207 return decrypted_message 208 209 def get_signature(self, message): 210 """ 211 [MS-NLMP] v28.0 2016-07-14 212 213 3.4.4 Message Signature Functions 214 Will create the signature based on the message to send to the server. 215 Depending on the negotiate_flags set this could either be an NTLMv1 216 signature or NTLMv2 with Extended Session Security signature. 217 218 :param message: The message data that will be signed 219 :return signature: Either _NtlmMessageSignature1 or 220 _NtlmMessageSignature2 depending on the flags set 221 """ 222 signature = calc_signature(message, self.negotiate_flags, 223 self.outgoing_signing_key, 224 self.outgoing_seq_num, self.outgoing_handle) 225 self.outgoing_seq_num += 1 226 227 return signature.get_data() 228 229 def verify_signature(self, message, signature): 230 """ 231 Will verify that the signature received from the server matches up with 232 the expected signature computed locally. Will throw an exception if 233 they do not match 234 235 :param message: The message data that is received from the server 236 :param signature: The signature of the message received from the server 237 """ 238 if self.negotiate_flags & \ 239 NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY: 240 actual_checksum = signature[4:12] 241 actual_seq_num = struct.unpack("<I", signature[12:16])[0] 242 else: 243 actual_checksum = signature[8:12] 244 actual_seq_num = struct.unpack("<I", signature[12:16])[0] 245 246 expected_signature = calc_signature(message, self.negotiate_flags, 247 self.incoming_signing_key, 248 self.incoming_seq_num, 249 self.incoming_handle) 250 expected_checksum = expected_signature.checksum 251 expected_seq_num = struct.unpack("<I", expected_signature.seq_num)[0] 252 253 if actual_checksum != expected_checksum: 254 raise Exception("The signature checksum does not match, message " 255 "has been altered") 256 257 if actual_seq_num != expected_seq_num: 258 raise Exception("The signature sequence number does not match up, " 259 "message not received in the correct sequence") 260 261 self.incoming_seq_num += 1 262 263 264def calc_signature(message, negotiate_flags, signing_key, seq_num, handle): 265 seq_num = struct.pack("<I", seq_num) 266 if negotiate_flags & \ 267 NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY: 268 checksum_hmac = hmac.new(signing_key, seq_num + message, 269 digestmod=hashlib.md5) 270 if negotiate_flags & NegotiateFlags.NTLMSSP_NEGOTIATE_KEY_EXCH: 271 checksum = handle.update(checksum_hmac.digest()[:8]) 272 else: 273 checksum = checksum_hmac.digest()[:8] 274 275 signature = _NtlmMessageSignature2(checksum, seq_num) 276 277 else: 278 message_crc = binascii.crc32(message) % (1 << 32) 279 checksum = struct.pack("<I", message_crc) 280 random_pad = handle.update(struct.pack("<I", 0)) 281 checksum = handle.update(checksum) 282 seq_num = handle.update(seq_num) 283 random_pad = struct.pack("<I", 0) 284 285 signature = _NtlmMessageSignature1(random_pad, checksum, seq_num) 286 287 return signature 288