class NTLMContext(AuthContext): _AUTH_PROVIDERS = { 'ntlm': '' } def __init__(self, username, password, cbt_app_data): if username is None: raise ValueError("Cannot use ntlm-auth with no username set") if password is None: raise ValueError("Cannot use ntlm-auth with no password set") super(NTLMContext, self).__init__(password, "ntlm", cbt_app_data) self._domain, self._username = self._get_domain_username(username) @property def domain(self): return self._domain @property def username(self): return self._username @property def complete(self): return self._context.complete def init_context(self): cbt_struct = None if self.cbt_app_data: cbt_struct = GssChannelBindingsStruct() cbt_struct[cbt_struct.APPLICATION_DATA] = self.cbt_app_data self._context = NtlmContext(self.username, self.password, self.domain, cbt_data=cbt_struct) def step(self): msg1 = self._context.step() log.debug("NTLM Negotiate message: %s" % binascii.hexlify(msg1)) msg2 = yield msg1 log.debug("NTLM: Parsing Challenge message and generating " "Authenticate message: %s" % binascii.hexlify(msg2)) msg3 = self._context.step(msg2) yield msg3 def wrap(self, data): wrapped_data = self._context.wrap(data) return wrapped_data[:16], wrapped_data[16:] def unwrap(self, header, data): return self._context.unwrap(header + data)
def retry_with_ntlm_auth(self, auth_header_field, auth_header, response, auth_type, args): try: cert_hash = self._get_server_cert(response) cbt_data = GssChannelBindingsStruct() cbt_data[cbt_data.APPLICATION_DATA] = b"tls-server-end-point:" + \ base64.b16decode(cert_hash) except Exception: cbt_data = None context = NtlmContext(self.username, self.password, self.domain, cbt_data=cbt_data, ntlm_compatibility=self.ntlm_compatibility) # Consume the original response contents and release the connection for # later response.content response.raw.release_conn() # Create the negotiate request msg1_req = response.request.copy() msg1 = context.step() msg1_header = "%s %s" % (auth_type, base64.b64encode(msg1).decode()) msg1_req.headers[auth_header] = msg1_header # Send the negotiate request and receive the challenge message disable_stream_args = dict(args, stream=False) msg2_resp = response.connection.send(msg1_req, **disable_stream_args) msg2_resp.content msg2_resp.raw.release_conn() # Parse the challenge response in the ntlm_context msg2_header = msg2_resp.headers[auth_header_field] msg2 = msg2_header.replace(auth_type + ' ', '') msg3 = context.step(base64.b64decode(msg2)) # Create the authenticate request msg3_req = msg2_resp.request.copy() msg3_header = auth_type + ' ' + base64.b64encode(msg3).decode() msg3_req.headers[auth_header] = msg3_header # Send the authenticate request final_response = msg2_resp.connection.send(msg3_req, **args) final_response.history.append(response) final_response.history.append(msg2_resp) return final_response
class NtlmContext(object): def __init__(self, username, password): if username is None: raise SMBAuthenticationError("The username must be set when using " "NTLM authentication") if password is None: raise SMBAuthenticationError("The password must be set when using " "NTLM authentication") log.info("Setting up NTLM Security Context for user %s" % username) self.domain, self.username = _split_username_and_domain(username) self.context = Ntlm(self.username, password, domain=self.domain) @property def complete(self): return self.context.complete def step(self): log.info("NTLM: Generating Negotiate message") msg1 = self.context.step() log.debug("NTLM: Negotiate message: %s" % _bytes_to_hex(msg1)) msg2 = yield msg1 log.info( "NTLM: Parsing Challenge message and generating Authentication message" ) log.debug("NTLM: Challenge message: %s" % _bytes_to_hex(msg2)) msg3 = self.context.step(input_token=msg2) yield msg3 def get_session_key(self): # The session_key was only recently added in ntlm-auth, we have the # fallback to the non-public interface for older versions where we # know this still works. This should be removed once ntlm-auth no # longer requires these older versions (>=1.4.0). return getattr(self.context, 'session_key', self.context._session_security.exported_session_key)
class Authentication(object): __slots__ = ('logger', '_ctx', '_method', '_sspi', '_channel_bindings', '_args', '_user', '_domain', '_password', '_kerberos') def __init__(self, logger): self.logger = logger self._ctx = None self._method = None self._sspi = False self._channel_bindings = None self._kerberos = KerberosBackend() def _create_auth1_message_sspi(self, url, method_data, user=None, password=None, domain=None, flags=0, certificate=None): if isinstance(method_data, Method): method = method_data.id else: method = method_data server = None principal = None kwargs = { 'user': user, 'password': password, 'domain': domain, 'gssflags': flags } parsed = urlparse(url) host = parsed.netloc scheme = parsed.scheme.upper() hostname = host.rsplit(':', 1)[0] if scheme.startswith('HTTP'): service = 'HTTP' else: service = scheme need_inquire_cred = False if (self._kerberos.has_sspi_nego_support or self._kerberos.has_gssapi_support) and method in ( METHOD_NEGOTIATE, METHOD_KERBEROS): if certificate: self.set_certificate(certificate) if method == METHOD_NEGOTIATE: mech_oid = self._kerberos.GSS_MECH_OID_SPNEGO else: mech_oid = self._kerberos.GSS_MECH_OID_KRB5 flags |= \ self._kerberos.GSS_C_MUTUAL_FLAG | \ self._kerberos.GSS_C_SEQUENCE_FLAG kwargs.update({'mech_oid': mech_oid, 'gssflags': flags}) server = service + '@' + hostname if self._kerberos.has_gssapi_support: del kwargs['user'] del kwargs['domain'] if user and domain and '@' not in user: principal = user + '@' + domain else: principal = user if not kwargs['password']: need_inquire_cred = True del kwargs['password'] elif self._kerberos.has_sspi_ntlm_support and method == METHOD_NTLM: # This is only for SSPI kwargs['mech_oid'] = self._kerberos.GSS_MECH_OID_NTLM server = hostname else: raise NotImplementedError('No supported auth methods') result = 0 try: result, self._ctx = self._kerberos.authGSSClientInit( server, principal, **kwargs) except TypeError as e: self.logger.error( 'GSSAPI: authGSSClientInit: failed to set password: %s', e) if password: raise NotImplementedError( "ccs-pykerberos doesn't support password auth") del kwargs['password'] result, self._ctx = self._kerberos.authGSSClientInit( server, principal, **kwargs) if result < 0: self.logger.error('GSSAPI: authGSSClientInit Result: %d', result) raise AuthenticationError(result) self.logger.debug( 'GSSAPI: New context for SPN=%s%s (need inquire creds: %s)', server, '' if not principal else 'Principal=' + principal, need_inquire_cred) if need_inquire_cred: result = self._kerberos.authGSSClientInquireCred(self._ctx) if result < 0: raise AuthenticationError(result) self.logger.warning( 'GSSAPI: Using principal: %s', self._kerberos.authGSSClientUserName(self._ctx), ) kwargs = {} if self._channel_bindings: kwargs['channel_bindings'] = self._channel_bindings result = self._kerberos.authGSSClientStep(self._ctx, '', **kwargs) if result < 0: self.logger.error('GSSAPI: authGSSClientInit Result: %d', result) raise AuthenticationError(result) self._method = method self._sspi = True return Result(result == self._kerberos.CONTINUE, self._method, self._kerberos.authGSSClientResponse(self._ctx)) def _create_auth2_message_sspi(self, payload): kwargs = {} if self._channel_bindings: kwargs['channel_bindings'] = self._channel_bindings result = self._kerberos.authGSSClientStep(self._ctx, payload, **kwargs) payload = self._kerberos.authGSSClientResponse(self._ctx) if result < 0: self.logger.error('GSSAPI: authGSSClientStep (2) Step Result: %d', result) raise AuthenticationError(result) elif result == self._kerberos.COMPLETE: if payload is not None: if self._method != METHOD_NTLM: # Known bug for NTLM and winkerberos self.logger.warning( 'GSSAPI: API bug: payload provided but step is not required' ) result = self._kerberos.CONTINUE else: self.logger.info( 'GSSAPI: authGSSClientStep (2) not required for %s', self._method) return Result(result == self._kerberos.CONTINUE, self._method, payload) def set_certificate(self, certificate): # FIXME: Handle case with different hash function somehow.. application_data = b'tls-server-end-point:' + hashlib.sha256( certificate).digest() self._channel_bindings = self._kerberos.channelBindings( application_data=application_data) self.logger.debug('Set channel bindings: %s -> %s', repr(application_data), self._channel_bindings) def _create_auth1_message_ntlm(self, domain, user, pw, certificate): domain = domain or None workstation = None cbt_data = None ntlm_compatibility = 1 if certificate is not None: ntlm_compatibility = 3 cbt_data = GssChannelBindingsStruct() cbt_data[cbt_data.APPLICATION_DATA] = \ b'tls-server-end-point:' + hashlib.sha256( certificate).digest() self._ctx = NtlmContext(user, pw, domain, workstation, cbt_data, ntlm_compatibility) payload = b64encode(self._ctx.step()).decode('ascii') self._method = METHOD_NTLM return Result(True, self._method, payload) def _create_auth2_message_ntlm(self, payload): payload = b64encode(self._ctx.step(b64decode(payload))).decode('ascii') return Result(False, self._method, payload) def _create_auth1_message_basic(self, user, pw): response = b64encode((user + ':' + pw).encode('utf-8')).decode('ascii') self._method = METHOD_BASIC return Result(False, self._method, response) def _create_auth1_message_digest(self, domain, user, pw, args): response = make_digest_response(domain, user, pw, args) self._method = METHOD_DIGEST return Result(False, self._method, response) def create_auth1_message(self, domain, user, pw, url, payloads, certificate=None, secure=True): if __debug__: assert (not domain or isinstance(domain, string_types)) assert (not user or isinstance(user, string_types)) assert (not pw or isinstance(pw, string_types)) assert (not url or isinstance(url, string_types)) assert (not certificate or isinstance(certificate, bytes)) supported_auth_methods, _ = get_supported_methods(payloads, secure) for method in supported_auth_methods: if method.id in METHODS_KERBEROS: try: return self._create_auth1_message_sspi( url, method, user, pw, domain, certificate=certificate) except self._kerberos.GSSError as e: self.logger.info('GSS error: method=%s error=%s (ignore)', method, e) except NotImplementedError: self.logger.debug('Not supported conditions for method %s', method) except AuthenticationError as e: self.logger.info('SSPI error: method=%s error=%s (ignore)', method, e) self._ctx = None elif user is None: continue elif method.id == METHOD_NTLM: self.logger.info( 'Fallback to py NTLM with creds: %s -> user=%s', url, user) return self._create_auth1_message_ntlm(domain, user, pw, certificate) elif method.id == METHOD_DIGEST: self.logger.info( 'Fallback to py Digest with creds: %s -> user=%s', url, user) return self._create_auth1_message_digest( domain, user, pw, method.args) elif method.id == METHOD_BASIC: self.logger.info( 'Fallback to py Basic with creds: %s -> user=%s', url, user) return self._create_auth1_message_basic(user, pw) if user is None: self.logger.info('No credentials found for URL=%s', url) raise AuthenticationError('No acceptable auth found for: URL: %s', url) def create_auth2_message(self, payload): if self._sspi: return self._create_auth2_message_sspi(payload) elif self._method == METHOD_NTLM: return self._create_auth2_message_ntlm(payload) else: raise AuthenticationError( 'Invalid state (expected method {})'.format(self._method))
def test_ntlm_context_with_mic(self, monkeypatch): monkeypatch.setattr('os.urandom', lambda s: b"\xaa" * 8) monkeypatch.setattr('ntlm_auth.messages.get_version', lambda s: b"\x05\x01\x28\x0A\x00\x00\x00\x0F") monkeypatch.setattr('ntlm_auth.messages.get_random_export_session_key', lambda: b"\x55" * 16) monkeypatch.setattr('ntlm_auth.compute_response.get_windows_timestamp', lambda: b"\x00" * 8) ch = 'E3CA49271E5089CC48CE82109F1324F41DBEDDC29A777410C738F7868C4FF405' cbt_data = GssChannelBindingsStruct() cbt_data[cbt_data.APPLICATION_DATA] = b"tls-server-end-point:" + \ base64.b16decode(ch) ntlm_context = NtlmContext("User", "Password", "Domain", "COMPUTER", cbt_data=cbt_data) ntlm_context.reset_rc4_state( ) # Verifies it won't fail when the session security isn't set up. actual_nego = ntlm_context.step() expected_nego = b"\x4e\x54\x4c\x4d\x53\x53\x50\x00" \ b"\x01\x00\x00\x00\x31\xb0\x88\xe2" \ b"\x06\x00\x06\x00\x28\x00\x00\x00" \ b"\x08\x00\x08\x00\x2e\x00\x00\x00" \ b"\x05\x01\x28\x0a\x00\x00\x00\x0f" \ b"\x44\x6f\x6d\x61\x69\x6e\x43\x4f" \ b"\x4d\x50\x55\x54\x45\x52" assert actual_nego == expected_nego assert not ntlm_context.mic_present assert not ntlm_context.complete challenge_msg = b"\x4E\x54\x4C\x4D\x53\x53\x50\x00" \ b"\x02\x00\x00\x00\x00\x00\x00\x00" \ b"\x38\x00\x00\x00\x33\x82\x8A\xE2" \ b"\x01\x23\x45\x67\x89\xAB\xCD\xEF" \ b"\x00\x00\x00\x00\x00\x00\x00\x00" \ b"\x30\x00\x30\x00\x38\x00\x00\x00" \ b"\x06\x01\xB1\x1D\x00\x00\x00\x0F" \ b"\x02\x00\x0C\x00\x44\x00\x6F\x00" \ b"\x6D\x00\x61\x00\x69\x00\x6E\x00" \ b"\x01\x00\x0C\x00\x53\x00\x65\x00" \ b"\x72\x00\x76\x00\x65\x00\x72\x00" \ b"\x07\x00\x08\x00\x00\x00\x00\x00" \ b"\x00\x00\x00\x00\x00\x00\x00\x00" actual_auth = ntlm_context.step(challenge_msg) expected_auth = b'\x4E\x54\x4C\x4D\x53\x53\x50\x00' \ b'\x03\x00\x00\x00\x18\x00\x18\x00' \ b'\x7C\x00\x00\x00\x7C\x00\x7C\x00' \ b'\x94\x00\x00\x00\x0C\x00\x0C\x00' \ b'\x58\x00\x00\x00\x08\x00\x08\x00' \ b'\x64\x00\x00\x00\x10\x00\x10\x00' \ b'\x6C\x00\x00\x00\x10\x00\x10\x00' \ b'\x10\x01\x00\x00\x31\x82\x8A\xE2' \ b'\x05\x01\x28\x0A\x00\x00\x00\x0F' \ b'\xC4\x45\x2C\xF7\xA8\x1E\x4D\x11' \ b'\xD0\x78\x18\x94\x09\x57\x5D\x9E' \ b'\x44\x00\x6F\x00\x6D\x00\x61\x00' \ b'\x69\x00\x6E\x00\x55\x00\x73\x00' \ b'\x65\x00\x72\x00\x43\x00\x4F\x00' \ b'\x4D\x00\x50\x00\x55\x00\x54\x00' \ b'\x45\x00\x52\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\xA1\x3D\x03\x8A' \ b'\xD0\xCA\x02\x64\x33\x89\x7C\x33' \ b'\x5E\x0F\x56\xDF\x01\x01\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\xAA\xAA\xAA\xAA' \ b'\xAA\xAA\xAA\xAA\x00\x00\x00\x00' \ b'\x02\x00\x0C\x00\x44\x00\x6F\x00' \ b'\x6D\x00\x61\x00\x69\x00\x6E\x00' \ b'\x01\x00\x0C\x00\x53\x00\x65\x00' \ b'\x72\x00\x76\x00\x65\x00\x72\x00' \ b'\x07\x00\x08\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x06\x00\x04\x00' \ b'\x02\x00\x00\x00\x0A\x00\x10\x00' \ b'\x6E\xA1\x9D\xF0\x66\xDA\x46\x22' \ b'\x05\x1F\x9C\x4F\x92\xC6\xDF\x74' \ b'\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x1D\x08\x89\xD1\xA5\xEE\xED\x21' \ b'\x91\x9E\x1A\xB8\x27\xC3\x0B\x17' assert actual_auth == expected_auth assert ntlm_context.complete assert ntlm_context.mic_present request_msg = b"test req" response_msg = b"test res" actual_wrapped = ntlm_context.wrap(request_msg) expected_wrapped = b"\x01\x00\x00\x00\xbc\xe3\x23\xa1" \ b"\x72\x06\x23\x78\x00\x00\x00\x00" \ b"\x70\x80\x1e\x11\xfe\x6b\x3a\xad" assert actual_wrapped == expected_wrapped server_sec = SessionSecurity( ntlm_context._session_security.negotiate_flags, ntlm_context._session_security.exported_session_key, "server") server_unwrap = server_sec.unwrap(actual_wrapped[16:], actual_wrapped[0:16]) assert server_unwrap == request_msg response_wrapped = server_sec.wrap(response_msg) actual_unwrap = ntlm_context.unwrap(response_wrapped[1] + response_wrapped[0]) assert actual_unwrap == response_msg msg = b"Hello" actual_sig1 = ntlm_context.sign(msg) expected_sig1 = b"\x01\x00\x00\x00\x08\xF0\x0D\x86\x34\x05\x1A\x1D\x01\x00\x00\x00" assert actual_sig1 == expected_sig1 server_sec.verify_signature(msg, actual_sig1) actual_sig2 = ntlm_context.sign(msg) expected_sig2 = b"\x01\x00\x00\x00\x07\x64\x0C\x30\x1C\xD7\x76\xF0\x02\x00\x00\x00" assert actual_sig2 == expected_sig2 server_sec.verify_signature(msg, actual_sig2) ntlm_context.reset_rc4_state() actual_sig3 = ntlm_context.sign(msg) expected_sig3 = b"\x01\x00\x00\x00\x1E\xD4\xA3\xE5\xE8\x05\x74\x01\x03\x00\x00\x00" assert actual_sig3 == expected_sig3 server_sec.reset_rc4_state(outgoing=False) server_sec.verify_signature(msg, actual_sig3) server_sig = server_sec.get_signature(msg) ntlm_context.verify(msg, server_sig)
def test_ntlm_context(self, monkeypatch): monkeypatch.setattr('os.urandom', lambda s: b"\xaa" * 8) monkeypatch.setattr('ntlm_auth.messages.get_version', lambda s: b"\x05\x01\x28\x0A\x00\x00\x00\x0F") monkeypatch.setattr('ntlm_auth.messages.get_random_export_session_key', lambda: b"\x55" * 16) monkeypatch.setattr('ntlm_auth.compute_response.get_windows_timestamp', lambda: b"\x00" * 8) ch = 'E3CA49271E5089CC48CE82109F1324F41DBEDDC29A777410C738F7868C4FF405' cbt_data = GssChannelBindingsStruct() cbt_data[cbt_data.APPLICATION_DATA] = b"tls-server-end-point:" + \ base64.b16decode(ch) ntlm_context = NtlmContext("User", "Password", "Domain", "COMPUTER", cbt_data=cbt_data) actual_nego = ntlm_context.step() expected_nego = b"\x4e\x54\x4c\x4d\x53\x53\x50\x00" \ b"\x01\x00\x00\x00\x31\xb0\x88\xe2" \ b"\x06\x00\x06\x00\x28\x00\x00\x00" \ b"\x08\x00\x08\x00\x2e\x00\x00\x00" \ b"\x05\x01\x28\x0a\x00\x00\x00\x0f" \ b"\x44\x6f\x6d\x61\x69\x6e\x43\x4f" \ b"\x4d\x50\x55\x54\x45\x52" assert actual_nego == expected_nego assert not ntlm_context.mic_present assert not ntlm_context.complete challenge_msg = b"\x4e\x54\x4c\x4d\x53\x53\x50\x00" \ b"\x02\x00\x00\x00\x2f\x82\x88\xe2" \ b"\x38\x00\x00\x00\x33\x82\x8a\xe2" \ b"\x01\x23\x45\x67\x89\xab\xcd\xef" \ b"\x00\x00\x00\x00\x00\x00\x00\x00" \ b"\x24\x00\x24\x00\x44\x00\x00\x00" \ b"\x06\x00\x70\x17\x00\x00\x00\x0f" \ b"\x53\x00\x65\x00\x72\x00\x76\x00" \ b"\x65\x00\x72\x00\x02\x00\x0c\x00" \ b"\x44\x00\x6f\x00\x6d\x00\x61\x00" \ b"\x69\x00\x6e\x00\x01\x00\x0c\x00" \ b"\x53\x00\x65\x00\x72\x00\x76\x00" \ b"\x65\x00\x72\x00\x00\x00\x00\x00" actual_auth = ntlm_context.step(challenge_msg) expected_auth = b'\x4e\x54\x4c\x4d\x53\x53\x50\x00' \ b'\x03\x00\x00\x00\x18\x00\x18\x00' \ b'\x6c\x00\x00\x00\x68\x00\x68\x00' \ b'\x84\x00\x00\x00\x0c\x00\x0c\x00' \ b'\x48\x00\x00\x00\x08\x00\x08\x00' \ b'\x54\x00\x00\x00\x10\x00\x10\x00' \ b'\x5c\x00\x00\x00\x10\x00\x10\x00' \ b'\xec\x00\x00\x00\x31\x82\x8a\xe2' \ b'\x05\x01\x28\x0a\x00\x00\x00\x0f' \ b'\x44\x00\x6f\x00\x6d\x00\x61\x00' \ b'\x69\x00\x6e\x00\x55\x00\x73\x00' \ b'\x65\x00\x72\x00\x43\x00\x4f\x00' \ b'\x4d\x00\x50\x00\x55\x00\x54\x00' \ b'\x45\x00\x52\x00\x86\xc3\x50\x97' \ b'\xac\x9c\xec\x10\x25\x54\x76\x4a' \ b'\x57\xcc\xcc\x19\xaa\xaa\xaa\xaa' \ b'\xaa\xaa\xaa\xaa\x04\x10\xc4\x7a' \ b'\xcf\x19\x97\x89\xde\x7f\x20\x11' \ b'\x95\x7a\xea\x50\x01\x01\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\xaa\xaa\xaa\xaa' \ b'\xaa\xaa\xaa\xaa\x00\x00\x00\x00' \ b'\x02\x00\x0c\x00\x44\x00\x6f\x00' \ b'\x6d\x00\x61\x00\x69\x00\x6e\x00' \ b'\x01\x00\x0c\x00\x53\x00\x65\x00' \ b'\x72\x00\x76\x00\x65\x00\x72\x00' \ b'\x0a\x00\x10\x00\x6e\xa1\x9d\xf0' \ b'\x66\xda\x46\x22\x05\x1f\x9c\x4f' \ b'\x92\xc6\xdf\x74\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\xe5\x69\x95\x1d' \ b'\x15\xd4\x73\x5f\x49\xe1\x4c\xf9' \ b'\xa7\xd3\xe6\x72' assert actual_auth == expected_auth assert ntlm_context.complete assert not ntlm_context.mic_present request_msg = b"test req" response_msg = b"test res" actual_wrapped = ntlm_context.wrap(request_msg) expected_wrapped = b"\x01\x00\x00\x00\xbc\xe3\x23\xa1" \ b"\x72\x06\x23\x78\x00\x00\x00\x00" \ b"\x70\x80\x1e\x11\xfe\x6b\x3a\xad" assert actual_wrapped == expected_wrapped server_sec = SessionSecurity( ntlm_context._session_security.negotiate_flags, ntlm_context._session_security.exported_session_key, "server") server_unwrap = server_sec.unwrap(actual_wrapped[16:], actual_wrapped[0:16]) assert server_unwrap == request_msg response_wrapped = server_sec.wrap(response_msg) actual_unwrap = ntlm_context.unwrap(response_wrapped[1] + response_wrapped[0]) assert actual_unwrap == response_msg