Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/ntlm_auth/messages.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

232 statements  

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)