diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 1af1139b9b4..63f735d6920 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -1870,6 +1870,7 @@ def GSS_Accept_sec_context( ), ProductMajorVersion=10, ProductMinorVersion=0, + ProductBuild=26100, Payload=[ ("TargetName", ""), ( diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index 695903ddf38..d5590963cab 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -9,6 +9,7 @@ Implements parts of: - GSSAPI SPNEGO: RFC4178 > RFC2478 +- [MS-SPNG]: Microsoft's SPNEGO extensions + "LF" (Late Fallback mechanism) - GSSAPI SPNEGO NEGOEX: [MS-NEGOEX] .. note:: @@ -243,7 +244,7 @@ class SPNEGO_negTokenResp(ASN1_Packet): ), # [MS-SPNG] Late Fallback Mechanism ASN1F_optional( - ASN1F_PACKET("supportedMechs", None, SPNEGO_MechTypes, explicit_tag=0xA4) + ASN1F_PACKET("mechTypes", None, SPNEGO_MechTypes, explicit_tag=0xA4) ), ) @@ -572,6 +573,7 @@ class CONTEXT(SSP.CONTEXT): "ssps", "server_mechtypes", "client_mechtypes", + "all_mechtypes", "first_choice", "require_mic", "verified_mic", @@ -597,6 +599,7 @@ def __init__( self.ssps = ssps self.server_mechtypes = None # the mechtypes the server requested self.client_mechtypes = None # the mechtypes the client requested + self.all_mechtypes = [] # all mechtypes of the exchange, for mechListMIC self.first_choice = True # whether the SSP was the peer's first choice self.require_mic = False # whether the mechListMIC is required or not self.verified_mic = False # whether mechListMIC has been verified @@ -649,7 +652,6 @@ def get_mechListMIC(self): Return the binary used for mechListMIC """ # See help(mechListMIC) for more details - mechtypes = self.client_mechtypes[:] # [MS-SPNG] sect 3.1.5.1 - Late Fallback # "When Negotiate Late Fallback is supported by both parties, @@ -657,8 +659,9 @@ def get_mechListMIC(self): # supportedMechs per the order of over-the-wire transmission # with delimiters" if self.late_fallback_negotiated: - # XXX Doesn't work. FIXME - mechtypes += self.server_mechtypes[:] + mechtypes = self.all_mechtypes + else: + mechtypes = self.client_mechtypes return mechListMIC(mechtypes) @@ -669,19 +672,15 @@ def negotiate_ssp(self) -> None: This updates our context and sets it with the first SSP that is common to both client and server. This also applies rules from [MS-SPNG] and RFC4178 to determine if mechListMIC is required. + + If the ssp is already negotiated, this only updates meta attributes. """ if not self.IsAcceptor: other_mechtypes = self.server_mechtypes else: other_mechtypes = self.client_mechtypes - if other_mechtypes is None: - # We don't have any information about the peer's preferred SSPs. - # This typically happens on client side, when NegTokenInit2 isn't used. - self.ssp = self.ssps[0] - ssp_oid = self.ssp.GSS_Inquire_names_for_mech()[0] - else: - # Get first common SSP between us and our peer + if other_mechtypes is not None: other_oids = [x.oid.val for x in other_mechtypes] # See if the peer supports SPNEGO Late Fallback @@ -691,6 +690,16 @@ def negotiate_ssp(self) -> None: ): self.late_fallback_negotiated = True + if self.ssp is not None: + # ssp has already been negotiated, return. + return + + if other_mechtypes is None: + # We don't have any information about the peer's preferred SSPs. + # This typically happens on client side, when NegTokenInit2 isn't used. + self.ssp = self.ssps[0] + ssp_oid = self.ssp.GSS_Inquire_names_for_mech()[0] + else: # Find first common SSP try: self.ssp, ssp_oid = next( @@ -757,11 +766,15 @@ def flags(self, x): self.ssp_context.flags = x def __repr__(self): - return "SPNEGOSSP[%s]" % repr(self.ssp_context) + return "SPNEGOSSP[%s%s]" % ( + repr(self.ssp_context), + "/LF" if self.late_fallback_negotiated else "", + ) def __init__( self, ssps: List[SSP], + # Disabled by default right now, enable once mainstream. SUPPORT_LATE_FALLBACK=False, **kwargs, ): @@ -999,6 +1012,10 @@ def from_cli_arguments( # Build the SSP return cls(ssps) + # [MS-SPNG] Late Fallback Mechanism + _LATE_FALLBACK_INITIATOR = ASN1_OID("0.0.99.99.99.99.99.99.99.99") + _LATE_FALLBACK_ACCEPTOR = ASN1_OID("0.0.115.115.115.115.115.115.115.115") + def NegTokenInit2(self): """ Server-Initiation of GSSAPI/SPNEGO. @@ -1064,7 +1081,7 @@ def MapStatusToNegState(self, status: int) -> int: """ if status == GSS_S_COMPLETE: return 0 # accept_completed - elif status == GSS_S_CONTINUE_NEEDED: + elif status & GSS_S_CONTINUE_NEEDED: return 1 # accept_incomplete else: return 2 # reject @@ -1123,8 +1140,21 @@ def GSS_Init_sec_context( negState = input_token.negState # [MS-SPNG] Late Fallback Mechanism - if input_token.supportedMechs is not None: - Context.server_mechtypes = input_token.supportedMechs.mechTypes + if input_token.mechTypes is not None: + Context.server_mechtypes = input_token.mechTypes.mechTypes + Context.all_mechtypes.append(self._LATE_FALLBACK_ACCEPTOR) + Context.all_mechtypes += Context.server_mechtypes + Context.all_mechtypes += [input_token.supportedMech] + + if input_token.supportedMech is not None: + # If the server selected a different SSP than the one we sent, + # retry negotiating. + if Context.ssp_mechtype != input_token.supportedMech: + log_runtime.warning( + "SPNEGOSSP: %s wasn't selected. " + "Retrying with next in queue." % repr(Context.ssp) + ) + Context.ssp = None else: # The blob is a raw token. We aren't using SPNEGO here. Context.raw = True @@ -1132,13 +1162,12 @@ def GSS_Init_sec_context( self.GuessOtherMechtypes(Context, input_token) # Perform SSP negotiation - if Context.ssp is None: - try: - Context.negotiate_ssp() - except ValueError as ex: - # Couldn't find common SSP - log_runtime.warning("SPNEGOSSP: %s" % ex) - return Context, None, GSS_S_BAD_MECH + try: + Context.negotiate_ssp() + except ValueError as ex: + # Couldn't find common SSP + log_runtime.warning("SPNEGOSSP: %s" % ex) + return Context, None, GSS_S_BAD_MECH if Context.state == SPNEGOSSP.STATE.MICONLY: # We have already finished the inner-ssp, and are just doing @@ -1202,6 +1231,10 @@ def GSS_Init_sec_context( # First freeze the list of available mechtypes on the first message Context.client_mechtypes = Context.get_supported_mechtypes() + # [MS-SPNG] Late Fallback Mechanism + Context.all_mechtypes.append(self._LATE_FALLBACK_INITIATOR) + Context.all_mechtypes += Context.client_mechtypes + # Now build the token spnego_tok = GSSAPI_BLOB( innerToken=SPNEGO_negToken( @@ -1279,6 +1312,10 @@ def GSS_Accept_sec_context( # Populate context with values from the client's request if input_token.mechTypes: Context.client_mechtypes = input_token.mechTypes + + # [MS-SPNG] Late Fallback Mechanism + Context.all_mechtypes.append(self._LATE_FALLBACK_INITIATOR) + Context.all_mechtypes += Context.client_mechtypes if input_token.mechToken: input_token_inner = input_token.mechToken.value _mechListMIC = input_token.mechListMIC or input_token._mechListMIC @@ -1298,13 +1335,12 @@ def GSS_Accept_sec_context( return Context, None, GSS_S_FAILURE # Perform SSP negotiation - if Context.ssp is None: - try: - Context.negotiate_ssp() - except ValueError as ex: - # Couldn't find common SSP - log_runtime.warning("SPNEGOSSP: %s" % ex) - return Context, None, GSS_S_FAILURE + try: + Context.negotiate_ssp() + except ValueError as ex: + # Couldn't find common SSP + log_runtime.warning("SPNEGOSSP: %s" % ex) + return Context, None, GSS_S_FAILURE output_token_inner = None status = GSS_S_CONTINUE_NEEDED @@ -1353,10 +1389,21 @@ def GSS_Accept_sec_context( ) ) if Context.state == SPNEGOSSP.STATE.FIRST: + Context.server_mechtypes = Context.get_supported_mechtypes() + # Include the supportedMech list if this is the first message we send # or a renegotiation. spnego_tok.token.supportedMech = Context.ssp_mechtype + if Context.late_fallback_negotiated: + # [MS-SPNG] Late Fallback Mechanism + spnego_tok.token.mechTypes = SPNEGO_MechTypes( + mechTypes=Context.server_mechtypes + ) + Context.all_mechtypes.append(self._LATE_FALLBACK_ACCEPTOR) + Context.all_mechtypes += Context.server_mechtypes + Context.all_mechtypes += [Context.ssp_mechtype] + # Add the output token if provided if output_token_inner: spnego_tok.token.responseToken = SPNEGO_Token(value=output_token_inner) @@ -1409,6 +1456,10 @@ def GSS_Passive( if Context.IsAcceptor: # NegTokenInit Context.client_mechtypes = input_token.mechTypes + + # [MS-SPNG] Late Fallback Mechanism + Context.all_mechtypes.append(self._LATE_FALLBACK_INITIATOR) + Context.all_mechtypes += Context.client_mechtypes else: # NegTokenInit2 Context.server_mechtypes = input_token.mechTypes @@ -1417,6 +1468,13 @@ def GSS_Passive( elif isinstance(input_token, SPNEGO_negTokenResp): if input_token.supportedMech is not None: Context.server_mechtypes = [input_token.supportedMech] + + # [MS-SPNG] Late Fallback Mechanism + if input_token.mechTypes is not None: + Context.server_mechtypes = input_token.mechTypes.mechTypes + Context.all_mechtypes.append(self._LATE_FALLBACK_ACCEPTOR) + Context.all_mechtypes += Context.server_mechtypes + Context.all_mechtypes += [input_token.supportedMech] if input_token.responseToken: input_token_inner = input_token.responseToken.value else: @@ -1432,14 +1490,17 @@ def GSS_Passive( # If we still haven't got a mechtype, guess (raw, most likely) if other_mechtypes is None: self.GuessOtherMechtypes(Context, input_token) + else: + if Context.ssp_mechtype not in other_mechtypes: + # Negotiated ssp wasn't accepted by the peer + Context.ssp = None # Uninitialized OR allowed mechtypes have changed - if Context.ssp is None or Context.ssp_mechtype not in other_mechtypes: - try: - Context.negotiate_ssp() - except ValueError: - # Couldn't find common SSP - return Context, GSS_S_FAILURE + try: + Context.negotiate_ssp() + except ValueError: + # Couldn't find common SSP + return Context, GSS_S_FAILURE # Passthrough Context.ssp_context, status = Context.ssp.GSS_Passive( diff --git a/test/scapy/layers/spnego.uts b/test/scapy/layers/spnego.uts index a8f11ac9ce1..b7b3ae38f10 100644 --- a/test/scapy/layers/spnego.uts +++ b/test/scapy/layers/spnego.uts @@ -6,7 +6,10 @@ % A SPNEGOSSP server talking to a non SPNEGOSSP client should work. -srvssp = SPNEGOSSP([KerberosSSP(), NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")})]) +srvssp = SPNEGOSSP( + [KerberosSSP(), NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")})], + SUPPORT_LATE_FALLBACK=False, +) clissp = NTLMSSP(UPN="User1", PASSWORD="Password123!") clictx, tok, status = clissp.GSS_Init_sec_context(None) @@ -24,13 +27,19 @@ assert tok is None, repr(tok) % Two SPNEGOSSPs with different preferred mechanisms should work, % and mechListMIC should be used. -srvssp = SPNEGOSSP([ - KerberosSSP(), - NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) -]) -clissp = SPNEGOSSP([ - NTLMSSP(UPN="User1", PASSWORD="Password123!"), -]) +srvssp = SPNEGOSSP( + [ + KerberosSSP(), + NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) + ], + SUPPORT_LATE_FALLBACK=False, +) +clissp = SPNEGOSSP( + [ + NTLMSSP(UPN="User1", PASSWORD="Password123!"), + ], + SUPPORT_LATE_FALLBACK=False, +) clictx, tok, status = clissp.GSS_Init_sec_context(None) assert clictx.require_mic @@ -74,13 +83,19 @@ assert tok is None % Same but with NegTokenInit2 -srvssp = SPNEGOSSP([ - KerberosSSP(), - NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) -]) -clissp = SPNEGOSSP([ - NTLMSSP(UPN="User1", PASSWORD="Password123!"), -]) +srvssp = SPNEGOSSP( + [ + KerberosSSP(), + NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) + ], + SUPPORT_LATE_FALLBACK=False, +) +clissp = SPNEGOSSP( + [ + NTLMSSP(UPN="User1", PASSWORD="Password123!"), + ], + SUPPORT_LATE_FALLBACK=False, +) srvctx, tok = srvssp.NegTokenInit2() assert tok.MechType.val == '1.3.6.1.5.5.2' @@ -127,6 +142,73 @@ tok.token.mechListMIC = None srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) assert status == GSS_S_CONTINUE_NEEDED, status # Should now be CONTINUE instead of COMPLETE ! += SPNEGOSSP - SSP negotiation + mechListMIC + Late fallback Mechanism + +% Same as the previous test, with Late Fallback mechanism enabled + +srvssp = SPNEGOSSP( + [ + KerberosSSP(), + NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) + ], + SUPPORT_LATE_FALLBACK=True, +) +clissp = SPNEGOSSP( + [ + NTLMSSP(UPN="User1", PASSWORD="Password123!"), + ], + SUPPORT_LATE_FALLBACK=True, +) + +clictx, tok, status = clissp.GSS_Init_sec_context(None) +assert clictx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert len(tok.innerToken.token.mechTypes) == 2 +assert tok.innerToken.token.mechTypes[0].oid.val == '1.3.6.1.4.1.311.2.2.10' +assert tok.innerToken.token.mechTypes[1].oid.val == '1.3.6.1.4.1.311.2.2.40' +assert tok.innerToken.token.mechListMIC is None +assert tok.innerToken.token._mechListMIC is None +assert isinstance(tok.innerToken.token.mechToken.value, NTLM_NEGOTIATE) + +srvctx, tok, status = srvssp.GSS_Accept_sec_context(None, tok) +assert srvctx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert tok.token.mechListMIC is None +assert tok.token.negState == 1 +assert tok.token.supportedMech.oid.val == '1.3.6.1.4.1.311.2.2.10' +assert [x.oid.val for x in tok.token.mechTypes.mechTypes] == [ + '1.3.6.1.4.1.311.2.2.10', + '1.2.840.48018.1.2.2', + '1.2.840.113554.1.2.2', + '1.3.6.1.4.1.311.2.2.40', +] +assert isinstance(tok.token.responseToken.value, NTLM_CHALLENGE) +assert srvctx.late_fallback_negotiated +assert bytes(srvctx.get_mechListMIC()) == b'0h\x06\t\x00cccccccc\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\x06\n+\x06\x01\x04\x01\x827\x02\x02(\x06\t\x00ssssssss\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\x06\t*\x86H\x86\xf7\x12\x01\x02\x02\x06\n+\x06\x01\x04\x01\x827\x02\x02(\x06\n+\x06\x01\x04\x01\x827\x02\x02\n' + +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_CONTINUE_NEEDED, status +assert tok.token.negState is None +assert tok.token.supportedMech is None +assert isinstance(tok.token.responseToken.value, NTLM_AUTHENTICATE) +assert isinstance(tok.token.mechListMIC.value, NTLMSSP_MESSAGE_SIGNATURE) +assert tok.token.mechListMIC.value.SeqNum == 0 +assert tok.token.mechListMIC.value.Version == 1 +assert clictx.late_fallback_negotiated +assert bytes(clictx.get_mechListMIC()) == b'0h\x06\t\x00cccccccc\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\x06\n+\x06\x01\x04\x01\x827\x02\x02(\x06\t\x00ssssssss\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\x06\t*\x86H\x86\xf7\x12\x01\x02\x02\x06\n+\x06\x01\x04\x01\x827\x02\x02(\x06\n+\x06\x01\x04\x01\x827\x02\x02\n' + +srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) +assert status == GSS_S_COMPLETE, status +assert tok is not None +assert isinstance(tok.token, SPNEGO_negTokenResp) +assert isinstance(tok.token.mechListMIC.value, NTLMSSP_MESSAGE_SIGNATURE) +assert tok.token.mechListMIC.value.Version == 1 +assert tok.token.mechListMIC.value.SeqNum == 0 + +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_COMPLETE, status +assert tok is None + = SPNEGOSSP.from_cli_arguments - Utils from unittest import mock