def step(self, in_token=None): log.debug("SPNEGO step input: %s", to_text(base64.b64encode(in_token or b""))) # Step 1. Process SPNEGO mechs. mech_token_in, mech_list_mic, is_spnego = self._step_spnego_input( in_token=in_token) mech_token_out = None if mech_token_in or self.usage == 'initiate': # Step 2. Process the inner context tokens. mech_token_out = self._step_spnego_token(in_token=mech_token_in) if is_spnego: # Step 3. Process / generate the mechListMIC. out_mic = self._step_spnego_mic(in_mic=mech_list_mic) # Step 4. Generate the output SPNEGO token. out_token = self._step_spnego_output(out_token=mech_token_out, out_mic=out_mic) else: out_token = mech_token_out self._complete = self._context.complete if self.complete: # Remove the leftover contexts if there are still others remaining. self._context_list = collections.OrderedDict([ (self._chosen_mech, (self._context, None)) ]) log.debug("SPNEGO step output: %s" % to_text(base64.b64encode(out_token or b""))) return out_token
def split_username( username ): # type: (Optional[str]) -> Tuple[Optional[str], Optional[str]] """Splits a username and returns the domain component. Will split a username in the Netlogon form `DOMAIN\\username` and return the domain and user part as separate strings. If the user does not contain the `DOMAIN\\` prefix or is in the `UPN` form then then user stays the same and the domain is an empty string. Args: username: The username to split Returns: Tuple[Optional[str], Optional[str]]: The domain and username. """ if username is None: return None, None if '\\' in username: domain, username = username.split('\\', 1) else: domain = None return to_text(domain, nonstring='passthru'), to_text(username, nonstring='passthru')
def step(self, in_token=None): if not self._is_wrapped: log.debug("NTLM step input: %s", to_text(base64.b64encode(in_token or b""))) out_token = getattr(self, '_step_%s' % self.usage)(in_token=in_token) if not self._is_wrapped: log.debug("NTLM step output: %s", to_text(base64.b64encode(out_token or b""))) if self._complete: self._temp_msg = None # Clear out any temp data we still have stored. in_usage = 'accept' if self.usage == 'initiate' else 'initiate' self._sign_key_out = signkey(self._context_attr, self._session_key, self.usage) self._sign_key_in = signkey(self._context_attr, self._session_key, in_usage) # Found a vague reference in MS-NLMP that states if NTLMv2 authentication was not used then only 1 key is # used for sealing. This seems to reference when NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY is not set and # not NTLMv2 messages itself. # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/d1c86e81-eb66-47fd-8a6f-970050121347 if self._context_attr & NegotiateFlags.extended_session_security: self._handle_out = rc4init( sealkey(self._context_attr, self._session_key, self.usage)) self._handle_in = rc4init( sealkey(self._context_attr, self._session_key, in_usage)) else: self._handle_out = self._handle_in = rc4init( sealkey(self._context_attr, self._session_key, self.usage)) return out_token
def ntlm_cred(tmpdir, monkeypatch): cleanup = None try: # Use unicode credentials to test out edge cases when dealing with non-ascii chars. username = u'ÜseӜ' password = u'Pӓ$sw0r̈d' if HAS_SSPI: domain = to_text(socket.gethostname()) # Can only test this out with Windows due to issue with gss-ntlmssp when dealing with surrogate pairs. # https://github.com/gssapi/gss-ntlmssp/issues/20 clef = to_text(b"\xF0\x9D\x84\x9E") username += clef password += clef buff = { 'name': username, 'password': password, 'priv': win32netcon.USER_PRIV_USER, 'comment': 'Test account for pypsnego tests', 'flags': win32netcon.UF_NORMAL_ACCOUNT, } try: win32net.NetUserAdd(None, 1, buff) except win32net.error as err: if err.winerror != 2224: # Account already exists raise def cleanup(): win32net.NetUserDel(None, username) else: domain = u'Dȫm̈Ąiᴞ' # gss-ntlmssp does a string comparison of the user/domain part using the current process locale settings. # To ensure it matches the credentials we specify with the non-ascii chars we need to ensure the locale is # something that can support UTF-8 character comparison. macOS can fail with unknown locale on getlocale(), # just default to env vars if this get fails. try: original_locale = locale.getlocale(locale.LC_CTYPE) except ValueError: original_locale = (None, None) def cleanup(): locale.setlocale(locale.LC_CTYPE, original_locale) locale.setlocale(locale.LC_CTYPE, 'en_US.UTF-8') tmp_creds = os.path.join(to_text(tmpdir), u'pÿspᴞӛgӫ TÈ$''.creds') with open(tmp_creds, mode='wb') as fd: fd.write(to_bytes(u'%s:%s:%s' % (domain, username, password))) monkeypatch.setenv('NTLM_USER_FILE', to_native(tmp_creds)) yield u"%s\\%s" % (domain, username), password finally: if cleanup: cleanup()
def test_get_credential_file(tmpdir, monkeypatch): tmp_creds = os.path.join(to_text(tmpdir), u'pÿspᴞӛgӫ TÈ$' '.creds') with open(tmp_creds, mode='wb') as fd: fd.write(b"data") monkeypatch.setenv('NTLM_USER_FILE', to_native(tmp_creds)) actual = ntlm._get_credential_file() assert actual == to_text(tmp_creds)
def __init__(self, username=None, password=None, hostname=None, service=None, channel_bindings=None, context_req=ContextReq.default, usage='initiate', protocol='negotiate', options=0): if not HAS_SSPI: reraise( ImportError( "SSPIProxy requires the SSPI Cython extension to be compiled" ), SSPI_IMP_ERR) super(SSPIProxy, self).__init__(username, password, hostname, service, channel_bindings, context_req, usage, protocol, options, False) self._attr_sizes = None self._complete = False self._credential = None self._context = SSPISecContext() self.__seq_num = 0 credential_kwargs = { 'package': to_text(protocol), } if usage == 'initiate': # TODO: It seems like the SPN is just an empty string for anon auth. credential_kwargs['principal'] = None credential_kwargs['credential_use'] = CredentialUse.outbound if self.username: domain, username = split_username(self.username) credential_kwargs['auth_data'] = WinNTAuthIdentity( to_text(username, nonstring='passthru'), to_text(domain, nonstring='passthru'), to_text(password, nonstring='passthru')) else: credential_kwargs['principal'] = self.spn credential_kwargs['credential_use'] = CredentialUse.inbound try: self._credential = acquire_credentials_handle(**credential_kwargs) except NativeError as win_err: reraise( SpnegoError(base_error=win_err, context_msg="Getting SSPI credential"))
def step(self, in_token=None): if not self._is_wrapped: log.debug("GSSAPI step input: %s", to_text(base64.b64encode(in_token or b""))) out_token = self._context.step(in_token) self._context_attr = int(self._context.actual_flags) if not self._is_wrapped: log.debug("GSSAPI step output: %s", to_text(base64.b64encode(out_token or b""))) return out_token
def __init__(self, username, password, hostname, service, channel_bindings, context_req, usage, protocol, options, _is_wrapped): # type: (Optional[text_type], Optional[text_type], Optional[text_type], Optional[text_type], Optional[GssChannelBindings], ContextReq, str, text_type, NegotiateOptions, bool) -> None # noqa self.usage = usage.lower() if self.usage not in ['initiate', 'accept']: raise ValueError("Invalid usage '%s', must be initiate or accept" % self.usage) self.protocol = protocol.lower() if self.protocol not in [u'ntlm', u'kerberos', u'negotiate']: raise ValueError( to_native( u"Invalid protocol '%s', must be ntlm, kerberos, or negotiate" % self.protocol)) if self.protocol not in self.available_protocols(options=options): raise ValueError("Protocol %s is not available" % self.protocol) self.username = to_text(username, nonstring='passthru') self.password = to_text(password, nonstring='passthru') self.spn = None if service or hostname: self.spn = to_text( "%s/%s" % (service.upper() if service else "HOST", hostname or "unspecified")) self.channel_bindings = channel_bindings self.options = NegotiateOptions(options) self.context_req = context_req # Generic context requirements. self._context_req = 0 # Provider specific context requirements. for generic, provider in self._context_attr_map: if context_req & generic: self._context_req |= provider self._context_attr = 0 # Provider specific context attributes, set by self.step(). # Whether the context is wrapped inside another context. self._is_wrapped = _is_wrapped # type: bool if options & NegotiateOptions.negotiate_kerberos and ( self.protocol == 'negotiate' and 'kerberos' not in self.available_protocols()): raise FeatureMissingError(NegotiateOptions.negotiate_kerberos) if options & NegotiateOptions.wrapping_iov and not self.iov_available( ): raise FeatureMissingError(NegotiateOptions.wrapping_iov)
def test_win_nt_auth_identity_set_username(): identity = sspi.WinNTAuthIdentity(u"original", None, None) test_user = u"user" + to_text(b"\xF0\x9D\x84\x9E") identity.username = test_user assert identity.username == test_user assert str(identity) == to_native(test_user)
def unpack(b_data): # type: (bytes) -> TargetInfo """ Unpacks the structure from bytes. """ target_info = TargetInfo() b_io = io.BytesIO(b_data) b_av_id = b_io.read(2) while b_av_id: av_id = struct.unpack("<H", b_av_id)[0] length = struct.unpack("<H", b_io.read(2))[0] b_value = b_io.read(length) if av_id in TargetInfo._FIELD_TYPES['text']: # All AV_PAIRS are UNICODE encoded. value = to_text(b_value, encoding='utf-16-le') elif av_id in TargetInfo._FIELD_TYPES['int32']: value = AvFlags(struct.unpack("<I", b_value)[0]) elif av_id == AvId.timestamp: value = FileTime.unpack(b_value) elif av_id == AvId.single_host: value = SingleHost.unpack(b_value) else: value = b_value target_info[AvId(av_id)] = value b_av_id = b_io.read(2) return target_info
def test_get_credential_file_env_var_missing_file(tmpdir, monkeypatch): tmp_creds = os.path.join(to_text(tmpdir), u'pÿspᴞӛgӫ TÈ$' '.creds') monkeypatch.setenv('NTLM_USER_FILE', to_native(tmp_creds)) actual = ntlm._get_credential_file() assert actual is None
def test_win_nt_auth_identity_set_domain(): identity = sspi.WinNTAuthIdentity(None, u"original", None) test_domain = u"domain" + to_text(b"\xF0\x9D\x84\x9E") identity.domain = test_domain assert identity.domain == test_domain assert str(identity) == to_native(test_domain) + "\\"
def test_ntowfv1_hash(): lm_hash = os.urandom(16) nt_hash = os.urandom(16) ntlm_hash = to_text(b"%s:%s" % (base64.b16encode(lm_hash), base64.b16encode(nt_hash))) actual = crypto.ntowfv1(ntlm_hash) assert actual == nt_hash
def step(self, in_token=None): log.debug("SSPI step input: %s", to_text(base64.b64encode(in_token or b""))) sec_tokens = [] if in_token: sec_tokens.append(SecBuffer(SecBufferType.token, in_token)) if self.channel_bindings: sec_tokens.append( SecBuffer(SecBufferType.channel_bindings, self._get_native_bindings())) in_buffer = SecBufferDesc(sec_tokens) if sec_tokens else None out_buffer = SecBufferDesc([SecBuffer(SecBufferType.token)]) if self.usage == 'initiate': res = initialize_security_context(self._credential, self._context, self.spn, context_req=self._context_req, input_buffer=in_buffer, output_buffer=out_buffer) else: res = accept_security_context(self._credential, self._context, in_buffer, context_req=self._context_req, output_buffer=out_buffer) self._context_attr = int(self._context.context_attr) if res == SecStatus.SEC_E_OK: self._complete = True self._attr_sizes = query_context_attributes( self._context, SecPkgAttr.sizes) # TODO: Determine if this returns None or an empty byte string. out_token = out_buffer[0].buffer log.debug("SSPI step output: %s", to_text(base64.b64encode(out_token or b""))) return out_token
def test_get_credential_from_file_no_matches(tmpdir, monkeypatch): tmp_creds = os.path.join(to_text(tmpdir), u'pÿspᴞӛgӫ TÈ$' '.creds') monkeypatch.setenv('NTLM_USER_FILE', to_native(tmp_creds)) with open(tmp_creds, mode='wb') as fd: fd.write(b'domain:username:password') with pytest.raises( SpnegoError, match="Failed to find any matching credential in NTLM_USER_FILE " "credential store."): ntlm._NTLMCredential("fake", "username")
def _step_accept_negotiate(self, token): # type: (bytes) -> bytes """ Process the Negotiate message from the initiator. """ negotiate = Negotiate.unpack(token) flags = negotiate.flags | NegotiateFlags.request_target | NegotiateFlags.ntlm | \ NegotiateFlags.always_sign | NegotiateFlags.target_info | NegotiateFlags.target_type_server # Make sure either UNICODE or OEM is set, not both. if flags & NegotiateFlags.unicode: flags &= ~NegotiateFlags.oem elif flags & NegotiateFlags.oem == 0: raise SpnegoError( ErrorCode.failure, context_msg= "Neither NEGOTIATE_OEM or NEGOTIATE_UNICODE flags were " "set, cannot derive encoding for text fields") if flags & NegotiateFlags.extended_session_security: flags &= ~NegotiateFlags.lm_key server_challenge = os.urandom(8) target_name = to_text(socket.gethostname()).upper() target_info = TargetInfo() target_info[AvId.nb_computer_name] = target_name target_info[AvId.nb_domain_name] = u"WORKSTATION" target_info[AvId.dns_computer_name] = to_text(socket.getfqdn()) target_info[AvId.timestamp] = FileTime.now() challenge = Challenge(flags, server_challenge, target_name=target_name, target_info=target_info) self._temp_msg = { 'negotiate': negotiate, 'challenge': challenge, } return challenge.pack()
def _rebuild_context_list(self, mech_types=None ): # type: (Optional[List[str]]) -> List[str] """ Builds a new context list that are available to the client. """ context_kwargs = { 'username': self.username, 'password': self.password, 'hostname': self._hostname, 'service': self._service, 'channel_bindings': self.channel_bindings, 'context_req': self.context_req, 'usage': self.usage, 'options': self.options, '_is_wrapped': True, } gssapi_protocols = [ p for p in GSSAPIProxy.available_protocols(options=self.options) if p != 'negotiate' ] all_protocols = gssapi_protocols[:] if 'ntlm' not in all_protocols: all_protocols.append('ntlm') self._context_list = collections.OrderedDict() mech_list = [] last_err = None for protocol in all_protocols: mech = getattr(GSSMech, protocol) if mech_types and mech.value not in mech_types: continue try: proxy_obj = GSSAPIProxy if protocol in gssapi_protocols else NTLMProxy context = proxy_obj(protocol=protocol, **context_kwargs) first_token = context.step( ) if self.usage == 'initiate' else None except Exception as e: last_err = e log.debug( "Failed to create gssapi context for SPNEGO protocol %s: %s", protocol, to_text(e)) continue self._context_list[mech] = (context, first_token) mech_list.append(mech.value) if not mech_list: raise BadMechanismError( context_msg="Unable to negotiate common mechanism", base_error=last_err) return mech_list
def test_ntlm_custom_time(include_time, expected, ntlm_cred, mocker, monkeypatch): c = spnego.client(ntlm_cred[0], ntlm_cred[1], hostname=socket.gethostname(), options=spnego.NegotiateOptions.use_ntlm, protocol='ntlm') b_negotiate = c.step() negotiate = Negotiate.unpack(b_negotiate) flags = negotiate.flags | NegotiateFlags.request_target | NegotiateFlags.ntlm | \ NegotiateFlags.always_sign | NegotiateFlags.target_info | NegotiateFlags.target_type_server server_challenge = os.urandom(8) target_name = to_text(socket.gethostname()).upper() target_info = TargetInfo() target_info[AvId.nb_computer_name] = target_name target_info[AvId.nb_domain_name] = u"WORKSTATION" target_info[AvId.dns_computer_name] = to_text(socket.getfqdn()) if include_time: target_info[AvId.timestamp] = FileTime.now() challenge = Challenge(flags, server_challenge, target_name=target_name, target_info=target_info) mock_now = mocker.MagicMock() mock_now.side_effect = FileTime.now monkeypatch.setattr(FileTime, 'now', mock_now) c.step(challenge.pack()) assert c.complete assert mock_now.call_count == expected
def test_ntlm_workstation_override(env_var, expected, ntlm_cred, monkeypatch): if env_var is not None: monkeypatch.setenv('NETBIOS_COMPUTER_NAME', env_var) c = spnego.client(ntlm_cred[0], ntlm_cred[1], hostname=socket.gethostname(), options=spnego.NegotiateOptions.use_ntlm, protocol='ntlm') b_negotiate = c.step() negotiate = Negotiate.unpack(b_negotiate) flags = negotiate.flags | NegotiateFlags.request_target | NegotiateFlags.ntlm | \ NegotiateFlags.always_sign | NegotiateFlags.target_info | NegotiateFlags.target_type_server server_challenge = os.urandom(8) target_name = to_text(socket.gethostname()).upper() target_info = TargetInfo() target_info[AvId.nb_computer_name] = target_name target_info[AvId.nb_domain_name] = u"WORKSTATION" target_info[AvId.dns_computer_name] = to_text(socket.getfqdn()) target_info[AvId.timestamp] = FileTime.now() version = Version(10, 0, 0, 1) challenge = Challenge(flags, server_challenge, target_name=target_name, target_info=target_info, version=version) b_auth = c.step(challenge.pack()) auth = Authenticate.unpack(b_auth) assert auth.workstation == expected
def _get_credential_file(): # type: () -> Optional[text_type] """Get the path to the NTLM credential store. Returns the path to the NTLM credential store specified by the environment variable `NTLM_USER_FILE`. Returns: Optional[bytes]: The path to the NTLM credential file or None if not set or found. """ user_file_path = os.environ.get('NTLM_USER_FILE', None) if not user_file_path: return user_file_path = to_text(user_file_path, encoding='utf-8') if os.path.isfile(user_file_path): return user_file_path
def test_get_credential_from_file(line, username, domain, lm_hash, nt_hash, explicit, tmpdir, monkeypatch): tmp_creds = os.path.join(to_text(tmpdir), u'pÿspᴞӛgӫ TÈ$' '.creds') monkeypatch.setenv('NTLM_USER_FILE', to_native(tmp_creds)) with open(tmp_creds, mode='wb') as fd: fd.write(to_bytes(line)) if explicit: actual = ntlm._NTLMCredential(domain, username) else: actual = ntlm._NTLMCredential() assert actual.username == username assert actual.domain == domain assert actual.lm_hash == base64.b16decode(lm_hash) assert actual.nt_hash == base64.b16decode(nt_hash)
def _get_workstation(): # type: () -> Optional[str] """Get the current workstation name. This gets the current workstation name that respects `NETBIOS_COMPUTER_NAME`. The env var is used by the library that gss-ntlmssp calls and makes sure that this Python implementation is a closer in its behaviour. Returns: Optional[str]: The workstation to supply in the NTLM authentication message or None. """ if 'NETBIOS_COMPUTER_NAME' in os.environ: workstation = os.environ['NETBIOS_COMPUTER_NAME'] else: workstation = socket.gethostname().upper() # An empty workstation should be None so we don't set it in the message. return to_text(workstation) if workstation else None
def unpack_asn1_generalized_time(value): # type: (Union[bytes, ASN1Value]) -> datetime.datetime """ Unpacks an ASN.1 GeneralizedTime value. """ data = to_text(extract_asn1_tlv(value, TagClass.universal, TypeTagNumber.generalized_time)) # While ASN.1 can have a timezone encoded, KerberosTime is the only thing we use and it is always in UTC with the # Z prefix. We strip out the Z because Python 2 doesn't support the %z identifier and add the UTC tz to the object. # https://www.rfc-editor.org/rfc/rfc4120#section-5.2.3 if data.endswith('Z'): data = data[:-1] err = None for datetime_format in ['%Y%m%d%H%M%S.%f', '%Y%m%d%H%M%S']: try: dt = datetime.datetime.strptime(data, datetime_format) return dt.replace(tzinfo=UTC()) except ValueError as e: err = e else: raise err
def domain_name(self): # type: () -> Optional[text_type] """ The domain or computer name hosting the user account. """ return to_text(_unpack_payload(self._data, 28), encoding=self._encoding, nonstring='passthru')
FileTime, NegotiateFlags, NTClientChallengeV2, TargetInfo, ) from spnego._text import ( text_type, to_bytes, to_text, ) # A user does not need to specify their actual plaintext password they can specify the LM and NT hash (from lmowfv1 and # ntowfv2) in the form 'lm_hash_hex:nt_hash_hex'. This is still considered a plaintext pass as we can use it to build # the LM and NT response but it's only usable for NTLM. _NTLM_HASH_PATTERN = re.compile(to_text(r'^[a-fA-F0-9]{32}:[a-fA-F0-9]{32}$')) class RC4Handle: """ RC4 class to wrap the underlying crypto function. """ def __init__(self, key): # type: (bytes) -> None self._key = key self._handle = None self.reset() def update(self, b_data): # type: (bytes) -> bytes """ Update the RC4 stream and return the encrypted/decrypted bytes. """ return self._handle.update(b_data) def reset(self): # type: () -> None """ Reset's the cipher stream back to the original state. """
if sys.version_info[0] == 2: assert actual == r"SecBuffer(cbBuffer=4, BufferType=1, pvBuffer='\x01\x02\x03\x04')" else: assert actual == r"SecBuffer(cbBuffer=4, BufferType=1, pvBuffer=b'\x01\x02\x03\x04')" @pytest.mark.skipif( SKIP, reason='Can only test Cython code on Windows with compiled code.') @pytest.mark.parametrize( 'username, domain, expected', [(u"username", u"domain", "domain\\username"), (u"username@DOMAIN", u"", "username@DOMAIN"), (u"username@DOMAIN", None, "username@DOMAIN"), (None, u"domain", "domain\\"), (None, None, u""), (u"", u"", u""), (u"user" + to_text(b"\xF0\x9D\x84\x9E"), u"domain" + to_text(b"\xF0\x9D\x84\x9E"), to_native(u"domain{0}\\user{0}".format(to_text(b"\xF0\x9D\x84\x9E"))))]) def test_win_nt_auth_identity(username, domain, expected): identity = sspi.WinNTAuthIdentity(username, domain, u"password") assert repr( identity) == "<spnego._sspi_raw.sspi.WinNTAuthIdentity %s>" % expected assert str(identity) == expected @pytest.mark.skipif( SKIP, reason='Can only test Cython code on Windows with compiled code.') def test_win_nt_auth_identity_set_username(): identity = sspi.WinNTAuthIdentity(u"original", None, None)
def user_name(self): # type: () -> Optional[text_type] """ The name of the user to be authenticated. """ return to_text(_unpack_payload(self._data, 36), encoding=self._encoding, nonstring='passthru')
hostname=socket.gethostname(), options=spnego.NegotiateOptions.use_ntlm, protocol='ntlm') s = spnego.server(options=spnego.NegotiateOptions.use_ntlm, protocol='ntlm') auth = memoryview(bytearray(c.step(s.step(c.step())))) auth[64:80] = b"\x01" * 16 with pytest.raises(InvalidTokenError, match="Invalid MIC in NTLM authentication message"): s.step(auth.tobytes()) @pytest.mark.parametrize('env_var, expected', [ (None, to_text(socket.gethostname()).upper()), ('', None), ('custom', 'custom'), ]) def test_ntlm_workstation_override(env_var, expected, ntlm_cred, monkeypatch): if env_var is not None: monkeypatch.setenv('NETBIOS_COMPUTER_NAME', env_var) c = spnego.client(ntlm_cred[0], ntlm_cred[1], hostname=socket.gethostname(), options=spnego.NegotiateOptions.use_ntlm, protocol='ntlm') b_negotiate = c.step() negotiate = Negotiate.unpack(b_negotiate)
def test_win_nt_auth_identity_set_password(): identity = sspi.WinNTAuthIdentity(None, None, u"original") test_password = u"password" + to_text(b"\xF0\x9D\x84\x9E") identity.password = test_password assert identity.password == test_password
def workstation(self): # type: () -> Optional[text_type] """ The name of the computer to which the user is logged on. """ return to_text(_unpack_payload(self._data, 44), encoding=self._encoding, nonstring='passthru')