diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 3e173be1825..64d933590a8 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -1,7 +1,7 @@ name: CIFuzz on: - pull_request: + push: branches: [master] permissions: diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index 168e2d7ecab..2ab19f5db83 100644 --- a/doc/scapy/layers/kerberos.rst +++ b/doc/scapy/layers/kerberos.rst @@ -41,11 +41,11 @@ This section tries to give many usage examples, but isn't exhaustive. For more d >>> t.show() Tickets: 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL - Start time End time Renew until Auth time + Start time End time Renew until Auth time 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 1. Administrator@DOMAIN.LOCAL -> host/dc1.domain.local@DOMAIN.LOCAL - Start time End time Renew until Auth time + Start time End time Renew until Auth time 31/08/23 11:39:07 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 @@ -68,12 +68,41 @@ This section tries to give many usage examples, but isn't exhaustive. For more d >>> # Using the AES-256-SHA1-96 Kerberos Key >>> t.request_tgt("Administrator@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) +- **Request a TGT using PKINIT**: + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import EncryptionType + >>> load_module("ticketer") + >>> t = Ticketer() + >>> # If P12: + >>> t.request_tgt("Administrator@DOMAIN.LOCAL", p12="admin.pfx", ca="ca.pem") + >>> # One could also have used a different cert and key file: + >>> t.request_tgt("Administrator@DOMAIN.LOCAL", x509="admin.cert", x509key="admin.key", ca="ca.pem") + +- **Request a user TGT with Kerberos armoring (FAST)** + +The ``armor_with`` keyword allows to select a ticket to armor the request with. + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("Machine01$@DOMAIN.LOCAL", key=Key(EncryptionType.RC4_HMAC, bytes.fromhex("2b576acbe6bcfda7294d6bd18041b8fe"))) + >>> t.show() + Tickets: + 0. Machine01$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 + >>> t.request_tgt("Administrator@domain.local", armor_with=0) # Armor with ticket n°0 + - **Renew a TGT or ST**: .. code:: >>> t.renew(0) # renew TGT >>> t.renew(1) # renew ST. Works only with 'host/' SPNs + >>> t.renew(1, armor_with=0) # renew something with armoring - **Import tickets from a ccache**: diff --git a/scapy/asn1/mib.py b/scapy/asn1/mib.py index 029f8281225..900a6ab1acc 100644 --- a/scapy/asn1/mib.py +++ b/scapy/asn1/mib.py @@ -284,16 +284,24 @@ def load_mib(filenames): "1.3.101.113": "Ed448", } +# pkcs3 # + +pkcs3_oids = { + "1.2.840.113549.1.3": "pkcs-3", + "1.2.840.113549.1.3.1": "dhKeyAgreement", +} + # pkcs7 # pkcs7_oids = { + "1.2.840.113549.1.7": "pkcs-7", "1.2.840.113549.1.7.2": "id-signedData", } # pkcs9 # pkcs9_oids = { - "1.2.840.113549.1.9": "pkcs9", + "1.2.840.113549.1.9": "pkcs-9", "1.2.840.113549.1.9.0": "modules", "1.2.840.113549.1.9.1": "emailAddress", "1.2.840.113549.1.9.2": "unstructuredName", @@ -724,6 +732,7 @@ def load_mib(filenames): secsig_oids, nist_oids, thawte_oids, + pkcs3_oids, pkcs7_oids, pkcs9_oids, attributeType_oids, diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index f2d8613af37..9a5284bddfc 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -31,7 +31,6 @@ BER_tagging_enc, ) from scapy.base_classes import BasePacket -from scapy.compat import raw from scapy.volatile import ( GeneralizedTime, RandChoice, @@ -599,7 +598,7 @@ def build(self, pkt): elif val is None: s = b"" else: - s = b"".join(raw(i) for i in val) + s = b"".join(bytes(i) for i in val) return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -642,6 +641,9 @@ class ASN1F_TIME_TICKS(ASN1F_INTEGER): ############################# class ASN1F_optional(ASN1F_element): + """ + ASN.1 field that is optional. + """ def __init__(self, field): # type: (ASN1F_field[Any, Any]) -> None field.flexible_tag = False @@ -682,6 +684,20 @@ def i2repr(self, pkt, x): return self._field.i2repr(pkt, x) +class ASN1F_omit(ASN1F_field[None, None]): + """ + ASN.1 field that is not specified. This is simply omitted on the network. + This is different from ASN1F_NULL which has a network representation. + """ + def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[None, bytes] + return None, s + + def i2m(self, pkt, x): + # type: (ASN1_Packet, Optional[bytes]) -> bytes + return b"" + + _CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] @@ -769,7 +785,7 @@ def i2m(self, pkt, x): if x is None: s = b"" else: - s = raw(x) + s = bytes(x) if hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] s = BER_tagging_enc(s, @@ -852,11 +868,11 @@ def i2m(self, s = x elif isinstance(x, ASN1_Object): if x.val: - s = raw(x.val) + s = bytes(x.val) else: s = b"" else: - s = raw(x) + s = bytes(x) if not hasattr(x, "ASN1_root"): # A normal Packet (!= ASN1) return s @@ -897,7 +913,7 @@ def __init__(self, self.cls = cls super(ASN1F_BIT_STRING_ENCAPS, self).__init__( # type: ignore name, - default and raw(default), + default and bytes(default), context=context, implicit_tag=implicit_tag, explicit_tag=explicit_tag diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py index 28dfa6f97a0..7174e59993b 100644 --- a/scapy/layers/dcerpc.py +++ b/scapy/layers/dcerpc.py @@ -451,6 +451,14 @@ class RPC_C_AUTHN_LEVEL(IntEnum): DCE_C_AUTHN_LEVEL = RPC_C_AUTHN_LEVEL # C706 name +class RPC_C_IMP_LEVEL(IntEnum): + DEFAULT = 0x0 + ANONYMOUS = 0x1 + IDENTIFY = 0x2 + IMPERSONATE = 0x3 + DELEGATE = 0x4 + + # C706 sect 13.2.6.1 @@ -2766,9 +2774,9 @@ def __init__(self, *args, **kwargs): self.ssp = kwargs.pop("ssp", None) self.sspcontext = kwargs.pop("sspcontext", None) self.auth_level = kwargs.pop("auth_level", None) - self.auth_context_id = kwargs.pop("auth_context_id", 0) self.sent_cont_ids = [] self.cont_id = 0 # Currently selected context + self.auth_context_id = 0 # Currently selected authentication context self.map_callid_opnum = {} self.frags = collections.defaultdict(lambda: b"") self.sniffsspcontexts = {} # Unfinished contexts for passive @@ -3283,7 +3291,6 @@ def __init__(self, *args, **kwargs): self.session = DceRpcSession( ssp=kwargs.pop("ssp", None), auth_level=kwargs.pop("auth_level", None), - auth_context_id=kwargs.pop("auth_context_id", None), support_header_signing=kwargs.pop("support_header_signing", True), ) super(DceRpcSocket, self).__init__(*args, **kwargs) diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py index 547e09a4734..d14f2360f43 100644 --- a/scapy/layers/gssapi.py +++ b/scapy/layers/gssapi.py @@ -34,7 +34,7 @@ ASN1_Class_UNIVERSAL, ASN1_Codecs, ) -from scapy.asn1.ber import BERcodec_SEQUENCE +from scapy.asn1.ber import BERcodec_SEQUENCE, BER_id_dec from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1fields import ( ASN1F_OID, @@ -104,19 +104,25 @@ class GSSAPI_BLOB(ASN1_Packet): @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt and len(_pkt) >= 1: - if ord(_pkt[:1]) & 0xA0 >= 0xA0: + if _pkt[0] & 0xA0 >= 0xA0: from scapy.layers.spnego import SPNEGO_negToken # XXX: sometimes the token is raw, we should look from # the session what to use here. For now: hardcode SPNEGO # (THIS IS A VERY STRONG ASSUMPTION) return SPNEGO_negToken - if _pkt[:7] == b"NTLMSSP": + elif _pkt[:7] == b"NTLMSSP": from scapy.layers.ntlm import NTLM_Header # XXX: if no mechTypes are provided during SPNEGO exchange, # Windows falls back to a plain NTLM_Header. return NTLM_Header.dispatch_hook(_pkt=_pkt, *args, **kargs) + elif BER_id_dec(_pkt)[0] & 0x7F > 0x60: + from scapy.layers.kerberos import Kerberos + + # XXX: Heuristic to detect raw Kerberos packets, when Windows + # fallsback or when the parent data hasn't got any mechtype specified. + return Kerberos return cls @@ -454,7 +460,7 @@ class STATE(IntEnum): def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, @@ -468,7 +474,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -477,10 +483,21 @@ def GSS_Accept_sec_context( """ raise NotImplementedError + @abc.abstractmethod + def GSS_Inquire_names_for_mech(self) -> List[str]: + """ + Get the available OIDs for this mech, in order of preference. + """ + raise NotImplementedError + # Passive @abc.abstractmethod - def GSS_Passive(self, Context: CONTEXT, token=None): + def GSS_Passive( + self, + Context: CONTEXT, + input_token=None, + ): """ GSS_Passive: client/server call for the SSP in passive mode """ @@ -591,6 +608,9 @@ def GSS_GetMIC( message: bytes, qop_req: int = GSS_C_QOP_DEFAULT, ): + """ + See GSS_GetMICEx + """ return self.GSS_GetMICEx( Context, [ @@ -609,7 +629,10 @@ def GSS_VerifyMIC( Context: CONTEXT, message: bytes, signature, - ): + ) -> None: + """ + See GSS_VerifyMICEx + """ self.GSS_VerifyMICEx( Context, [ @@ -630,6 +653,9 @@ def GSS_Wrap( conf_req_flag: bool, qop_req: int = GSS_C_QOP_DEFAULT, ): + """ + See GSS_WrapEx + """ _msgs, signature = self.GSS_WrapEx( Context, [ @@ -647,7 +673,14 @@ def GSS_Wrap( # sect 2.3.4 - def GSS_Unwrap(self, Context: CONTEXT, signature): + def GSS_Unwrap( + self, + Context: CONTEXT, + signature, + ): + """ + See GSS_UnwrapEx + """ data = b"" if signature.payload: # signature has a payload that is the data. Let's get that payload @@ -679,19 +712,19 @@ def NegTokenInit2(self): """ return None, None - def canMechListMIC(self, Context: CONTEXT): + def SupportsMechListMIC(self): """ - Returns whether or not mechListMIC can be computed + Returns whether mechListMIC is supported or not """ - return False + return True - def getMechListMIC(self, Context, input): + def GetMechListMIC(self, Context, input): """ Compute mechListMIC """ - return bytes(self.GSS_GetMIC(Context, input)) + return self.GSS_GetMIC(Context, input) - def verifyMechListMIC(self, Context, otherMIC, input): + def VerifyMechListMIC(self, Context, otherMIC, input): """ Verify mechListMIC """ diff --git a/scapy/layers/http.py b/scapy/layers/http.py index 016337738fc..26fc3727eb0 100644 --- a/scapy/layers/http.py +++ b/scapy/layers/http.py @@ -763,6 +763,7 @@ class HTTP_Client(object): :param ssl: whether to use HTTPS or not :param ssp: the SSP object to use for binding :param no_check_certificate: with SSL, do not check the certificate + :param no_chan_bindings: force disable sending the channel bindings """ def __init__( @@ -772,6 +773,7 @@ def __init__( sslcontext=None, ssp=None, no_check_certificate=False, + no_chan_bindings=False, ): self.sock = None self._sockinfo = None @@ -781,6 +783,7 @@ def __init__( self.ssp = ssp self.sspcontext = None self.no_check_certificate = no_check_certificate + self.no_chan_bindings = no_chan_bindings self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS def _connect_or_reuse(self, host, port=None, tls=False, timeout=5): @@ -823,7 +826,7 @@ def _connect_or_reuse(self, host, port=None, tls=False, timeout=5): else: context = self.sslcontext sock = context.wrap_socket(sock, server_hostname=host) - if self.ssp: + if self.ssp and not self.no_chan_bindings: # Compute the channel binding token (CBT) self.chan_bindings = GssChannelBindings.fromssl( ChannelBindingType.TLS_SERVER_END_POINT, @@ -941,7 +944,7 @@ def request( # SPNEGO / Kerberos / NTLM self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - ssp_blob, + input_token=ssp_blob, target_name="http/" + host, req_flags=0, chan_bindings=self.chan_bindings, diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index c8a3320d6e7..a3125382ed5 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -63,11 +63,12 @@ ASN1_BIT_STRING, ASN1_BOOLEAN, ASN1_Class, + ASN1_Codecs, ASN1_GENERAL_STRING, ASN1_GENERALIZED_TIME, ASN1_INTEGER, + ASN1_OID, ASN1_STRING, - ASN1_Codecs, ) from scapy.asn1fields import ( ASN1F_BIT_STRING_ENCAPS, @@ -135,6 +136,7 @@ GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, GSS_S_DEFECTIVE_TOKEN, + GSS_S_DEFECTIVE_CREDENTIAL, GSS_S_FAILURE, GSS_S_FLAGS, GssChannelBindings, @@ -145,7 +147,20 @@ from scapy.layers.inet import TCP, UDP from scapy.layers.smb import _NV_VERSION from scapy.layers.smb2 import STATUS_ERREF -from scapy.layers.tls.cert import Cert, PrivKey +from scapy.layers.tls.cert import ( + Cert, + CertList, + CertTree, + CMS_Engine, + PrivKey, +) +from scapy.layers.tls.crypto.hash import ( + Hash_SHA, + Hash_SHA256, + Hash_SHA384, + Hash_SHA512, +) +from scapy.layers.tls.crypto.groups import _ffdh_groups from scapy.layers.x509 import ( _CMS_ENCAPSULATED, CMS_ContentInfo, @@ -154,17 +169,37 @@ X509_AlgorithmIdentifier, X509_DirectoryName, X509_SubjectPublicKeyInfo, + DomainParameters, ) # Redirect exports from RFC3961 try: from scapy.libs.rfc3961 import * # noqa: F401,F403 + from scapy.libs.rfc3961 import ( + _rfc1964pad, + ChecksumType, + Cipher, + decrepit_algorithms, + EncryptionType, + Hmac_MD5, + Key, + KRB_FX_CF2, + octetstring2key, + ) except ImportError: pass + +# Crypto imports +if conf.crypto_valid: + from cryptography.hazmat.primitives.serialization import pkcs12 + from cryptography.hazmat.primitives.asymmetric import dh + # Typing imports from typing import ( + List, Optional, + Union, ) @@ -356,6 +391,9 @@ def get_usage(self): elif isinstance(self.underlayer, KRB_AS_REP): # AS-REP encrypted part return 3, EncASRepPart + elif isinstance(self.underlayer, KRB_KDC_REQ_BODY): + # KDC-REQ enc-authorization-data + return 4, AuthorizationData elif isinstance(self.underlayer, KRB_AP_REQ) and isinstance( self.underlayer.underlayer, PADATA ): @@ -450,8 +488,6 @@ class EncryptionKey(ASN1_Packet): ) def toKey(self): - from scapy.libs.rfc3961 import Key - return Key( etype=self.keytype.val, key=self.keyvalue.val, @@ -519,7 +555,7 @@ def get_usage(self): def verify(self, key, text, key_usage_number=None): """ - Decrypt and return the data contained in cipher. + Verify a signature of text using a key. :param key: the key to use to check the checksum :param text: the bytes to verify @@ -532,7 +568,7 @@ def verify(self, key, text, key_usage_number=None): def make(self, key, text, key_usage_number=None, cksumtype=None): """ - Encrypt text and set it into cipher. + Make a signature. :param key: the key to use to make the checksum :param text: the bytes to make a checksum of @@ -950,9 +986,10 @@ class KERB_AD_RESTRICTION_ENTRY(ASN1_Packet): class KERB_AUTH_DATA_AP_OPTIONS(Packet): name = "KERB-AUTH-DATA-AP-OPTIONS" fields_desc = [ - LEIntEnumField( + FlagsField( "apOptions", 0x4000, + -32, { 0x4000: "KERB_AP_OPTIONS_CBT", 0x8000: "KERB_AP_OPTIONS_UNVERIFIED_TARGET_NAME", @@ -1248,7 +1285,7 @@ class PA_PK_AS_REQ(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "trustedCertifiers", - [ExternalPrincipalIdentifier()], + None, ExternalPrincipalIdentifier, explicit_tag=0xA1, ), @@ -1277,11 +1314,59 @@ class PAChecksum2(ASN1_Packet): ), ) + def verify(self, text): + """ + Verify a checksum of text. + + :param text: the bytes to verify + """ + # [MS-PKCA] 2.2.3 - PAChecksum2 + + # Only some OIDs are supported. Dumb but readable code. + oid = self.algorithmIdentifier.algorithm.val + if oid == "1.3.14.3.2.26": + hashcls = Hash_SHA + elif oid == "2.16.840.1.101.3.4.2.1": + hashcls = Hash_SHA256 + elif oid == "2.16.840.1.101.3.4.2.2": + hashcls = Hash_SHA384 + elif oid == "2.16.840.1.101.3.4.2.3": + hashcls = Hash_SHA512 + else: + raise ValueError("Bad PAChecksum2 checksum !") + + if hashcls().digest(text) != self.checksum.val: + raise ValueError("Bad PAChecksum2 checksum !") + + def make(self, text, h="sha256"): + """ + Make a checksum. + + :param text: the bytes to make a checksum of + """ + # Only some OIDs are supported. Dumb but readable code. + if h == "sha1": + hashcls = Hash_SHA + self.algorithmIdentifier.algorithm = ASN1_OID("1.3.14.3.2.26") + elif h == "sha256": + hashcls = Hash_SHA256 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.1") + elif h == "sha384": + hashcls = Hash_SHA384 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.2") + elif h == "sha512": + hashcls = Hash_SHA512 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.3") + else: + raise ValueError("Bad PAChecksum2 checksum !") + + self.checksum = ASN1_STRING(hashcls().digest(text)) + # still RFC 4556 sect 3.2.1 -class PKAuthenticator(ASN1_Packet): +class KRB_PKAuthenticator(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( Microseconds("cusec", 0, explicit_tag=0xA0), @@ -1292,14 +1377,34 @@ class PKAuthenticator(ASN1_Packet): ), # RFC8070 extension ASN1F_optional( - ASN1F_STRING("freshnessToken", "", explicit_tag=0xA4), + ASN1F_STRING("freshnessToken", None, explicit_tag=0xA4), ), # [MS-PKCA] sect 2.2.3 ASN1F_optional( - ASN1F_PACKET("paChecksum2", None, PAChecksum2, explicit_tag=0xA5), + ASN1F_PACKET("paChecksum2", PAChecksum2(), PAChecksum2, explicit_tag=0xA5), ), ) + def make_checksum(self, text, h="sha256"): + """ + Populate paChecksum and paChecksum2 + """ + # paChecksum (always sha-1) + self.paChecksum = ASN1_STRING(Hash_SHA().digest(text)) + + # paChecksum2 + self.paChecksum2 = PAChecksum2() + self.paChecksum2.make(text, h=h) + + def verify_checksum(self, text): + """ + Verify paChecksum and paChecksum2 + """ + if self.paChecksum.val != Hash_SHA().digest(text): + raise ValueError("Bad paChecksum checksum !") + + self.paChecksum2.verify(text) + # RFC8636 sect 6 @@ -1314,13 +1419,13 @@ class KDFAlgorithmId(ASN1_Packet): # still RFC 4556 sect 3.2.1 -class AuthPack(ASN1_Packet): +class KRB_AuthPack(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_PACKET( "pkAuthenticator", - PKAuthenticator(), - PKAuthenticator, + KRB_PKAuthenticator(), + KRB_PKAuthenticator, explicit_tag=0xA0, ), ASN1F_optional( @@ -1334,13 +1439,13 @@ class AuthPack(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "supportedCMSTypes", - [], + None, X509_AlgorithmIdentifier, explicit_tag=0xA2, ), ), ASN1F_optional( - ASN1F_STRING("clientDCNonce", None, explicit_tag=0xA3), + ASN1F_STRING("clientDHNonce", None, explicit_tag=0xA3), ), # RFC8636 extension ASN1F_optional( @@ -1349,7 +1454,7 @@ class AuthPack(ASN1_Packet): ) -_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = AuthPack +_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = KRB_AuthPack # sect 3.2.3 @@ -1709,6 +1814,12 @@ class KRB_AS_REP(ASN1_Packet): implicit_tag=ASN1_Class_KRB.AS_REP, ) + def getUPN(self): + return "%s@%s" % ( + self.cname.toString(), + self.crealm.val.decode(), + ) + class KRB_TGS_REP(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -2013,15 +2124,17 @@ def m2i(self, pkt, s): # 25: KDC_ERR_PREAUTH_REQUIRED # 36: KRB_AP_ERR_BADMATCH return MethodData(val[0].val, _underlayer=pkt), val[1] - elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 41, 60]: + elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 32, 41, 60, 62]: # 6: KDC_ERR_C_PRINCIPAL_UNKNOWN # 7: KDC_ERR_S_PRINCIPAL_UNKNOWN # 12: KDC_ERR_POLICY # 13: KDC_ERR_BADOPTION # 18: KDC_ERR_CLIENT_REVOKED # 29: KDC_ERR_SVC_UNAVAILABLE + # 32: KRB_AP_ERR_TKT_EXPIRED # 41: KRB_AP_ERR_MODIFIED # 60: KRB_ERR_GENERIC + # 62: KERB_ERR_TYPE_EXTENDED try: return KERB_ERROR_DATA(val[0].val, _underlayer=pkt), val[1] except BER_Decoding_Error: @@ -2112,9 +2225,10 @@ class KRB_ERROR(ASN1_Packet): 52: "KRB_ERR_RESPONSE_TOO_BIG", 60: "KRB_ERR_GENERIC", 61: "KRB_ERR_FIELD_TOOLONG", - 62: "KDC_ERROR_CLIENT_NOT_TRUSTED", - 63: "KDC_ERROR_KDC_NOT_TRUSTED", - 64: "KDC_ERROR_INVALID_SIG", + # RFC4556 + 62: "KDC_ERR_CLIENT_NOT_TRUSTED", + 63: "KDC_ERR_KDC_NOT_TRUSTED", + 64: "KDC_ERR_INVALID_SIG", 65: "KDC_ERR_KEY_TOO_WEAK", 66: "KDC_ERR_CERTIFICATE_MISMATCH", 67: "KRB_AP_ERR_NO_TGT", @@ -2127,6 +2241,11 @@ class KRB_ERROR(ASN1_Packet): 74: "KDC_ERR_REVOCATION_STATUS_UNAVAILABLE", 75: "KDC_ERR_CLIENT_NAME_MISMATCH", 76: "KDC_ERR_KDC_NAME_MISMATCH", + 77: "KDC_ERR_INCONSISTENT_KEY_PURPOSE", + 78: "KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED", + 79: "KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED", + 80: "KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED", + 81: "KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED", # draft-ietf-kitten-iakerb 85: "KRB_AP_ERR_IAKERB_KDC_NOT_FOUND", 86: "KRB_AP_ERR_IAKERB_KDC_NO_RESPONSE", @@ -2313,11 +2432,11 @@ class KRB_AuthenticatorChecksum(Packet): }, ), ConditionalField( - LEShortField("DlgOpt", 0), + LEShortField("DlgOpt", 1), lambda pkt: pkt.Flags.GSS_C_DELEG_FLAG, ), ConditionalField( - FieldLenField("Dlgth", None, length_of="Deleg"), + FieldLenField("Dlgth", None, length_of="Deleg", fmt="I", Context.SendSeqNum) tok = KRB_InnerToken( @@ -4683,13 +5008,6 @@ def MakeToSign(Confounder, DecText): msgs[0].data = Data return msgs elif Context.KrbSessionKey.etype in [23, 24]: # RC4 - from scapy.libs.rfc3961 import ( - Cipher, - Hmac_MD5, - _rfc1964pad, - decrepit_algorithms, - ) - # Drop wrapping tok = signature.innerToken @@ -4747,7 +5065,7 @@ def MakeToSign(Confounder, DecText): def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, @@ -4756,8 +5074,6 @@ def GSS_Init_sec_context( # New context Context = self.CONTEXT(IsAcceptor=False, req_flags=req_flags) - from scapy.libs.rfc3961 import Key - if Context.state == self.STATE.INIT and self.U2U: # U2U - Get TGT Context.state = self.STATE.CLI_SENT_TGTREQ @@ -4776,59 +5092,84 @@ def GSS_Init_sec_context( if Context.state in [self.STATE.INIT, self.STATE.CLI_SENT_TGTREQ]: if not self.UPN: raise ValueError("Missing UPN attribute") + # Do we have a ST? if self.ST is None: # Client sends an AP-req if not self.SPN and not target_name: raise ValueError("Missing SPN/target_name attribute") additional_tickets = [] + if self.U2U: try: # GSSAPI / Kerberos - tgt_rep = token.root.innerToken.root + tgt_rep = input_token.root.innerToken.root except AttributeError: try: # Kerberos - tgt_rep = token.innerToken.root + tgt_rep = input_token.innerToken.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN if not isinstance(tgt_rep, KRB_TGT_REP): tgt_rep.show() - raise ValueError("KerberosSSP: Unexpected token !") + raise ValueError("KerberosSSP: Unexpected input_token !") additional_tickets = [tgt_rep.ticket] - if self.TGT is not None: - if not self.KEY: - raise ValueError("Cannot use TGT without the KEY") - # Use TGT - res = krb_tgs_req( - upn=self.UPN, - spn=self.SPN or target_name, - ip=self.DC_IP, - sessionkey=self.KEY, - ticket=self.TGT, - additional_tickets=additional_tickets, - u2u=self.U2U, - debug=self.debug, - ) - else: - # Ask for TGT then ST - res = krb_as_and_tgs( + + if self.TGT is None: + # Get TGT. We were passed a kerberos key + res = krb_as_req( upn=self.UPN, - spn=self.SPN or target_name, ip=self.DC_IP, key=self.KEY, password=self.PASSWORD, - additional_tickets=additional_tickets, - u2u=self.U2U, debug=self.debug, + verbose=bool(self.debug), ) + if res is None: + # Failed to retrieve the ticket + return Context, None, GSS_S_FAILURE + + # Update UPN (could have been canonicalized) + self.UPN = res.upn + + # Store TGT, + self.TGT = res.asrep.ticket + self.TGTSessionKey = res.sessionkey + else: + # We have a TGT and were passed its key + self.TGTSessionKey = self.KEY + + # Get ST + if not self.TGTSessionKey: + raise ValueError("Cannot use TGT without the KEY") + + res = krb_tgs_req( + upn=self.UPN, + spn=self.SPN or target_name, + ip=self.DC_IP, + sessionkey=self.TGTSessionKey, + ticket=self.TGT, + additional_tickets=additional_tickets, + u2u=self.U2U, + debug=self.debug, + verbose=bool(self.debug), + ) if not res: # Failed to retrieve the ticket return Context, None, GSS_S_FAILURE - self.ST, self.KEY = res.tgsrep.ticket, res.sessionkey + + # Store the service ticket and associated key + self.ST, Context.STSessionKey = res.tgsrep.ticket, res.sessionkey elif not self.KEY: raise ValueError("Must provide KEY with ST") - Context.STSessionKey = self.KEY + else: + # We were passed a ST and its key + Context.STSessionKey = self.KEY + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + raise ValueError( + "Cannot use GSS_C_DELEG_FLAG when passed a service ticket !" + ) # Save ServerHostname if len(self.ST.sname.nameString) == 2: @@ -4860,25 +5201,47 @@ def GSS_Init_sec_context( # Get the realm of the client _, crealm = _parse_upn(self.UPN) + # Build the RFC4121 authenticator checksum + authenticator_checksum = KRB_AuthenticatorChecksum( + # RFC 4121 sect 4.1.1.2 + # "The Bnd field contains the MD5 hash of channel bindings" + Bnd=( + chan_bindings.digestMD5() + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else (b"\x00" * 16) + ), + Flags=int(Context.flags), + ) + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + # Delegate TGT + raise NotImplementedError("GSS_C_DELEG_FLAG is not implemented !") + # authenticator_checksum.Deleg = KRB_CRED( + # tickets=[self.TGT], + # encPart=EncryptedData() + # ) + # authenticator_checksum.encPart.encrypt( + # Context.STSessionKey, + # EncKrbCredPart( + # ticketInfo=KrbCredInfo( + # key=EncryptionKey.fromKey(self.TGTSessionKey), + # prealm=ASN1_GENERAL_STRING(crealm), + # pname=PrincipalName.fromUPN(self.UPN), + # # TODO: rework API to pass starttime... here. + # sreralm=self.TGT.realm, + # sname=self.TGT.sname, + # ) + # ) + # ) + # Build and encrypt the full KRB_Authenticator ap_req.authenticator.encrypt( Context.STSessionKey, KRB_Authenticator( crealm=crealm, cname=PrincipalName.fromUPN(self.UPN), - # RFC 4121 checksum cksum=Checksum( - cksumtype="KRB-AUTHENTICATOR", - checksum=KRB_AuthenticatorChecksum( - # RFC 4121 sect 4.1.1.2 - # "The Bnd field contains the MD5 hash of channel bindings" - Bnd=( - chan_bindings.digestMD5() - if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS - else (b"\x00" * 16) - ), - Flags=int(Context.flags), - ), + cksumtype="KRB-AUTHENTICATOR", checksum=authenticator_checksum ), ctime=ASN1_GENERALIZED_TIME(now_time), cusec=ASN1_INTEGER(0), @@ -4895,7 +5258,9 @@ def GSS_Init_sec_context( adData=KERB_AD_RESTRICTION_ENTRY( restriction=LSAP_TOKEN_INFO_INTEGRITY( MachineID=bytes(RandBin(32)), - PermanentMachineID=bytes(RandBin(32)), # noqa: E501 + PermanentMachineID=bytes( + RandBin(32) + ), ) ), ), @@ -4941,30 +5306,32 @@ def GSS_Init_sec_context( ) elif Context.state == self.STATE.CLI_SENT_APREQ: - if isinstance(token, KRB_AP_REP): + if isinstance(input_token, KRB_AP_REP): # Raw AP_REP was passed - ap_rep = token + ap_rep = input_token else: try: # GSSAPI / Kerberos - ap_rep = token.root.innerToken.root + ap_rep = input_token.root.innerToken.root except AttributeError: try: # Kerberos - ap_rep = token.innerToken.root + ap_rep = input_token.innerToken.root except AttributeError: try: # Raw kerberos DCE-STYLE - ap_rep = token.root + ap_rep = input_token.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN if not isinstance(ap_rep, KRB_AP_REP): return Context, None, GSS_S_DEFECTIVE_TOKEN + # Retrieve SessionKey repPart = ap_rep.encPart.decrypt(Context.STSessionKey) if repPart.subkey is not None: Context.SessionKey = repPart.subkey.keyvalue.val Context.KrbSessionKey = repPart.subkey.toKey() + # OK ! Context.state = self.STATE.CLI_RCVD_APREP if Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: @@ -4996,7 +5363,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -5004,7 +5371,6 @@ def GSS_Accept_sec_context( # New context Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) - from scapy.libs.rfc3961 import Key import scapy.layers.msrpce.mspac # noqa: F401 if Context.state == self.STATE.INIT: @@ -5023,21 +5389,21 @@ def GSS_Accept_sec_context( self.TGT, self.KEY = res.asrep.ticket, res.sessionkey # Server receives AP-req, sends AP-rep - if isinstance(token, KRB_AP_REQ): + if isinstance(input_token, KRB_AP_REQ): # Raw AP_REQ was passed - ap_req = token + ap_req = input_token else: try: # GSSAPI/Kerberos - ap_req = token.root.innerToken.root + ap_req = input_token.root.innerToken.root except AttributeError: try: # Kerberos - ap_req = token.innerToken.root + ap_req = input_token.innerToken.root except AttributeError: try: # Raw kerberos - ap_req = token.root + ap_req = input_token.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN @@ -5121,7 +5487,7 @@ def GSS_Accept_sec_context( ), ) ) - return Context, err, GSS_S_DEFECTIVE_TOKEN + return Context, err, GSS_S_DEFECTIVE_CREDENTIAL # Store information about the user in the Context if tkt.authorizationData and tkt.authorizationData.seq: @@ -5194,20 +5560,20 @@ def GSS_Accept_sec_context( # [MS-KILE] sect 3.4.5.1 # The server MUST receive the additional AP exchange reply message and # verify that the message is constructed correctly. - if not token: + if not input_token: return Context, None, GSS_S_DEFECTIVE_TOKEN # Server receives AP-req, sends AP-rep - if isinstance(token, KRB_AP_REP): + if isinstance(input_token, KRB_AP_REP): # Raw AP_REP was passed - ap_rep = token + ap_rep = input_token else: try: # GSSAPI/Kerberos - ap_rep = token.root.innerToken.root + ap_rep = input_token.root.innerToken.root except AttributeError: try: # Raw Kerberos - ap_rep = token.root + ap_rep = input_token.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN # Decrypt the AP-REP @@ -5223,7 +5589,7 @@ def GSS_Accept_sec_context( def GSS_Passive( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, ): if Context is None: @@ -5236,25 +5602,31 @@ def GSS_Passive( and req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE ): Context, _, status = self.GSS_Accept_sec_context( - Context, token, req_flags=req_flags + Context, + input_token=input_token, + req_flags=req_flags, ) if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: Context.state = self.STATE.CLI_SENT_APREQ else: Context.state = self.STATE.FAILED - return Context, status elif Context.state == self.STATE.CLI_SENT_APREQ: Context, _, status = self.GSS_Init_sec_context( - Context, token, req_flags=req_flags + Context, + input_token=input_token, + req_flags=req_flags, ) if status == GSS_S_COMPLETE: + if req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + status = GSS_S_CONTINUE_NEEDED Context.state = self.STATE.SRV_SENT_APREP else: Context.state == self.STATE.FAILED - return Context, status + else: + # Unknown state. Don't crash though. + status = GSS_S_FAILURE - # Unknown state. Don't crash though. - return Context, GSS_S_FAILURE + return Context, status def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): if Context.IsAcceptor is not IsAcceptor: @@ -5287,6 +5659,3 @@ def MaximumSignatureLength(self, Context: CONTEXT): raise NotImplementedError else: return 28 - - def canMechListMIC(self, Context: CONTEXT): - return bool(Context.KrbSessionKey) diff --git a/scapy/layers/ldap.py b/scapy/layers/ldap.py index c09e07e64c1..38651dacd45 100644 --- a/scapy/layers/ldap.py +++ b/scapy/layers/ldap.py @@ -2070,7 +2070,7 @@ def bind( # 3. Second exchange: Response self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - GSSAPI_BLOB(val), + input_token=GSSAPI_BLOB(val), target_name="ldap/" + self.host, chan_bindings=self.chan_bindings, ) @@ -2126,7 +2126,7 @@ def bind( break self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - GSSAPI_BLOB(val), + input_token=GSSAPI_BLOB(val), target_name="ldap/" + self.host, chan_bindings=self.chan_bindings, ) diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index fef1007e562..5933889c2d5 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -22,6 +22,7 @@ NL_AUTH_MESSAGE, NL_AUTH_SIGNATURE, ) +from scapy.layers.kerberos import KerberosSSP, _parse_upn from scapy.layers.gssapi import ( GSS_C_FLAGS, GSS_C_NO_CHANNEL_BINDINGS, @@ -29,8 +30,9 @@ GSS_S_CONTINUE_NEEDED, GSS_S_FAILURE, GSS_S_FLAGS, + SSP, ) -from scapy.layers.ntlm import RC4, RC4K, RC4Init, SSP +from scapy.layers.ntlm import RC4, RC4K, RC4Init, MD4le from scapy.layers.msrpce.rpcclient import ( DCERPC_Client, @@ -40,6 +42,8 @@ from scapy.layers.msrpce.raw.ms_nrpc import ( NetrServerAuthenticate3_Request, NetrServerAuthenticate3_Response, + NetrServerAuthenticateKerberos_Request, + NetrServerAuthenticateKerberos_Response, NetrServerReqChallenge_Request, NetrServerReqChallenge_Response, NETLOGON_SECURE_CHANNEL_TYPE, @@ -52,8 +56,14 @@ from cryptography.hazmat.primitives import hashes, hmac from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from scapy.libs.rfc3961 import DES + + try: + # cryptography > 47.0 + from cryptography.hazmat.decrepit.ciphers.modes import CFB8 + except ImportError: + from cryptography.hazmat.primitives.ciphers.modes import CFB8 else: - hashes = hmac = Cipher = algorithms = modes = DES = None + hashes = hmac = Cipher = algorithms = modes = DES = CFB8 = None # Typing imports @@ -114,15 +124,17 @@ 0x00200000: "RODC-passthrough", # W: Supports Advanced Encryption Standard (AES) encryption and SHA2 hashing. 0x01000000: "AES", - # Supports Kerberos as the security support provider for secure channel setup. - 0x20000000: "Kerberos", + # Not used. MUST be ignored on receipt. + 0x20000000: "X", # Y: Supports Secure RPC. 0x40000000: "SecureRPC", - # Not used. MUST be ignored on receipt. - 0x80000000: "Z", + # Supports Kerberos as the security support provider for secure channel setup. + 0x80000000: "Kerberos", } _negotiateFlags = FlagsField("", 0, -32, _negotiateFlags).names +# -- CRYPTO + # [MS-NRPC] sect 3.1.4.3.1 @crypto_validator @@ -150,7 +162,7 @@ def ComputeSessionKeyStrongKey(HashNt, ClientChallenge, ServerChallenge): # [MS-NRPC] sect 3.1.4.4.1 @crypto_validator def ComputeNetlogonCredentialAES(Input, Sk): - cipher = Cipher(algorithms.AES(Sk), mode=modes.CFB8(b"\x00" * 16)) + cipher = Cipher(algorithms.AES(Sk), mode=CFB8(b"\x00" * 16)) encryptor = cipher.encryptor() return encryptor.update(Input) @@ -281,6 +293,9 @@ def __init__(self, SessionKey, computername, domainname, AES=True, **kwargs): self.domainname = domainname super(NetlogonSSP, self).__init__(**kwargs) + def GSS_Inquire_names_for_mech(self): + raise NotImplementedError("Netlogon cannot be used with SPNEGO !") + def _secure(self, Context, msgs, Seal): """ Internal function used by GSS_WrapEx and GSS_GetMICEx @@ -336,7 +351,7 @@ def _secure(self, Context, msgs, Seal): if Context.AES: IV = SequenceNumber * 2 encryptor = Cipher( - algorithms.AES(EncryptionKey), mode=modes.CFB8(IV) + algorithms.AES(EncryptionKey), mode=CFB8(IV) ).encryptor() # Confounder signature.Confounder = encryptor.update(Confounder) @@ -363,7 +378,7 @@ def _secure(self, Context, msgs, Seal): if Context.AES: EncryptionKey = self.SessionKey IV = signature.Checksum * 2 - cipher = Cipher(algorithms.AES(EncryptionKey), mode=modes.CFB8(IV)) + cipher = Cipher(algorithms.AES(EncryptionKey), mode=CFB8(IV)) encryptor = cipher.encryptor() signature.SequenceNumber = encryptor.update(SequenceNumber) else: @@ -395,7 +410,7 @@ def _unsecure(self, Context, msgs, signature, Seal): if Context.AES: EncryptionKey = self.SessionKey IV = signature.Checksum * 2 - cipher = Cipher(algorithms.AES(EncryptionKey), mode=modes.CFB8(IV)) + cipher = Cipher(algorithms.AES(EncryptionKey), mode=CFB8(IV)) decryptor = cipher.decryptor() SequenceNumber = decryptor.update(signature.SequenceNumber) else: @@ -426,7 +441,7 @@ def _unsecure(self, Context, msgs, signature, Seal): if Context.AES: IV = SequenceNumber * 2 decryptor = Cipher( - algorithms.AES(EncryptionKey), mode=modes.CFB8(IV) + algorithms.AES(EncryptionKey), mode=CFB8(IV) ).decryptor() # Confounder Confounder = decryptor.update(signature.Confounder) @@ -477,7 +492,7 @@ def GSS_VerifyMICEx(self, Context, msgs, signature): def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, @@ -503,7 +518,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -569,8 +584,8 @@ class NetlogonClient(DCERPC_Client): >>> cli = NetlogonClient() >>> cli.connect_and_bind("192.168.0.100") >>> cli.establish_secure_channel( - ... domainname="DOMAIN", computername="WIN10", - ... HashNT=bytes.fromhex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ... UPN="WIN10@DOMAIN", + ... HASHNT=bytes.fromhex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), ... ) """ @@ -583,26 +598,25 @@ def __init__( **kwargs, ): self.interface = find_dcerpc_interface("logon") - self.ndr64 = False # Netlogon doesn't work with NDR64 self.SessionKey = None self.ClientStoredCredential = None self.supportAES = supportAES super(NetlogonClient, self).__init__( DCERPC_Transport.NCACN_IP_TCP, auth_level=auth_level, - ndr64=self.ndr64, verb=verb, **kwargs, ) - def connect_and_bind(self, remoteIP): + def connect(self, host, **kwargs): """ This calls DCERPC_Client's connect_and_bind to bind the 'logon' interface. """ - super(NetlogonClient, self).connect_and_bind(remoteIP, self.interface) - - def alter_context(self): - return super(NetlogonClient, self).alter_context(self.interface) + super(NetlogonClient, self).connect( + host=host, + interface=self.interface, + **kwargs, + ) def create_authenticator(self): """ @@ -653,53 +667,56 @@ def validate_authenticator(self, auth): def establish_secure_channel( self, - computername: str, - domainname: str, - HashNt: bytes, + UPN: str, + DC_FQDN: str, + HASHNT: Optional[bytes] = None, + PASSWORD: Optional[str] = None, + KEY=None, + ssp: Optional[KerberosSSP] = None, mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, secureChannelType=NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, ): """ Function to establish the Netlogon Secure Channel. - This uses NetrServerAuthenticate3 to negotiate the session key, then creates a - NetlogonSSP that uses that session key and alters the DCE/RPC session to use it. + This uses NetrServerAuthenticate3 or NetrServerAuthenticateKerberos to + negotiate the session key, then creates a NetlogonSSP that uses that session + key and alters the DCE/RPC session to use it. :param mode: one of NETLOGON_SECURE_CHANNEL_METHOD. This defines which method to use to establish the secure channel. - :param computername: the netbios computer account name that is used to establish - the secure channel. (e.g. WIN10) - :param domainname: the netbios domain name to connect to (e.g. DOMAIN) - :param HashNt: the HashNT of the computer account. + :param UPN: the UPN of the computer account name that is used to establish + the secure channel. (e.g. WIN10$@domain.local) + :param DC_FQDN: the FQDN name of the DC. + + The function then requires one of the following: + + :param HASHNT: the HashNT of the computer account (in Authenticate3 mode). + :param KEY: a Kerberos key to use (in Kerberos mode) + :param PASSWORD: the password of the computer account (any mode). + :param ssp: a KerberosSSP to use (in Kerberos mode) """ - # Flow documented in 3.1.4 Session-Key Negotiation - # and sect 3.4.5.2 for specific calls - clientChall = os.urandom(8) - - # Step 1: NetrServerReqChallenge - netr_server_req_chall_response = self.sr1_req( - NetrServerReqChallenge_Request( - PrimaryName=None, - ComputerName=computername, - ClientChallenge=PNETLOGON_CREDENTIAL( - data=clientChall, - ), - ndr64=self.ndr64, - ndrendian=self.ndrendian, - ) - ) - if ( - NetrServerReqChallenge_Response not in netr_server_req_chall_response - or netr_server_req_chall_response.status != 0 - ): - print( - conf.color_theme.fail( - "! %s" - % STATUS_ERREF.get(netr_server_req_chall_response.status, "Failure") + computername, domainname = _parse_upn(UPN) + # We need to normalize here, since the functions require both the accountname + # and the normal (no dollar) computer name. + if computername.endswith("$"): + computername = computername[:-1] + + if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: + if ssp or KEY: + raise ValueError("Cannot use 'ssp' on 'KEY' in Authenticate3 mode !") + if not HASHNT: + if PASSWORD: + HASHNT = MD4le(PASSWORD) + else: + raise ValueError("Missing either 'PASSWORD' or 'HASHNT' !") + if "." in domainname: + raise ValueError( + "The UPN in Authenticate3 must have a NETBIOS domain name !" ) - ) - netr_server_req_chall_response.show() - raise ValueError + else: + if HASHNT: + raise ValueError("Cannot use 'HASHNT' in Kerberos mode !") # Calc NegotiateFlags NegotiateFlags = FlagValue( @@ -712,23 +729,61 @@ def establish_secure_channel( # We are either using NetrServerAuthenticate3 or NetrServerAuthenticateKerberos if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: # We use the legacy NetrServerAuthenticate3 function (NetlogonSSP) - # Step 2: Build the session key + + # Make sure the interface is bound + if not self.bind_or_alter(self.interface): + raise ValueError("Bind failed !") + + # Flow documented in 3.1.4 Session-Key Negotiation + # and sect 3.4.5.2 for specific calls + clientChall = os.urandom(8) + + # Perform NetrServerReqChallenge request + netr_server_req_chall_response = self.sr1_req( + NetrServerReqChallenge_Request( + PrimaryName=None, + ComputerName=computername, + ClientChallenge=PNETLOGON_CREDENTIAL( + data=clientChall, + ), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerReqChallenge_Response not in netr_server_req_chall_response + or netr_server_req_chall_response.status != 0 + ): + print( + conf.color_theme.fail( + "! %s" + % STATUS_ERREF.get( + netr_server_req_chall_response.status, "Failure" + ) + ) + ) + netr_server_req_chall_response.show() + raise ValueError("NetrServerReqChallenge failed !") + + # Build the session key serverChall = netr_server_req_chall_response.ServerChallenge.data if self.supportAES: - SessionKey = ComputeSessionKeyAES(HashNt, clientChall, serverChall) + SessionKey = ComputeSessionKeyAES(HASHNT, clientChall, serverChall) self.ClientStoredCredential = ComputeNetlogonCredentialAES( clientChall, SessionKey ) else: SessionKey = ComputeSessionKeyStrongKey( - HashNt, clientChall, serverChall + HASHNT, clientChall, serverChall ) self.ClientStoredCredential = ComputeNetlogonCredentialDES( clientChall, SessionKey ) + + # Perform Authenticate3 request netr_server_auth3_response = self.sr1_req( NetrServerAuthenticate3_Request( - PrimaryName=None, + PrimaryName="\\\\" + DC_FQDN, AccountName=computername + "$", SecureChannelType=secureChannelType, ComputerName=computername, @@ -740,10 +795,7 @@ def establish_secure_channel( ndrendian=self.ndrendian, ) ) - if ( - NetrServerAuthenticate3_Response not in netr_server_auth3_response - or netr_server_auth3_response.status != 0 - ): + if netr_server_auth3_response.status != 0: # An error occurred. NegotiatedFlags = None if NetrServerAuthenticate3_Response in netr_server_auth3_response: @@ -758,20 +810,8 @@ def establish_secure_channel( % (NegotiatedFlags ^ NegotiateFlags) ) ) + raise ValueError("NetrServerAuthenticate3 failed !") - # Show the error - print( - conf.color_theme.fail( - "! %s" - % STATUS_ERREF.get(netr_server_auth3_response.status, "Failure") - ) - ) - - # If error is unknown, show the packet entirely - if netr_server_auth3_response.status not in STATUS_ERREF: - netr_server_auth3_response.show() - - raise ValueError # Check Server Credential if self.supportAES: if ( @@ -798,10 +838,48 @@ def establish_secure_channel( domainname=domainname, computername=computername, ) + + # Finally alter context (to use the SSP) + if not self.alter_context(self.interface): + raise ValueError("Bind failed !") + elif mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos: + # We use the brand new NetrServerAuthenticateKerberos function NegotiateFlags += "Kerberos" - # TODO - raise NotImplementedError - # Finally alter context (to use the SSP) - self.alter_context() + # Set KerberosSSP and alter context + if ssp: + self.ssp = self.sock.session.ssp = ssp + else: + self.ssp = self.sock.session.ssp = KerberosSSP( + UPN=UPN, + SPN="netlogon/" + DC_FQDN, + PASSWORD=PASSWORD, + KEY=KEY, + ) + if not self.bind_or_alter(self.interface): + raise ValueError("Bind failed !") + + # Send AuthenticateKerberos request + netr_server_authkerb_response = self.sr1_req( + NetrServerAuthenticateKerberos_Request( + PrimaryName="\\\\" + DC_FQDN, + AccountName=computername + "$", + AccountType=secureChannelType, + ComputerName=computername, + NegotiateFlags=int(NegotiateFlags), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerAuthenticateKerberos_Response + not in netr_server_authkerb_response + or netr_server_authkerb_response.status != 0 + ): + # An error occurred + netr_server_authkerb_response.show() + raise ValueError("NetrServerAuthenticateKerberos failed !") + + # The NRPC session key is in this case the kerberos one + self.SessionKey = self.sspcontext.SessionKey diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index a25f6587126..e3a1d46a435 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -41,6 +41,7 @@ find_dcerpc_interface, NDRContextHandle, NDRPointer, + RPC_C_IMP_LEVEL, ) from scapy.layers.gssapi import ( SSP, @@ -80,6 +81,7 @@ class DCERPC_Client(object): :param ndrendian: the endianness to use (default little) :param verb: enable verbose logging (default True) :param auth_level: the DCE_C_AUTHN_LEVEL to use + :param impersonation_type: the RPC_C_IMP_LEVEL to use """ def __init__( @@ -89,7 +91,7 @@ def __init__( ndrendian: str = "little", verb: bool = True, auth_level: Optional[DCE_C_AUTHN_LEVEL] = None, - auth_context_id: int = 0, + impersonation_type: RPC_C_IMP_LEVEL = RPC_C_IMP_LEVEL.DEFAULT, **kwargs, ): self.sock = None @@ -100,7 +102,8 @@ def __init__( # Counters self.call_id = 0 - self.all_cont_id = 0 # number of contexts sent + self.next_cont_id = 0 # next available context id + self.next_auth_contex_id = 0 # next available auth context id # Session parameters if ndr64 is None: @@ -118,8 +121,12 @@ def __init__( self.auth_level = DCE_C_AUTHN_LEVEL.CONNECT else: self.auth_level = DCE_C_AUTHN_LEVEL.NONE - self.auth_context_id = auth_context_id + if impersonation_type == RPC_C_IMP_LEVEL.DEFAULT: + # Same default as windows + impersonation_type = RPC_C_IMP_LEVEL.IDENTIFY + self.impersonation_type = impersonation_type self._first_time_on_interface = True + self.contexts = {} self.dcesockargs = kwargs self.dcesockargs["transport"] = self.transport @@ -135,7 +142,6 @@ def from_smblink(cls, smbcli, smb_kwargs={}, **kwargs): DceRpc5, ssp=client.ssp, auth_level=client.auth_level, - auth_context_id=client.auth_context_id, **client.dcesockargs, ) return client @@ -144,23 +150,71 @@ def from_smblink(cls, smbcli, smb_kwargs={}, **kwargs): def session(self) -> DceRpcSession: return self.sock.session - def connect(self, host, port=None, timeout=5, smb_kwargs={}): + def connect( + self, + host, + endpoint: Union[int, str] = None, + port: Optional[int] = None, + interface=None, + timeout=5, + smb_kwargs={}, + ): """ Initiate a connection. :param host: the host to connect to - :param port: (optional) the port to connect to + :param endpoint: (optional) the port/smb pipe to connect to + :param interface: (optional) if endpoint isn't provided, uses the endpoint + mapper to find the appropriate endpoint for that interface. :param timeout: (optional) the connection timeout (default 5) + :param port: (optional) the port to connect to. (useful for SMB) """ + if endpoint is None and interface is not None: + # Figure out the endpoint using the endpoint mapper + + if self.transport == DCERPC_Transport.NCACN_IP_TCP and port is None: + # IP/TCP + # ask the endpoint mapper (port 135) for the IP:PORT + endpoints = get_endpoint( + host, + interface, + ndrendian=self.ndrendian, + verb=self.verb, + ) + if endpoints: + _, endpoint = endpoints[0] + else: + raise ValueError( + "Could not find an available endpoint for that interface !" + ) + elif self.transport == DCERPC_Transport.NCACN_NP: + # SMB + # ask the endpoint mapper (over SMB) for the namedpipe + endpoints = get_endpoint( + host, + interface, + transport=self.transport, + ndrendian=self.ndrendian, + verb=self.verb, + smb_kwargs=smb_kwargs, + ) + if endpoints: + endpoint = endpoints[0].lstrip("\\pipe\\") + else: + return + + # Assign the default port if no port is provided if port is None: if self.transport == DCERPC_Transport.NCACN_IP_TCP: # IP/TCP - port = 135 + port = endpoint or 135 elif self.transport == DCERPC_Transport.NCACN_NP: # SMB port = 445 else: raise ValueError( "Can't guess the port for transport: %s" % self.transport ) + + # Start socket and connect self.host = host self.port = port sock = socket.socket() @@ -177,7 +231,12 @@ def connect(self, host, port=None, timeout=5, smb_kwargs={}): "\u2514 Connected from %s" % repr(sock.getsockname()) ) ) + if self.transport == DCERPC_Transport.NCACN_NP: # SMB + # If the endpoint is provided, connect to it. + if endpoint is not None: + self.open_smbpipe(endpoint) + # We pack the socket into a SMB_RPC_SOCKET sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock( sock, ssp=self.ssp, **smb_kwargs @@ -189,7 +248,6 @@ def connect(self, host, port=None, timeout=5, smb_kwargs={}): DceRpc5, ssp=self.ssp, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, **self.dcesockargs, ) @@ -347,10 +405,15 @@ def _get_bind_context(self, interface): """ Internal: get the bind DCE/RPC context. """ + if interface in self.contexts: + # We have already found acceptable contexts for this interface, + # reuse that. + return self.contexts[interface] + # NDR 2.0 contexts = [ DceRpc5Context( - cont_id=self.all_cont_id, + cont_id=self.next_cont_id, abstract_syntax=DceRpc5AbstractSyntax( if_uuid=interface.uuid, if_version=interface.if_version, @@ -364,13 +427,13 @@ def _get_bind_context(self, interface): ], ), ] - self.all_cont_id += 1 + self.next_cont_id += 1 # NDR64 if self.ndr64: contexts.append( DceRpc5Context( - cont_id=self.all_cont_id, + cont_id=self.next_cont_id, abstract_syntax=DceRpc5AbstractSyntax( if_uuid=interface.uuid, if_version=interface.if_version, @@ -384,12 +447,12 @@ def _get_bind_context(self, interface): ], ) ) - self.all_cont_id += 1 + self.next_cont_id += 1 # BindTimeFeatureNegotiationBitmask contexts.append( DceRpc5Context( - cont_id=self.all_cont_id, + cont_id=self.next_cont_id, abstract_syntax=DceRpc5AbstractSyntax( if_uuid=interface.uuid, if_version=interface.if_version, @@ -402,11 +465,28 @@ def _get_bind_context(self, interface): ], ) ) - self.all_cont_id += 1 + self.next_cont_id += 1 + + # Store contexts for this interface + self.contexts[interface] = contexts return contexts - def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls): + def _check_bind_context(self, interface, contexts) -> bool: + """ + Internal: check the answer DCE/RPC bind context, and update them. + """ + for i, ctx in enumerate(contexts): + if ctx.result == 0: + # Context was accepted. Remove all others from cache + self.contexts[interface] = [self.contexts[interface][i]] + return True + + return False + + def _bind( + self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls + ) -> bool: """ Internal: used to send a bind/alter request """ @@ -418,6 +498,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls + (" (with %s)" % self.ssp.__class__.__name__ if self.ssp else "") ) ) + # Do we need an authenticated bind if not self.ssp or ( self.sspcontext is not None @@ -452,13 +533,25 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY else 0 ) + | ( + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG + if self.impersonation_type <= RPC_C_IMP_LEVEL.IDENTIFY + else 0 + ) + | ( + GSS_C_FLAGS.GSS_C_DELEG_FLAG + if self.impersonation_type == RPC_C_IMP_LEVEL.DELEGATE + else 0 + ) ), target_name="host/" + self.host, ) + if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: # Authentication failed. self.sspcontext.clifailure() return False + resp = self.sr1( reqcls(context_elem=self._get_bind_context(interface)), auth_verifier=( @@ -467,7 +560,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls else CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, + auth_context_id=self.session.auth_context_id, auth_value=token, ) ), @@ -481,16 +574,21 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls ) ), ) - if respcls not in resp: + + # Check that the answer looks valid and contexts were accepted + if respcls not in resp or not self._check_bind_context( + interface, resp.results + ): token = None status = GSS_S_FAILURE else: # Call the underlying SSP self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - token=resp.auth_verifier.auth_value, + input_token=resp.auth_verifier.auth_value, target_name="host/" + self.host, ) + if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: # Authentication should continue, in two ways: # - through DceRpc5Auth3 (e.g. NTLM) @@ -503,7 +601,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls auth_verifier=CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, + auth_context_id=self.session.auth_context_id, auth_value=token, ), ) @@ -518,7 +616,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls auth_verifier=CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, + auth_context_id=self.session.auth_context_id, auth_value=token, ), ) @@ -530,22 +628,22 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls break self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - token=resp.auth_verifier.auth_value, + input_token=resp.auth_verifier.auth_value, target_name="host/" + self.host, ) else: log_runtime.error("GSS_Init_sec_context failed with %s !" % status) + # Check context acceptance if ( status == GSS_S_COMPLETE and respcls in resp - and any(x.result == 0 for x in resp.results[: int(self.ndr64) + 1]) + and self._check_bind_context(interface, resp.results) ): self.call_id = 0 # reset call id port = resp.sec_addr.port_spec.decode() ndr = self.session.ndr64 and "NDR64" or "NDR32" self.ndr64 = self.session.ndr64 - self.cont_id = int(self.session.ndr64) # ctx 0 for NDR32, 1 for NDR64 if self.verb: print( conf.color_theme.success( @@ -592,7 +690,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls resp.show() return False - def bind(self, interface: Union[DceRpcInterface, ComInterface]): + def bind(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: """ Bind the client to an interface @@ -600,7 +698,7 @@ def bind(self, interface: Union[DceRpcInterface, ComInterface]): """ return self._bind(interface, DceRpc5Bind, DceRpc5BindAck) - def alter_context(self, interface: Union[DceRpcInterface, ComInterface]): + def alter_context(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: """ Alter context: post-bind context negotiation @@ -608,7 +706,7 @@ def alter_context(self, interface: Union[DceRpcInterface, ComInterface]): """ return self._bind(interface, DceRpc5AlterContext, DceRpc5AlterContextResp) - def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]): + def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: """ Bind the client to an interface or alter the context if already bound @@ -616,10 +714,11 @@ def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]): """ if not self.session.rpc_bind_interface: # No interface is bound - self.bind(interface) + return self.bind(interface) elif self.session.rpc_bind_interface != interface: # An interface is already bound - self.alter_context(interface) + return self.alter_context(interface) + return True def open_smbpipe(self, name: str): """ @@ -640,7 +739,7 @@ def close_smbpipe(self): def connect_and_bind( self, - ip: str, + host: str, interface: DceRpcInterface, port: Optional[int] = None, timeout: int = 5, @@ -650,45 +749,20 @@ def connect_and_bind( Asks the Endpoint Mapper what address to use to connect to the interface, then uses connect() followed by a bind() - :param ip: the ip to connect to + :param host: the host to connect to :param interface: the DceRpcInterface object :param port: (optional, NCACN_NP only) the port to connect to :param timeout: (optional) the connection timeout (default 5) """ - if self.transport == DCERPC_Transport.NCACN_IP_TCP: - # IP/TCP - # 1. ask the endpoint mapper (port 135) for the IP:PORT - endpoints = get_endpoint( - ip, - interface, - ndrendian=self.ndrendian, - verb=self.verb, - ) - if endpoints: - ip, port = endpoints[0] - else: - return - # 2. Connect to that IP:PORT - self.connect(ip, port=port, timeout=timeout) - elif self.transport == DCERPC_Transport.NCACN_NP: - # SMB - # 1. ask the endpoint mapper (over SMB) for the namedpipe - endpoints = get_endpoint( - ip, - interface, - transport=self.transport, - ndrendian=self.ndrendian, - verb=self.verb, - smb_kwargs=smb_kwargs, - ) - if endpoints: - pipename = endpoints[0].lstrip("\\pipe\\") - else: - return - # 2. connect to the SMB server - self.connect(ip, port=port, timeout=timeout, smb_kwargs=smb_kwargs) - # 3. open the new named pipe - self.open_smbpipe(pipename) + # Connect to the interface using the endpoint mapper + self.connect( + host=host, + interface=interface, + port=port, + timeout=timeout, + smb_kwargs=smb_kwargs, + ) + # Bind in RPC self.bind(interface) @@ -861,15 +935,24 @@ def get_endpoint( """ client = DCERPC_Client( transport, + # EPM only works with NDR32 ndr64=False, ndrendian=ndrendian, verb=verb, ssp=ssp, - ) # EPM only works with NDR32 - client.connect(ip, smb_kwargs=smb_kwargs) - if transport == DCERPC_Transport.NCACN_NP: # SMB - client.open_smbpipe("epmapper") + ) + + if transport == DCERPC_Transport.NCACN_IP_TCP: + endpoint = 135 + elif transport == DCERPC_Transport.NCACN_NP: + endpoint = "epmapper" + else: + raise ValueError("Unknown transport value !") + + client.connect(ip, endpoint=endpoint, smb_kwargs=smb_kwargs) + client.bind(find_dcerpc_interface("ept")) endpoints = client.epm_map(interface) + client.close() return endpoints diff --git a/scapy/layers/msrpce/rpcserver.py b/scapy/layers/msrpce/rpcserver.py index 5a8435cac17..767d8f09c0c 100644 --- a/scapy/layers/msrpce/rpcserver.py +++ b/scapy/layers/msrpce/rpcserver.py @@ -377,9 +377,8 @@ def recv(self, data): print( conf.color_theme.success( f">> {cls.__name__} {self.session.rpc_bind_interface.name}" - f" is on port '{port_spec.decode()}' using " + ( - "NDR64" if self.ndr64 else "NDR32" - ) + f" is on port '{port_spec.decode()}' using " + + ("NDR64" if self.ndr64 else "NDR32") ) ) elif DceRpc5Request in req: diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 556faee0ea5..60258eb6be4 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -60,6 +60,8 @@ from scapy.sessions import StringBuffer from scapy.layers.gssapi import ( + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, GSS_C_FLAGS, GSS_C_NO_CHANNEL_BINDINGS, GSS_S_BAD_BINDINGS, @@ -70,8 +72,6 @@ GSS_S_FLAGS, GssChannelBindings, SSP, - _GSSAPI_OIDS, - _GSSAPI_SIGNATURE_OIDS, ) # Typing imports @@ -94,6 +94,18 @@ ########## +# NTLM structures are all in all very complicated. Many fields don't have a fixed +# position, but are rather referred to with an offset (from the beginning of the +# structure) and a length. In addition to that, there are variants of the structure +# with missing fields when running old versions of Windows (sometimes also seen when +# talking to products that reimplement NTLM, most notably backup applications). + +# We add `_NTLMPayloadField` and `_NTLMPayloadPacket` to parse fields that use an +# offset, and `_NTLM_post_build` to be able to rebuild those offsets. +# In addition, the `NTLM_VARIANT*` allows to select what flavor of NTLM to use +# (NT, XP, or Recent). But in real world use only Recent should be used. + + class _NTLMPayloadField(_StrField[List[Tuple[str, Any]]]): """Special field used to dissect NTLM payloads. This isn't trivial because the offsets are variable.""" @@ -396,6 +408,41 @@ def _NTLM_post_build(self, p, pay_offset, fields, config=_NTLM_CONFIG): ############## +# -- Util: VARIANT class + + +class NTLM_VARIANT(IntEnum): + """ + The message variant to use for NTLM. + """ + + NT_OR_2000 = 0 + XP_OR_2003 = 1 + RECENT = 2 + + +class _NTLM_VARIANT_Packet(_NTLMPayloadPacket): + def __init__(self, *args, **kwargs): + self.VARIANT = kwargs.pop("VARIANT", NTLM_VARIANT.RECENT) + super(_NTLM_VARIANT_Packet, self).__init__(*args, **kwargs) + + def clone_with(self, *args, **kwargs): + pkt = super(_NTLM_VARIANT_Packet, self).clone_with(*args, **kwargs) + pkt.VARIANT = self.VARIANT + return pkt + + def copy(self): + pkt = super(_NTLM_VARIANT_Packet, self).copy() + pkt.VARIANT = self.VARIANT + + return pkt + + def show2(self, dump=False, indent=3, lvl="", label_lvl=""): + return self.__class__(bytes(self), VARIANT=self.VARIANT).show( + dump, indent, lvl, label_lvl + ) + + # Sect 2.2 @@ -406,13 +453,17 @@ class NTLM_Header(Packet): LEIntEnumField( "MessageType", 3, - {1: "NEGOTIATE_MESSAGE", 2: "CHALLENGE_MESSAGE", 3: "AUTHENTICATE_MESSAGE"}, + { + 1: "NEGOTIATE_MESSAGE", + 2: "CHALLENGE_MESSAGE", + 3: "AUTHENTICATE_MESSAGE", + }, ), ] @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): - if _pkt and len(_pkt) >= 10: + if cls is NTLM_Header and _pkt and len(_pkt) >= 10: MessageType = struct.unpack(" 32) and 40 or 32) + OFFSET = lambda pkt: ( + 32 + if ( + pkt.VARIANT == NTLM_VARIANT.NT_OR_2000 + or (pkt.DomainNameBufferOffset or 40) <= 32 + ) + else 40 + ) fields_desc = ( [ NTLM_Header, @@ -510,15 +569,18 @@ class NTLM_NEGOTIATE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) x, - lambda pkt: ( + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.XP_OR_2003 + and ( ( - 40 - if pkt.DomainNameBufferOffset is None - else pkt.DomainNameBufferOffset or len(pkt.original or b"") + ( + 40 + if pkt.DomainNameBufferOffset is None + else pkt.DomainNameBufferOffset or len(pkt.original or b"") + ) + > 32 ) - > 32 - ) - or pkt.fields.get(x.name, b""), + or pkt.fields.get(x.name, b"") + ), ) for x in _NTLM_Version.fields_desc ] @@ -556,12 +618,42 @@ def post_build(self, pkt, pay): class Single_Host_Data(Packet): fields_desc = [ - LEIntField("Size", 48), + LEIntField("Size", None), LEIntField("Z4", 0), - XStrFixedLenField("CustomData", b"", length=8), + # "CustomData" guessed using LSAP_TOKEN_INFO_INTEGRITY. + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "UAC-Restricted", + }, + ), + LEIntEnumField( + "TokenIL", + 0x00002000, + { + 0x00000000: "Untrusted", + 0x00001000: "Low", + 0x00002000: "Medium", + 0x00003000: "High", + 0x00004000: "System", + 0x00005000: "Protected process", + }, + ), XStrFixedLenField("MachineID", b"", length=32), + # KB 5068222 - still waiting for [MS-KILE] update (oct. 2025) + ConditionalField( + XStrFixedLenField("PermanentMachineID", None, length=32), + lambda pkt: pkt.Size is None or pkt.Size > 48, + ), ] + def post_build(self, pkt, pay): + if self.Size is None: + pkt = struct.pack(" 48) and 56 or 48) + OFFSET = lambda pkt: ( + 48 + if ( + pkt.VARIANT == NTLM_VARIANT.NT_OR_2000 + or (pkt.TargetInfoBufferOffset or 56) <= 48 + ) + else 56 + ) fields_desc = ( [ NTLM_Header, @@ -653,8 +753,11 @@ class NTLM_CHALLENGE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) x, - lambda pkt: ((pkt.TargetInfoBufferOffset or 56) > 40) - or pkt.fields.get(x.name, b""), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.XP_OR_2003 + and ( + ((pkt.TargetInfoBufferOffset or 56) > 40) + or pkt.fields.get(x.name, b"") + ), ) for x in _NTLM_Version.fields_desc ] @@ -770,14 +873,23 @@ def computeNTProofStr(self, ResponseKeyNT, ServerChallenge): return HMAC_MD5(ResponseKeyNT, ServerChallenge + temp) -class NTLM_AUTHENTICATE(_NTLMPayloadPacket): +class NTLM_AUTHENTICATE(_NTLM_VARIANT_Packet, NTLM_Header): name = "NTLM Authenticate" + __slots__ = ["VARIANT"] MessageType = 3 NTLM_VERSION = 1 OFFSET = lambda pkt: ( - ((pkt.DomainNameBufferOffset or 88) <= 64) - and 64 - or (((pkt.DomainNameBufferOffset or 88) > 72) and 88 or 72) + 64 + if ( + pkt.VARIANT == NTLM_VARIANT.NT_OR_2000 + or (pkt.DomainNameBufferOffset or 88) <= 64 + ) + else ( + 72 + if pkt.VARIANT == NTLM_VARIANT.XP_OR_2003 + or ((pkt.DomainNameBufferOffset or 88) <= 72) + else 88 + ) ) fields_desc = ( [ @@ -814,8 +926,11 @@ class NTLM_AUTHENTICATE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) x, - lambda pkt: ((pkt.DomainNameBufferOffset or 88) > 64) - or pkt.fields.get(x.name, b""), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.XP_OR_2003 + and ( + ((pkt.DomainNameBufferOffset or 88) > 64) + or pkt.fields.get(x.name, b"") + ), ) for x in _NTLM_Version.fields_desc ] @@ -824,8 +939,11 @@ class NTLM_AUTHENTICATE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) XStrFixedLenField("MIC", b"", length=16), - lambda pkt: ((pkt.DomainNameBufferOffset or 88) > 72) - or pkt.fields.get("MIC", b""), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.RECENT + and ( + ((pkt.DomainNameBufferOffset or 88) > 72) + or pkt.fields.get("MIC", b"") + ), ), # Payload _NTLMPayloadField( @@ -1190,7 +1308,6 @@ class NTLMSSP(SSP): authenticates inbound users. """ - oid = "1.3.6.1.4.1.311.2.2.10" auth_type = 0x0A class STATE(SSP.STATE): @@ -1215,6 +1332,7 @@ class CONTEXT(SSP.CONTEXT): "neg_tok", "chall_tok", "ServerHostname", + "ServerDomain", ] def __init__(self, IsAcceptor, req_flags=None): @@ -1232,6 +1350,7 @@ def __init__(self, IsAcceptor, req_flags=None): self.neg_tok = None self.chall_tok = None self.ServerHostname = None + self.ServerDomain = None self.IsAcceptor = IsAcceptor super(NTLMSSP.CONTEXT, self).__init__(req_flags=req_flags) @@ -1241,12 +1360,16 @@ def clifailure(self): def __repr__(self): return "NTLMSSP" + # [MS-NLMP] note <36>: "the maximum lifetime is 36 hours" (lol, Kerberos has 5min) + NTLM_MaxLifetime = 36 * 3600 + def __init__( self, UPN=None, HASHNT=None, PASSWORD=None, USE_MIC=True, + VARIANT: NTLM_VARIANT = NTLM_VARIANT.RECENT, NTLM_VALUES={}, DOMAIN_FQDN=None, DOMAIN_NB_NAME=None, @@ -1261,9 +1384,17 @@ def __init__( if HASHNT is None and PASSWORD is not None: HASHNT = MD4le(PASSWORD) self.HASHNT = HASHNT - self.USE_MIC = USE_MIC + self.VARIANT = VARIANT + if self.VARIANT != NTLM_VARIANT.RECENT: + log_runtime.warning( + "VARIANT != NTLM_VARIANT.RECENT. You shouldn't touch this !" + ) + self.USE_MIC = False + else: + self.USE_MIC = USE_MIC self.NTLM_VALUES = NTLM_VALUES if UPN is not None: + # Populate values used only in server mode. from scapy.layers.kerberos import _parse_upn try: @@ -1274,14 +1405,17 @@ def __init__( COMPUTER_NB_NAME = user except ValueError: pass + + # Compute various netbios/fqdn names self.DOMAIN_FQDN = DOMAIN_FQDN or "domain.local" self.DOMAIN_NB_NAME = ( DOMAIN_NB_NAME or self.DOMAIN_FQDN.split(".")[0].upper()[:15] ) - self.COMPUTER_NB_NAME = COMPUTER_NB_NAME or "SRV" + self.COMPUTER_NB_NAME = COMPUTER_NB_NAME or "WIN10" self.COMPUTER_FQDN = COMPUTER_FQDN or ( self.COMPUTER_NB_NAME.lower() + "." + self.DOMAIN_FQDN ) + self.IDENTITIES = IDENTITIES self.DO_NOT_CHECK_LOGIN = DO_NOT_CHECK_LOGIN self.SERVER_CHALLENGE = SERVER_CHALLENGE @@ -1290,6 +1424,9 @@ def __init__( def LegsAmount(self, Context: CONTEXT): return 3 + def GSS_Inquire_names_for_mech(self): + return ["1.3.6.1.4.1.311.2.2.10"] + def GSS_GetMICEx(self, Context, msgs, qop_req=0): """ [MS-NLMP] sect 3.4.8 @@ -1349,18 +1486,18 @@ def GSS_UnwrapEx(self, Context, msgs, signature): self.GSS_VerifyMICEx(Context, msgs, signature) return msgs - def canMechListMIC(self, Context): + def SupportsMechListMIC(self): if not self.USE_MIC: # RFC 4178 # "If the mechanism selected by the negotiation does not support integrity # protection, then no mechlistMIC token is used." return False - if not Context or not Context.SessionKey: - # Not available yet + if self.DO_NOT_CHECK_LOGIN: + # In this mode, we won't negotiate any credentials. return False return True - def getMechListMIC(self, Context, input): + def GetMechListMIC(self, Context, input): # [MS-SPNG] # "When NTLM is negotiated, the SPNG server MUST set OriginalHandle to # ServerHandle before generating the mechListMIC, then set ServerHandle to @@ -1368,11 +1505,11 @@ def getMechListMIC(self, Context, input): OriginalHandle = Context.SendSealHandle Context.SendSealHandle = RC4Init(Context.SendSealKey) try: - return super(NTLMSSP, self).getMechListMIC(Context, input) + return super(NTLMSSP, self).GetMechListMIC(Context, input) finally: Context.SendSealHandle = OriginalHandle - def verifyMechListMIC(self, Context, otherMIC, input): + def VerifyMechListMIC(self, Context, otherMIC, input): # [MS-SPNG] # "the SPNEGO Extension server MUST set OriginalHandle to ClientHandle before # validating the mechListMIC and then set ClientHandle to OriginalHandle after @@ -1380,14 +1517,14 @@ def verifyMechListMIC(self, Context, otherMIC, input): OriginalHandle = Context.RecvSealHandle Context.RecvSealHandle = RC4Init(Context.RecvSealKey) try: - return super(NTLMSSP, self).verifyMechListMIC(Context, otherMIC, input) + return super(NTLMSSP, self).VerifyMechListMIC(Context, otherMIC, input) finally: Context.RecvSealHandle = OriginalHandle def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, @@ -1399,6 +1536,7 @@ def GSS_Init_sec_context( # Client: negotiate # Create a default token tok = NTLM_NEGOTIATE( + VARIANT=self.VARIANT, NegotiateFlags="+".join( [ "NEGOTIATE_UNICODE", @@ -1408,10 +1546,14 @@ def GSS_Init_sec_context( "TARGET_TYPE_DOMAIN", "NEGOTIATE_EXTENDED_SESSIONSECURITY", "NEGOTIATE_TARGET_INFO", - "NEGOTIATE_VERSION", "NEGOTIATE_128", "NEGOTIATE_56", ] + + ( + ["NEGOTIATE_VERSION"] + if self.VARIANT >= NTLM_VARIANT.XP_OR_2003 + else [] + ) + ( [ "NEGOTIATE_KEY_EXCH", @@ -1455,54 +1597,79 @@ def GSS_Init_sec_context( return Context, tok, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.CLI_SENT_NEGO: # Client: auth (token=challenge) - chall_tok = token + chall_tok = input_token if self.UPN is None or self.HASHNT is None: raise ValueError( "Must provide a 'UPN' and a 'HASHNT' or 'PASSWORD' when " "running in standalone !" ) + + from scapy.layers.kerberos import _parse_upn + + # Check token sanity if not chall_tok or NTLM_CHALLENGE not in chall_tok: log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Challenge") return Context, None, GSS_S_DEFECTIVE_TOKEN - # Take a default token + + # Some information from the CHALLENGE are stored + try: + Context.ServerHostname = chall_tok.getAv(0x0001).Value + except IndexError: + pass + try: + Context.ServerDomain = chall_tok.getAv(0x0002).Value + except IndexError: + pass + try: + # the server SHOULD set the timestamp in the CHALLENGE_MESSAGE + ServerTimestamp = chall_tok.getAv(0x0007).Value + ServerTime = (ServerTimestamp / 1e7) - 11644473600 + + if abs(ServerTime - time.time()) >= NTLMSSP.NTLM_MaxLifetime: + log_runtime.warning( + "Server and Client times are off by more than 36h !" + ) + # We could error here, but we don't. + except IndexError: + pass + + # Initialize a default token tok = NTLM_AUTHENTICATE_V2( + VARIANT=self.VARIANT, NegotiateFlags=chall_tok.NegotiateFlags, ProductMajorVersion=10, ProductMinorVersion=0, ProductBuild=19041, ) tok.LmChallengeResponse = LMv2_RESPONSE() - from scapy.layers.kerberos import _parse_upn + # Populate the token + # 1. Set username try: tok.UserName, realm = _parse_upn(self.UPN) except ValueError: - tok.UserName, realm = self.UPN, None + tok.UserName, realm = self.UPN, Context.ServerDomain + + # 2. Set domain name if realm is None: - try: - tok.DomainName = chall_tok.getAv(0x0002).Value - except IndexError: - log_runtime.warning( - "No realm specified in UPN, nor provided by server" - ) - tok.DomainName = self.DOMAIN_NB_NAME.encode() + log_runtime.warning( + "No realm specified in UPN, nor provided by server." + ) + tok.DomainName = self.DOMAIN_FQDN else: tok.DomainName = realm - try: - tok.Workstation = Context.ServerHostname = chall_tok.getAv( - 0x0001 - ).Value # noqa: E501 - except IndexError: - tok.Workstation = "WIN" + + # 3. Set workstation name + tok.Workstation = self.COMPUTER_NB_NAME + + # 4. Create and calculate the ChallengeResponse + # 4.1 Build the payload cr = tok.NtChallengeResponse = NTLMv2_RESPONSE( ChallengeFromClient=os.urandom(8), ) - try: - # the server SHOULD set the timestamp in the CHALLENGE_MESSAGE - cr.TimeStamp = chall_tok.getAv(0x0007).Value - except IndexError: - cr.TimeStamp = int((time.time() + 11644473600) * 1e7) + cr.TimeStamp = int((time.time() + 11644473600) * 1e7) cr.AvPairs = ( + # Repeat AvPairs from the server chall_tok.TargetInfo[:-1] + ( [ @@ -1530,7 +1697,10 @@ def GSS_Init_sec_context( else [] ) + [ - AV_PAIR(AvId="MsvAvTargetName", Value="host/" + tok.Workstation), + AV_PAIR( + AvId="MsvAvTargetName", + Value=target_name or ("host/" + Context.ServerHostname), + ), AV_PAIR(AvId="MsvAvEOL"), ] ) @@ -1544,19 +1714,22 @@ def GSS_Init_sec_context( ]: if key in self.NTLM_VALUES: setattr(tok, key, self.NTLM_VALUES[key]) - # Compute the ResponseKeyNT + + # 4.2 Compute the ResponseKeyNT ResponseKeyNT = NTOWFv2( None, tok.UserName, tok.DomainName, HashNt=self.HASHNT, ) - # Compute the NTProofStr + + # 4.3 Compute the NTProofStr cr.NTProofStr = cr.computeNTProofStr( ResponseKeyNT, chall_tok.ServerChallenge, ) - # Compute the Session Key + + # 4.4 Compute the Session Key SessionBaseKey = NTLMv2_ComputeSessionBaseKey(ResponseKeyNT, cr.NTProofStr) KeyExchangeKey = SessionBaseKey # Only true for NTLMv2 if chall_tok.NegotiateFlags.NEGOTIATE_KEY_EXCH: @@ -1567,8 +1740,12 @@ def GSS_Init_sec_context( ) else: ExportedSessionKey = KeyExchangeKey + + # 4.5 Compute the MIC if self.USE_MIC: tok.compute_mic(ExportedSessionKey, Context.neg_tok, chall_tok) + + # 5. Perform key computations Context.ExportedSessionKey = ExportedSessionKey # [MS-SMB] 3.2.5.3 Context.SessionKey = Context.ExportedSessionKey @@ -1587,12 +1764,15 @@ def GSS_Init_sec_context( tok.NegotiateFlags, ExportedSessionKey, "Server" ) Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + + # Update the state Context.state = self.STATE.CLI_SENT_AUTH + return Context, tok, GSS_S_COMPLETE elif Context.state == self.STATE.CLI_SENT_AUTH: - if token: + if input_token: # what is that? - status = GSS_S_DEFECTIVE_CREDENTIAL + status = GSS_S_DEFECTIVE_TOKEN else: status = GSS_S_COMPLETE return Context, None, status @@ -1602,7 +1782,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -1610,14 +1790,16 @@ def GSS_Accept_sec_context( Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) if Context.state == self.STATE.INIT: - # Server: challenge (token=negotiate) - nego_tok = token + # Server: challenge (input_token=negotiate) + nego_tok = input_token if not nego_tok or NTLM_NEGOTIATE not in nego_tok: log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Negotiate") return Context, None, GSS_S_DEFECTIVE_TOKEN - # Take a default token + + # Build the challenge token currentTime = (time.time() + 11644473600) * 1e7 tok = NTLM_CHALLENGE( + VARIANT=self.VARIANT, ServerChallenge=self.SERVER_CHALLENGE or os.urandom(8), NegotiateFlags="+".join( [ @@ -1628,11 +1810,15 @@ def GSS_Accept_sec_context( "NEGOTIATE_EXTENDED_SESSIONSECURITY", "NEGOTIATE_TARGET_INFO", "TARGET_TYPE_DOMAIN", - "NEGOTIATE_VERSION", "NEGOTIATE_128", "NEGOTIATE_KEY_EXCH", "NEGOTIATE_56", ] + + ( + ["NEGOTIATE_VERSION"] + if self.VARIANT >= NTLM_VARIANT.XP_OR_2003 + else [] + ) + ( ["NEGOTIATE_SIGN"] if nego_tok.NegotiateFlags.NEGOTIATE_SIGN @@ -1696,12 +1882,17 @@ def GSS_Accept_sec_context( if ((x in self.NTLM_VALUES) or (i in avpairs)) and self.NTLM_VALUES.get(x, True) is not None ] + + # Store for next step Context.chall_tok = tok + + # Update the state Context.state = self.STATE.SRV_SENT_CHAL + return Context, tok, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.SRV_SENT_CHAL: - # server: OK or challenge again (token=auth) - auth_tok = token + # server: OK or challenge again (input_token=auth) + auth_tok = input_token if not auth_tok or NTLM_AUTHENTICATE_V2 not in auth_tok: log_runtime.debug( @@ -1710,7 +1901,7 @@ def GSS_Accept_sec_context( return Context, None, GSS_S_DEFECTIVE_TOKEN if self.DO_NOT_CHECK_LOGIN: - # Just trust me bro + # Just trust me bro. Typically used in "guest" mode. return Context, None, GSS_S_COMPLETE # Compute the session key @@ -1719,12 +1910,12 @@ def GSS_Accept_sec_context( # [MS-NLMP] sect 3.2.5.1.2 KeyExchangeKey = SessionBaseKey # Only true for NTLMv2 if auth_tok.NegotiateFlags.NEGOTIATE_KEY_EXCH: - if not auth_tok.EncryptedRandomSessionKeyLen: + try: + EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey + except AttributeError: # No EncryptedRandomSessionKey. libcurl for instance # hmm. this looks bad EncryptedRandomSessionKey = b"\x00" * 16 - else: - EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey ExportedSessionKey = RC4K(KeyExchangeKey, EncryptedRandomSessionKey) else: ExportedSessionKey = KeyExchangeKey @@ -1732,6 +1923,19 @@ def GSS_Accept_sec_context( # [MS-SMB] 3.2.5.3 Context.SessionKey = Context.ExportedSessionKey + # Check the timestamp + try: + ClientTimestamp = auth_tok.NtChallengeResponse.getAv(0x0007).Value + ClientTime = (ClientTimestamp / 1e7) - 11644473600 + + if abs(ClientTime - time.time()) >= NTLMSSP.NTLM_MaxLifetime: + log_runtime.warning( + "Server and Client times are off by more than 36h !" + ) + # We could error here, but we don't. + except IndexError: + pass + # Check the channel bindings if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: try: @@ -1744,7 +1948,6 @@ def GSS_Accept_sec_context( # Uhoh, we required channel bindings return Context, None, GSS_S_BAD_BINDINGS - # Check the NTProofStr if Context.SessionKey: # Compute NTLM keys Context.SendSignKey = SIGNKEY( @@ -1761,6 +1964,8 @@ def GSS_Accept_sec_context( auth_tok.NegotiateFlags, ExportedSessionKey, "Client" ) Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + + # Check the NTProofStr if self._checkLogin(Context, auth_tok): # Set negotiated flags if auth_tok.NegotiateFlags.NEGOTIATE_SIGN: @@ -1842,20 +2047,24 @@ def _getSessionBaseKey(self, Context, auth_tok): """ Function that returns the SessionBaseKey from the ntlm Authenticate. """ - if auth_tok.UserNameLen: + try: username = auth_tok.UserName - else: + except AttributeError: username = None - if auth_tok.DomainNameLen: + try: domain = auth_tok.DomainName - else: + except AttributeError: domain = "" if self.IDENTITIES and username in self.IDENTITIES: ResponseKeyNT = NTOWFv2( - None, username, domain, HashNt=self.IDENTITIES[username] + None, + username, + domain, + HashNt=self.IDENTITIES[username], ) return NTLMv2_ComputeSessionBaseKey( - ResponseKeyNT, auth_tok.NtChallengeResponse.NTProofStr + ResponseKeyNT, + auth_tok.NtChallengeResponse.NTProofStr, ) elif self.IDENTITIES: log_runtime.debug("NTLMSSP: Bad credentials for %s" % username) @@ -1868,17 +2077,20 @@ def _checkLogin(self, Context, auth_tok): Overwrite and return True to bypass. """ # Create the NTLM AUTH - if auth_tok.UserNameLen: + try: username = auth_tok.UserName - else: + except AttributeError: username = None - if auth_tok.DomainNameLen: + try: domain = auth_tok.DomainName - else: + except AttributeError: domain = "" if username in self.IDENTITIES: ResponseKeyNT = NTOWFv2( - None, username, domain, HashNt=self.IDENTITIES[username] + None, + username, + domain, + HashNt=self.IDENTITIES[username], ) NTProofStr = auth_tok.NtChallengeResponse.computeNTProofStr( ResponseKeyNT, @@ -1898,26 +2110,41 @@ class NTLMSSP_DOMAIN(NTLMSSP): mode: :param UPN: the UPN of the machine account to login for Netlogon. - :param HASHNT: the HASHNT of the machine account to use for Netlogon. - :param PASSWORD: the PASSWORD of the machine acconut to use for Netlogon. + :param HASHNT: the HASHNT of the machine account (use Netlogon secure channel). + :param ssp: a KerberosSSP to use (use Kerberos secure channel). + :param PASSWORD: the PASSWORD of the machine account to use for Netlogon. :param DC_IP: (optional) specify the IP of the DC. - Examples:: + Netlogon example:: >>> mySSP = NTLMSSP_DOMAIN( ... UPN="Server1@domain.local", ... HASHNT=bytes.fromhex("8846f7eaee8fb117ad06bdd830b7586c"), ... ) + + Kerberos example:: + + >>> mySSP = NTLMSSP_DOMAIN( + ... UPN="Server1@domain.local", + ... KEY=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, + ... key=bytes.fromhex( + ... "85abb9b61dc2fa49d4cc04317bbd108f8f79df28" + ... "239155ed7b144c5d2ebcf016" + ... ) + ... ), + ... ) """ - def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): + def __init__(self, UPN=None, *args, timeout=3, ssp=None, **kwargs): from scapy.layers.kerberos import KerberosSSP - # UPN is mandatory - kwargs["UPN"] = UPN - # Either PASSWORD or HASHNT or ssp - if "HASHNT" not in kwargs and "PASSWORD" not in kwargs and ssp is None: + if ( + "HASHNT" not in kwargs + and "PASSWORD" not in kwargs + and "KEY" not in kwargs + and ssp is None + ): raise ValueError( "Must specify either 'HASHNT', 'PASSWORD' or " "provide a ssp=KerberosSSP()" @@ -1925,6 +2152,16 @@ def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): elif ssp is not None and not isinstance(ssp, KerberosSSP): raise ValueError("'ssp' can only be None or a KerberosSSP !") + self.KEY = kwargs.pop("KEY", None) + self.PASSWORD = kwargs.get("PASSWORD", None) + + # UPN is mandatory + if UPN is None and ssp is not None and ssp.UPN: + UPN = ssp.UPN + elif UPN is None: + raise ValueError("Must specify a 'UPN' !") + kwargs["UPN"] = UPN + # Call parent super(NTLMSSP_DOMAIN, self).__init__( *args, @@ -1932,16 +2169,17 @@ def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): ) # Treat specific parameters - self.DC_IP = kwargs.pop("DC_IP", None) - if self.DC_IP is None: - # Get DC_IP from dclocator + self.DC_FQDN = kwargs.pop("DC_FQDN", None) + if self.DC_FQDN is None: + # Get DC_FQDN from dclocator from scapy.layers.ldap import dclocator - self.DC_IP = dclocator( + dc = dclocator( self.DOMAIN_FQDN, timeout=timeout, debug=kwargs.get("debug", 0), - ).ip + ) + self.DC_FQDN = dc.samlogon.DnsHostName.decode().rstrip(".") # If logging in via Kerberos self.ssp = ssp @@ -1957,37 +2195,41 @@ def _getSessionBaseKey(self, Context, ntlm): # Import RPC stuff from scapy.layers.dcerpc import NDRUnion from scapy.layers.msrpce.msnrpc import ( - NetlogonClient, NETLOGON_SECURE_CHANNEL_METHOD, + NetlogonClient, ) from scapy.layers.msrpce.raw.ms_nrpc import ( + NETLOGON_LOGON_IDENTITY_INFO, NetrLogonSamLogonWithFlags_Request, - PNETLOGON_NETWORK_INFO, PNETLOGON_AUTHENTICATOR, - NETLOGON_LOGON_IDENTITY_INFO, - UNICODE_STRING, + PNETLOGON_NETWORK_INFO, STRING, + UNICODE_STRING, ) # Create NetlogonClient with PRIVACY client = NetlogonClient() - client.connect_and_bind(self.DC_IP) + client.connect(self.DC_FQDN) # Establish the Netlogon secure channel (this will bind) try: - if self.ssp is None: + if self.ssp is None and self.KEY is None: # Login via classic NetlogonSSP client.establish_secure_channel( mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, - computername=self.COMPUTER_NB_NAME, - domainname=self.DOMAIN_NB_NAME, - HashNt=self.HASHNT, + UPN=f"{self.COMPUTER_NB_NAME}@{self.DOMAIN_NB_NAME}", + DC_FQDN=self.DC_FQDN, + HASHNT=self.HASHNT, ) else: # Login via KerberosSSP (Windows 2025) - # TODO client.establish_secure_channel( mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos, + UPN=self.UPN, + DC_FQDN=self.DC_FQDN, + PASSWORD=self.PASSWORD, + KEY=self.KEY, + ssp=self.ssp, ) except ValueError: log_runtime.warning( diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index 676021e1d6b..541ab10c292 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -1001,10 +1001,11 @@ class NETLOGON_SAM_LOGON_RESPONSE_NT40(NETLOGON): 0x00000800: "SELECT_SECRET_DOMAIN_6", 0x00001000: "FULL_SECRET_DOMAIN_6", 0x00002000: "WS", - 0x00004000: "DS_8", - 0x00008000: "DS_9", - 0x00010000: "DS_10", # guess - 0x00020000: "DS_11", # guess + 0x00004000: "DS_8", # >=2008R2 + 0x00008000: "DS_9", # >=2012 + 0x00010000: "DS_10", # >=2016 + 0x00020000: "DS_11", # >=2019 + 0x00040000: "DS_12", # >=2025 0x20000000: "DNS_CONTROLLER", 0x40000000: "DNS_DOMAIN", 0x80000000: "DNS_FOREST", diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py index 7d17e828375..3b4e650c9d7 100644 --- a/scapy/layers/smb2.py +++ b/scapy/layers/smb2.py @@ -4717,6 +4717,7 @@ def recv(self, x=None): pkt = self.queue.popleft() else: pkt = super(SMBStreamSocket, self).recv(x) + # If there are multiple SMB2_Header requests (aka. compounded), # take the first and store the rest in a queue. if pkt is not None and ( @@ -4725,14 +4726,17 @@ def recv(self, x=None): or SMB2_Compression_Transform_Header in pkt ): pkt = self.session.in_pkt(pkt) - pay = pkt[SMB2_Header].payload + smbh = pkt[SMB2_Header] + pay = smbh.payload while SMB2_Header in pay: pay = pay[SMB2_Header] + pay._decrypted = smbh._decrypted # Keep the _decrypted flag pay.underlayer.remove_payload() self.queue.append(pay) if not pay.NextCommand: break pay = pay.payload + # Verify the signature if required. # This happens here because we must have split compounded requests first. smbh = pkt.getlayer(SMB2_Header) diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 360acbce824..68bdc30d4c8 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -447,7 +447,7 @@ def NEGOTIATED(self, ssp_blob=None): # Begin session establishment ssp_tuple = self.session.ssp.GSS_Init_sec_context( self.session.sspcontext, - token=ssp_blob, + input_token=ssp_blob, target_name="cifs/" + self.HOST if self.HOST else None, req_flags=( GSS_C_FLAGS.GSS_C_MUTUAL_FLAG @@ -476,8 +476,8 @@ def update_smbheader(self, pkt): @ATMT.condition(NEGOTIATED, prio=1) def should_send_session_setup_request(self, ssp_tuple): - _, _, negResult = ssp_tuple - if negResult not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + _, _, status = ssp_tuple + if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: raise ValueError("Internal error: the SSP completed with an error.") raise self.SENT_SESSION_REQUEST().action_parameters(ssp_tuple) @@ -487,8 +487,8 @@ def SENT_SESSION_REQUEST(self): @ATMT.action(should_send_session_setup_request) def send_setup_session_request(self, ssp_tuple): - self.session.sspcontext, token, negResult = ssp_tuple - if self.SMB2 and negResult == GSS_S_CONTINUE_NEEDED: + self.session.sspcontext, token, status = ssp_tuple + if self.SMB2 and status == GSS_S_CONTINUE_NEEDED: # New session: force 0 self.SessionId = 0 if self.SMB2 or self.EXTENDED_SECURITY: @@ -608,7 +608,7 @@ def AUTH_FAILED(self): def AUTHENTICATED(self, ssp_blob=None): self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context( self.session.sspcontext, - token=ssp_blob, + input_token=ssp_blob, target_name="cifs/" + self.HOST if self.HOST else None, ) if status != GSS_S_COMPLETE: @@ -1123,7 +1123,7 @@ def __init__( HashAes256Sha96: bytes = None, HashAes128Sha96: bytes = None, port: int = 445, - timeout: int = 2, + timeout: int = 5, debug: int = 0, ssp=None, ST=None, diff --git a/scapy/layers/smbserver.py b/scapy/layers/smbserver.py index ff20151c7bd..be1c68ee247 100644 --- a/scapy/layers/smbserver.py +++ b/scapy/layers/smbserver.py @@ -662,7 +662,8 @@ def RECEIVED_SETUP_ANDX_REQUEST(self): @ATMT.action(receive_setup_andx_request) def on_setup_andx_request(self, pkt, ssp_blob): self.session.sspcontext, tok, status = self.session.ssp.GSS_Accept_sec_context( - self.session.sspcontext, ssp_blob + self.session.sspcontext, + ssp_blob, ) self.update_smbheader(pkt) if SMB2_Session_Setup_Request in pkt: diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index 3afb73268ed..a37091313b3 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -16,13 +16,14 @@ `GSSAPI `_ """ +import os import struct from uuid import UUID from scapy.asn1.asn1 import ( - ASN1_OID, - ASN1_STRING, ASN1_Codecs, + ASN1_OID, + ASN1_GENERAL_STRING, ) from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1fields import ( @@ -31,14 +32,14 @@ ASN1F_FLAGS, ASN1F_GENERAL_STRING, ASN1F_OID, + ASN1F_optional, ASN1F_PACKET, - ASN1F_SEQUENCE, ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_STRING_ENCAPS, ASN1F_STRING, - ASN1F_optional, ) from scapy.asn1packet import ASN1_Packet -from scapy.base_classes import Net from scapy.fields import ( FieldListField, LEIntEnumField, @@ -56,32 +57,34 @@ XStrFixedLenField, XStrLenField, ) +from scapy.error import log_runtime from scapy.packet import Packet, bind_layers from scapy.utils import ( valid_ip, valid_ip6, ) -from scapy.layers.inet6 import Net6 from scapy.layers.gssapi import ( - GSSAPI_BLOB, - GSSAPI_BLOB_SIGNATURE, + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, GSS_C_FLAGS, GSS_C_NO_CHANNEL_BINDINGS, GSS_S_BAD_MECH, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, + GSS_S_FAILURE, GSS_S_FLAGS, + GSSAPI_BLOB_SIGNATURE, + GSSAPI_BLOB, GssChannelBindings, SSP, - _GSSAPI_OIDS, - _GSSAPI_SIGNATURE_OIDS, ) # SSP Providers from scapy.layers.kerberos import ( Kerberos, KerberosSSP, + _parse_spn, _parse_upn, ) from scapy.layers.ntlm import ( @@ -96,6 +99,7 @@ # Typing imports from typing import ( Dict, + List, Optional, Tuple, ) @@ -116,13 +120,14 @@ class SPNEGO_MechTypes(ASN1_Packet): class SPNEGO_MechListMIC(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_STRING("value", "") + ASN1_root = ASN1F_STRING_ENCAPS("value", "", GSSAPI_BLOB_SIGNATURE) _mechDissector = { "1.3.6.1.4.1.311.2.2.10": NTLM_Header, # NTLM "1.2.840.48018.1.2.2": Kerberos, # MS KRB5 - Microsoft Kerberos 5 "1.2.840.113554.1.2.2": Kerberos, # Kerberos 5 + "1.2.840.113554.1.2.2.3": Kerberos, # Kerberos 5 - User to User } @@ -134,13 +139,16 @@ def i2m(self, pkt, x): def m2i(self, pkt, s): dat, r = super(_SPNEGO_Token_Field, self).m2i(pkt, s) + types = None if isinstance(pkt.underlayer, SPNEGO_negTokenInit): types = pkt.underlayer.mechTypes elif isinstance(pkt.underlayer, SPNEGO_negTokenResp): types = [pkt.underlayer.supportedMech] if types and types[0] and types[0].oid.val in _mechDissector: return _mechDissector[types[0].oid.val](dat.val), r - return dat, r + else: + # Use heuristics + return GSSAPI_BLOB(dat.val), r class SPNEGO_Token(ASN1_Packet): @@ -208,7 +216,7 @@ class SPNEGO_negTokenResp(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( ASN1F_ENUMERATED( - "negResult", + "negState", 0, { 0: "accept-completed", @@ -262,6 +270,17 @@ class SPNEGO_negToken(ASN1_Packet): def mechListMIC(oids): """ Implementation of RFC 4178 - Appendix D. mechListMIC Computation + + NOTE: The documentation on mechListMIC isn't super clear, so note that: + + - The mechListMIC that the client sends is computed over the + list of mechanisms that it requests. + - the mechListMIC that the server sends is computed over the + list of mechanisms that the client requested. + + This also means that NegTokenInit2 added by [MS-SPNG] is NOT protected. + That's not necessarily an issue, since it was optional in most cases, + but it's something to keep in mind. """ return bytes(SPNEGO_MechTypes(mechTypes=oids)) @@ -528,105 +547,160 @@ class SPNEGOSSP(SSP): """ __slots__ = [ - "supported_ssps", - "force_supported_mechtypes", + "ssps", ] + auth_type = 0x09 class STATE(SSP.STATE): FIRST = 1 - CHANGESSP = 2 - NORMAL = 3 + SUBSEQUENT = 2 class CONTEXT(SSP.CONTEXT): __slots__ = [ - "supported_mechtypes", - "requested_mechtypes", "req_flags", - "negotiated_mechtype", + "ssps", + "other_mechtypes", + "sent_mechtypes", "first_choice", - "sub_context", + "require_mic", + "verified_mic", "ssp", + "ssp_context", + "ssp_mechtype", + "raw", ] def __init__( - self, supported_ssps, req_flags=None, force_supported_mechtypes=None + self, + ssps: List[SSP], + req_flags=None, ): self.state = SPNEGOSSP.STATE.FIRST - self.requested_mechtypes = None self.req_flags = req_flags - self.first_choice = True - self.negotiated_mechtype = None - self.sub_context = None + # Information used during negotiation + self.ssps = ssps + self.other_mechtypes = None # the mechtypes our peer requested + self.sent_mechtypes = None # the mechtypes we sent when acting as a client + 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 + # Information about the currently selected SSP self.ssp = None - if force_supported_mechtypes is None: - self.supported_mechtypes = [ - SPNEGO_MechType(oid=ASN1_OID(oid)) for oid in supported_ssps - ] - self.supported_mechtypes.sort( - key=lambda x: SPNEGOSSP._PREF_ORDER.index(x.oid.val) - ) - else: - self.supported_mechtypes = force_supported_mechtypes + self.ssp_context = None + self.ssp_mechtype = None + self.raw = False # fallback to raw SSP super(SPNEGOSSP.CONTEXT, self).__init__() + # This is the order Windows chooses + _PREF_ORDER = [ + "1.2.840.113554.1.2.2.3", # Kerberos 5 - User to User + "1.2.840.48018.1.2.2", # MS KRB5 + "1.2.840.113554.1.2.2", # Kerberos 5 + "1.3.6.1.4.1.311.2.2.30", # NEGOEX + "1.3.6.1.4.1.311.2.2.10", # NTLM + ] + + def get_supported_mechtypes(self): + """ + Return an ordered list of mechtypes that are still available. + """ + # 1. Build mech list + mechs = [] + for ssp in self.ssps: + mechs.extend(ssp.GSS_Inquire_names_for_mech()) + + # 2. Sort according to the preference order. + mechs.sort(key=lambda x: self._PREF_ORDER.index(x)) + + # 3. Return wrapped in MechType + return [SPNEGO_MechType(oid=ASN1_OID(oid)) for oid in mechs] + + def negotiate_ssp(self) -> None: + """ + Perform SSP negotiation. + + 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 self.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 + other_oids = [x.oid.val for x in self.other_mechtypes] + try: + self.ssp, ssp_oid = next( + (ssp, requested_oid) + for requested_oid in other_oids + for ssp in self.ssps + if requested_oid in ssp.GSS_Inquire_names_for_mech() + ) + except StopIteration: + raise ValueError( + "Could not find a common SSP with the remote peer !" + ) + + # Check whether the selected SSP was the one preferred by the client + self.first_choice = ssp_oid == other_oids[0] + + # Check whether mechListMIC is mandatory for this exchange + if not self.first_choice: + # RFC4178 rules for mechListMIC: mandatory if not the first choice. + self.require_mic = True + elif ssp_oid == "1.3.6.1.4.1.311.2.2.10" and self.ssp.SupportsMechListMIC(): + # [MS-SPNG] note 8: "If NTLM authentication is most preferred by + # the client and the server, and the client includes a MIC in + # AUTHENTICATE_MESSAGE, then the mechListMIC field becomes + # mandatory" + self.require_mic = True + + # Get the associated ssp dissection class and mechtype + self.ssp_mechtype = SPNEGO_MechType(oid=ASN1_OID(ssp_oid)) + + # Reset the ssp context + self.ssp_context = None + # Passthrough attributes and functions def clifailure(self): - self.sub_context.clifailure() + if self.ssp_context is not None: + self.ssp_context.clifailure() def __getattr__(self, attr): try: return object.__getattribute__(self, attr) except AttributeError: - return getattr(self.sub_context, attr) + return getattr(self.ssp_context, attr) def __setattr__(self, attr, val): try: return object.__setattr__(self, attr, val) except AttributeError: - return setattr(self.sub_context, attr, val) + return setattr(self.ssp_context, attr, val) # Passthrough the flags property @property def flags(self): - if self.sub_context: - return self.sub_context.flags + if self.ssp_context: + return self.ssp_context.flags return GSS_C_FLAGS(0) @flags.setter def flags(self, x): - if not self.sub_context: + if not self.ssp_context: return - self.sub_context.flags = x + self.ssp_context.flags = x def __repr__(self): - return "SPNEGOSSP[%s]" % repr(self.sub_context) - - _MECH_ALIASES = { - # Kerberos has 2 ssps - "1.2.840.48018.1.2.2": "1.2.840.113554.1.2.2", - "1.2.840.113554.1.2.2": "1.2.840.48018.1.2.2", - } - - # This is the order Windows chooses. We mimic it for plausibility - _PREF_ORDER = [ - "1.2.840.48018.1.2.2", # MS KRB5 - "1.2.840.113554.1.2.2", # Kerberos 5 - "1.3.6.1.4.1.311.2.2.30", # NEGOEX - "1.3.6.1.4.1.311.2.2.10", # NTLM - ] + return "SPNEGOSSP[%s]" % repr(self.ssp_context) - def __init__(self, ssps, **kwargs): - self.supported_ssps = {x.oid: x for x in ssps} - # Apply MechTypes aliases - for ssp in ssps: - if ssp.oid in self._MECH_ALIASES: - self.supported_ssps[self._MECH_ALIASES[ssp.oid]] = self.supported_ssps[ - ssp.oid - ] - self.force_supported_mechtypes = kwargs.pop("force_supported_mechtypes", None) + def __init__(self, ssps: List[SSP], **kwargs): + self.ssps = ssps super(SPNEGOSSP, self).__init__(**kwargs) @classmethod @@ -640,8 +714,11 @@ def from_cli_arguments( HashAes128Sha96: bytes = None, kerberos_required: bool = False, ST=None, + TGT=None, KEY=None, + ccache: str = None, debug: int = 0, + use_krb5ccname: bool = False, ): """ Initialize a SPNEGOSSP from a list of many arguments. @@ -655,8 +732,12 @@ def from_cli_arguments( :param HashAes256Sha96: (bytes) if provided, used for auth (Kerberos) :param HashAes128Sha96: (bytes) if provided, used for auth (Kerberos) :param ST: if provided, the service ticket to use (Kerberos) + :param TGT: if provided, the TGT to use (Kerberos) :param KEY: if ST provided, the session key associated to the ticket (Kerberos). - Else, the user secret key. + This can be either for the ST or TGT. Else, the user secret key. + :param ccache: (str) if provided, a path to a CCACHE (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. """ kerberos = True hostname = None @@ -664,11 +745,9 @@ def from_cli_arguments( if ":" in target: if not valid_ip6(target): hostname = target - target = str(Net6(target)) else: if not valid_ip(target): hostname = target - target = str(Net(target)) # Check UPN try: @@ -680,6 +759,10 @@ def from_cli_arguments( # not a UPN: NTLM only kerberos = False + # If we're asked, check the environment for KRB5CCNAME + if use_krb5ccname and ccache is None and "KRB5CCNAME" in os.environ: + ccache = os.environ["KRB5CCNAME"] + # Do we need to ask the password? if all( x is None @@ -689,6 +772,7 @@ def from_cli_arguments( HashNt, HashAes256Sha96, HashAes128Sha96, + ccache, ] ): # yes. @@ -700,7 +784,44 @@ def from_cli_arguments( # Kerberos if kerberos and hostname: # Get ticket if we don't already have one. - if ST is None: + if ST is None and TGT is None and ccache is not None: + # In this case, load the KerberosSSP from ccache + from scapy.modules.ticketer import Ticketer + + # Import into a Ticketer object + t = Ticketer() + t.open_ccache(ccache) + + # Look for the ticket that we'll use. We chose: + # - either a ST if the SPN matches our target + # - else a TGT if we got nothing better + tgts = [] + for i, (tkt, key, upn, spn) in enumerate(t.iter_tickets()): + spn, _ = _parse_spn(spn) + spn_host = spn.split("/")[-1] + # Check that it's for the correct user + if upn.lower() == UPN.lower(): + # Check that it's either a TGT or a ST to the correct service + if spn.lower().startswith("krbtgt/"): + # TGT. Keep it, and see if we don't have a better ST. + tgts.append(t.ssp(i)) + elif hostname.lower() == spn_host.lower(): + # ST. We're done ! + ssps.append(t.ssp(i)) + break + else: + # No ST found + if tgts: + # Using a TGT ! + ssps.append(tgts[0]) + else: + # Nothing found + t.show() + raise ValueError( + f"Could not find a ticket for {upn}, either a " + f"TGT or towards {hostname}" + ) + elif ST is None and TGT is None: # In this case, KEY is supposed to be the user's key. from scapy.libs.rfc3961 import Key, EncryptionType @@ -734,6 +855,7 @@ def from_cli_arguments( KerberosSSP( UPN=UPN, ST=ST, + TGT=TGT, KEY=KEY, debug=debug, ) @@ -748,68 +870,33 @@ def from_cli_arguments( if not kerberos_required: if HashNt is None and password is not None: HashNt = MD4le(password) - ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) + if HashNt is not None: + ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) + + if not ssps: + raise ValueError("Unexpected case ! Please report.") # Build the SSP return cls(ssps) - def _extract_gssapi(self, Context, x): - status, otherMIC, rawToken = None, None, False - # Extract values from GSSAPI - if isinstance(x, GSSAPI_BLOB): - x = x.innerToken - if isinstance(x, SPNEGO_negToken): - x = x.token - if hasattr(x, "mechTypes"): - Context.requested_mechtypes = x.mechTypes - Context.negotiated_mechtype = None - if hasattr(x, "supportedMech") and x.supportedMech is not None: - Context.negotiated_mechtype = x.supportedMech - if hasattr(x, "mechListMIC") and x.mechListMIC: - otherMIC = GSSAPI_BLOB_SIGNATURE(x.mechListMIC.value.val) - if hasattr(x, "_mechListMIC") and x._mechListMIC: - otherMIC = GSSAPI_BLOB_SIGNATURE(x._mechListMIC.value.val) - if hasattr(x, "negResult"): - status = x.negResult - try: - x = x.mechToken - except AttributeError: - try: - x = x.responseToken - except AttributeError: - # No GSSAPI wrapper (windows fallback). Remember this for answer - rawToken = True - if isinstance(x, SPNEGO_Token): - x = x.value - if Context.requested_mechtypes: - try: - cls = _mechDissector[ - ( - Context.negotiated_mechtype or Context.requested_mechtypes[0] - ).oid.val # noqa: E501 - ] - except KeyError: - cls = conf.raw_layer - if isinstance(x, ASN1_STRING): - x = cls(x.val) - elif isinstance(x, conf.raw_layer): - x = cls(x.load) - return x, status, otherMIC, rawToken - def NegTokenInit2(self): """ Server-Initiation of GSSAPI/SPNEGO. See [MS-SPNG] sect 3.2.5.2 """ - Context = self.CONTEXT( - self.supported_ssps, - force_supported_mechtypes=self.force_supported_mechtypes, - ) + Context = SPNEGOSSP.CONTEXT(list(self.ssps)) return ( Context, GSSAPI_BLOB( innerToken=SPNEGO_negToken( - token=SPNEGO_negTokenInit(mechTypes=Context.supported_mechtypes) + token=SPNEGO_negTokenInit( + mechTypes=Context.get_supported_mechtypes(), + negHints=SPNEGO_negHints( + hintName=ASN1_GENERAL_STRING( + "not_defined_in_RFC4178@please_ignore" + ), + ), + ) ) ), ) @@ -830,320 +917,374 @@ def NegTokenInit2(self): def GSS_WrapEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_WrapEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_WrapEx(Context.ssp_context, *args, **kwargs) def GSS_UnwrapEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_UnwrapEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_UnwrapEx(Context.ssp_context, *args, **kwargs) def GSS_GetMICEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_GetMICEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_GetMICEx(Context.ssp_context, *args, **kwargs) def GSS_VerifyMICEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_VerifyMICEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_VerifyMICEx(Context.ssp_context, *args, **kwargs) def LegsAmount(self, Context: CONTEXT): return 4 - def _common_spnego_handler( + def MapStatusToNegState(self, status: int) -> int: + """ + Map a GSSAPI return code to SPNEGO negState codes + """ + if status == GSS_S_COMPLETE: + return 0 # accept_completed + elif status == GSS_S_CONTINUE_NEEDED: + return 1 # accept_incomplete + else: + return 2 # reject + + def GuessOtherMechtypes(self, Context: CONTEXT, input_token): + """ + Guesses the mechtype of the peer when the "raw" fallback is used. + """ + if isinstance(input_token, NTLM_Header): + Context.other_mechtypes = [ + SPNEGO_MechType(oid=ASN1_OID("1.3.6.1.4.1.311.2.2.10")) + ] + elif isinstance(input_token, Kerberos): + Context.other_mechtypes = [ + SPNEGO_MechType(oid=ASN1_OID("1.2.840.48018.1.2.2")) + ] + else: + Context.other_mechtypes = [] + + def GSS_Init_sec_context( self, - Context, - IsClient, - token=None, + Context: CONTEXT, + input_token=None, target_name: Optional[str] = None, - req_flags=None, + req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): - """ - Common code shared across both GSS_sec_Init_Context and GSS_sec_Accept_Context - """ if Context is None: # New Context Context = SPNEGOSSP.CONTEXT( - self.supported_ssps, + list(self.ssps), req_flags=req_flags, - force_supported_mechtypes=self.force_supported_mechtypes, ) - if IsClient: - Context.requested_mechtypes = Context.supported_mechtypes - # Extract values from GSSAPI token - status, MIC, otherMIC, rawToken = 0, None, None, False - if token: - token, status, otherMIC, rawToken = self._extract_gssapi(Context, token) + input_token_inner = None + negState = None + + # Extract values from GSSAPI token, if present + if input_token is not None: + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + # We are handling a NegTokenInit2 request ! + # Populate context with values from the server's request + Context.other_mechtypes = input_token.mechTypes + elif isinstance(input_token, SPNEGO_negTokenResp): + # Extract token and state from the client request + if input_token.responseToken is not None: + input_token_inner = input_token.responseToken.value + if input_token.negState is not None: + negState = input_token.negState + else: + # The blob is a raw token. We aren't using SPNEGO here. + Context.raw = True + input_token_inner = input_token + self.GuessOtherMechtypes(Context, input_token) - # If we don't have a SSP already negotiated, check for requested and available - # SSPs and find a common one. + # Perform SSP negotiation if Context.ssp is None: - if Context.negotiated_mechtype is None: - if Context.requested_mechtypes: - # Find a common SSP - try: - Context.negotiated_mechtype = next( - x - for x in Context.requested_mechtypes - if x in Context.supported_mechtypes - ) - except StopIteration: - # no common mechanisms - raise ValueError("No common SSP mechanisms !") - # Check whether the selected SSP was the one preferred by the client - if ( - Context.negotiated_mechtype != Context.requested_mechtypes[0] - and token - ): - Context.first_choice = False - # No SSPs were requested. Use the first available SSP we know. - elif Context.supported_mechtypes: - Context.negotiated_mechtype = Context.supported_mechtypes[0] - else: - raise ValueError("Can't figure out what SSP to use") - # Set Context.ssp to the object matching the chosen SSP type. - Context.ssp = self.supported_ssps[Context.negotiated_mechtype.oid.val] + 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 + + # Call inner-SSP + Context.ssp_context, output_token_inner, status = ( + Context.ssp.GSS_Init_sec_context( + Context.ssp_context, + input_token=input_token_inner, + target_name=target_name, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, + ) + ) - if not Context.first_choice: - # The currently provided token is not for this SSP ! - # Typically a client opportunistically starts with Kerberos, including - # its APREQ, and we want to use NTLM. We add one round trip - Context.state = SPNEGOSSP.STATE.FIRST - Context.first_choice = True # reset to not come here again. - tok, status = None, GSS_S_CONTINUE_NEEDED - else: - # The currently provided token is for this SSP ! - # Pass it to the sub ssp, with its own context - if IsClient: - Context.sub_context, tok, status = Context.ssp.GSS_Init_sec_context( - Context.sub_context, - token=token, + if negState == 2 or status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + # SSP failed. Remove it from the list of SSPs we're currently running + Context.ssps.remove(Context.ssp) + log_runtime.warning( + "SPNEGOSSP: %s failed. Retrying with next in queue." % repr(Context.ssp) + ) + + if Context.ssps: + # We have other SSPs remaining. Retry using another one. + Context.ssp = None + return self.GSS_Init_sec_context( + Context, + None, # No input for retry. target_name=target_name, - req_flags=Context.req_flags, + req_flags=req_flags, chan_bindings=chan_bindings, ) else: - Context.sub_context, tok, status = Context.ssp.GSS_Accept_sec_context( - Context.sub_context, - token=token, - req_flags=Context.req_flags, - chan_bindings=chan_bindings, - ) - # Check whether client or server says the specified mechanism is not valid - if status == GSS_S_BAD_MECH: - # Mechanism is not usable. Typically the Kerberos SPN is wrong - to_remove = [Context.negotiated_mechtype.oid.val] - # If there's an alias (for the multiple kerberos oids, also include it) - if Context.negotiated_mechtype.oid.val in SPNEGOSSP._MECH_ALIASES: - to_remove.append( - SPNEGOSSP._MECH_ALIASES[Context.negotiated_mechtype.oid.val] - ) - # Drop those unusable mechanisms from the supported list - for x in list(Context.supported_mechtypes): - if x.oid.val in to_remove: - Context.supported_mechtypes.remove(x) - break - # Re-calculate negotiated mechtype - try: - Context.negotiated_mechtype = next( - x - for x in Context.requested_mechtypes - if x in Context.supported_mechtypes - ) - except StopIteration: - # no common mechanisms - raise ValueError("No common SSP mechanisms after GSS_S_BAD_MECH !") - # Start again. - Context.state = SPNEGOSSP.STATE.CHANGESSP - Context.ssp = None # Reset the SSP - Context.sub_context = None # Reset the SSP context - if IsClient: - # Call ourselves again for the client to generate a token - return self._common_spnego_handler( - Context, - IsClient=True, - token=None, - req_flags=req_flags, - chan_bindings=chan_bindings, - ) - else: - # Return nothing but the supported SSP list - tok, status = None, GSS_S_CONTINUE_NEEDED - - if rawToken: - # No GSSAPI wrapper (fallback) - return Context, tok, status + # We don't have anything left + return Context, None, status + + # Raw processing ends here. + if Context.raw: + return Context, output_token_inner, status + + # Verify MIC if present. + if status == GSS_S_COMPLETE and input_token and input_token.mechListMIC: + # NOTE: the mechListMIC that the server sends is computed over the list of + # mechanisms that the **client requested**. + Context.ssp.VerifyMechListMIC( + Context.ssp_context, + input_token.mechListMIC.value, + mechListMIC(Context.sent_mechtypes), + ) + Context.verified_mic = True - # Client success - if IsClient and tok is None and status == GSS_S_COMPLETE: + if negState == 0 and status == GSS_S_COMPLETE: + # We are done. return Context, None, status + elif Context.state == SPNEGOSSP.STATE.FIRST: + # First freeze the list of available mechtypes on the first message + Context.sent_mechtypes = Context.get_supported_mechtypes() - # Map GSSAPI codes to SPNEGO - if status == GSS_S_COMPLETE: - negResult = 0 # accept_completed - elif status == GSS_S_CONTINUE_NEEDED: - negResult = 1 # accept_incomplete - else: - negResult = 2 # reject - - # GSSAPI-MIC - if Context.ssp and Context.ssp.canMechListMIC(Context.sub_context): - # The documentation on mechListMIC wasn't clear, so note that: - # - The mechListMIC that the client sends is computed over the - # list of mechanisms that it requests. - # - the mechListMIC that the server sends is computed over the - # list of mechanisms that the client requested. - # Yes, this does indeed mean that NegTokenInit2 added by [MS-SPNG] - # is NOT protected. That's not necessarily an issue, since it was - # optional in most cases, but it's something to keep in mind. - if otherMIC is not None: - # Check the received MIC if any - if IsClient: # from server - Context.ssp.verifyMechListMIC( - Context, - otherMIC, - mechListMIC(Context.supported_mechtypes), - ) - else: # from client - Context.ssp.verifyMechListMIC( - Context, - otherMIC, - mechListMIC(Context.requested_mechtypes), - ) - # Then build our own MIC - if IsClient: # client - if negResult == 0: - # Include MIC for the last packet. We could add a check - # here to only send the MIC when required (when preferred ssp - # isn't chosen) - MIC = Context.ssp.getMechListMIC( - Context, - mechListMIC(Context.supported_mechtypes), - ) - else: # server - MIC = Context.ssp.getMechListMIC( - Context, - mechListMIC(Context.requested_mechtypes), + # Now build the token + spnego_tok = GSSAPI_BLOB( + innerToken=SPNEGO_negToken( + token=SPNEGO_negTokenInit(mechTypes=Context.sent_mechtypes) ) + ) - if IsClient: - if Context.state == SPNEGOSSP.STATE.FIRST: - # First client token - spnego_tok = SPNEGO_negToken( - token=SPNEGO_negTokenInit(mechTypes=Context.supported_mechtypes) - ) - if tok: - spnego_tok.token.mechToken = SPNEGO_Token(value=tok) - else: - # Subsequent client tokens - spnego_tok = SPNEGO_negToken( # GSSAPI_BLOB is stripped - token=SPNEGO_negTokenResp( - supportedMech=None, - negResult=None, - ) + # Add the output token if provided + if output_token_inner is not None: + spnego_tok.innerToken.token.mechToken = SPNEGO_Token( + value=output_token_inner, ) - if tok: - spnego_tok.token.responseToken = SPNEGO_Token(value=tok) - if Context.state == SPNEGOSSP.STATE.CHANGESSP: - # On renegotiation, include the negResult and chosen mechanism - spnego_tok.token.negResult = negResult - spnego_tok.token.supportedMech = Context.negotiated_mechtype - else: - spnego_tok = SPNEGO_negToken( # GSSAPI_BLOB is stripped + elif Context.state == SPNEGOSSP.STATE.SUBSEQUENT: + # Build subsequent client tokens: without the list of supported mechtypes + # NOTE: GSSAPI_BLOB is stripped. + spnego_tok = SPNEGO_negToken( token=SPNEGO_negTokenResp( supportedMech=None, - negResult=negResult, + negState=None, ) ) - if Context.state in [SPNEGOSSP.STATE.FIRST, SPNEGOSSP.STATE.CHANGESSP]: - # Include the supportedMech list if this is the first thing we do - # or a renegotiation. - spnego_tok.token.supportedMech = Context.negotiated_mechtype - if tok: - spnego_tok.token.responseToken = SPNEGO_Token(value=tok) - # Apply MIC if available - if MIC: - spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( - value=ASN1_STRING(MIC), - ) - if ( - IsClient and Context.state == SPNEGOSSP.STATE.FIRST - ): # Client: after the first packet, specifying 'SPNEGO' is implicit. - # Always implicit for the server. - spnego_tok = GSSAPI_BLOB(innerToken=spnego_tok) - # Not the first token anymore - Context.state = SPNEGOSSP.STATE.NORMAL + + # Add the MIC if required and the exchange is finished. + if status == GSS_S_COMPLETE and Context.require_mic: + spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( + value=Context.ssp.GetMechListMIC( + Context.ssp_context, + mechListMIC(Context.sent_mechtypes), + ), + ) + + # If we still haven't verified the MIC, we aren't done. + if not Context.verified_mic: + status = GSS_S_CONTINUE_NEEDED + + # Add the output token if provided + if output_token_inner: + spnego_tok.token.responseToken = SPNEGO_Token( + value=output_token_inner, + ) + + # Update the state + Context.state = SPNEGOSSP.STATE.SUBSEQUENT + return Context, spnego_tok, status - def GSS_Init_sec_context( + def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, - target_name: Optional[str] = None, - req_flags: Optional[GSS_C_FLAGS] = None, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): - return self._common_spnego_handler( - Context, - True, - token=token, - target_name=target_name, - req_flags=req_flags, - chan_bindings=chan_bindings, + if Context is None: + # New Context + Context = SPNEGOSSP.CONTEXT( + list(self.ssps), + req_flags=req_flags, + ) + + input_token_inner = None + _mechListMIC = None + + # Extract values from GSSAPI token + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + # Populate context with values from the client's request + if input_token.mechTypes: + Context.other_mechtypes = input_token.mechTypes + if input_token.mechToken: + input_token_inner = input_token.mechToken.value + _mechListMIC = input_token.mechListMIC or input_token._mechListMIC + elif isinstance(input_token, SPNEGO_negTokenResp): + if input_token.responseToken: + input_token_inner = input_token.responseToken.value + _mechListMIC = input_token.mechListMIC + else: + # The blob is a raw token. We aren't using SPNEGO here. + Context.raw = True + input_token_inner = input_token + self.GuessOtherMechtypes(Context, input_token) + + if Context.other_mechtypes is None: + # At this point, we should have already gotten the mechtypes from a current + # or former request. + 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 + + output_token_inner = None + status = GSS_S_CONTINUE_NEEDED + + # If we didn't pick the client's first choice, the token we were passed + # isn't usable. + if not Context.first_choice: + # Typically a client opportunistically starts with Kerberos, including + # its APREQ, and we want to use NTLM. Here we add one round trip + Context.first_choice = True # Do not enter here again. + else: + # Send it to the negotiated SSP + Context.ssp_context, output_token_inner, status = ( + Context.ssp.GSS_Accept_sec_context( + Context.ssp_context, + input_token=input_token_inner, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, + ) + ) + + # Verify MIC if context succeeded + if status == GSS_S_COMPLETE and _mechListMIC: + # NOTE: the mechListMIC that the client sends is computed over the + # **list of mechanisms that it requests**. + if Context.ssp.SupportsMechListMIC(): + # We need to check we support checking the MIC. The only case where + # this is needed is NTLM in guest mode: the client will send a mic + # but we don't check it... + Context.ssp.VerifyMechListMIC( + Context.ssp_context, + _mechListMIC.value, + mechListMIC(Context.other_mechtypes), + ) + Context.verified_mic = True + Context.require_mic = True + + # Raw processing ends here. + if Context.raw: + return Context, output_token_inner, status + + # 0. Build the template response token + spnego_tok = SPNEGO_negToken( + token=SPNEGO_negTokenResp( + supportedMech=None, + ) ) + if Context.state == SPNEGOSSP.STATE.FIRST: + # Include the supportedMech list if this is the first message we send + # or a renegotiation. + spnego_tok.token.supportedMech = Context.ssp_mechtype - def GSS_Accept_sec_context( + # Add the output token if provided + if output_token_inner: + spnego_tok.token.responseToken = SPNEGO_Token(value=output_token_inner) + + # Update the state + Context.state = SPNEGOSSP.STATE.SUBSEQUENT + + # Add the MIC if required and the exchange is finished. + if status == GSS_S_COMPLETE and Context.require_mic: + spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( + value=Context.ssp.GetMechListMIC( + Context.ssp_context, + mechListMIC(Context.other_mechtypes), + ), + ) + + # If we still haven't verified the MIC, we aren't done. + if not Context.verified_mic: + status = GSS_S_CONTINUE_NEEDED + + # Set negState + spnego_tok.token.negState = self.MapStatusToNegState(status) + + return Context, spnego_tok, status + + def GSS_Passive( self, Context: CONTEXT, - token=None, - req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, - chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + input_token=None, + req_flags=None, ): - return self._common_spnego_handler( - Context, - False, - token=token, - req_flags=req_flags, - chan_bindings=chan_bindings, - ) - - def GSS_Passive(self, Context: CONTEXT, token=None, req_flags=None): if Context is None: # New Context - Context = SPNEGOSSP.CONTEXT(self.supported_ssps) + Context = SPNEGOSSP.CONTEXT(list(self.ssps)) Context.passive = True - # Extraction - token, status, _, rawToken = self._extract_gssapi(Context, token) + input_token_inner = None - if token is None and status == GSS_S_COMPLETE: - return Context, None - - # Just get the negotiated SSP - if Context.negotiated_mechtype: - mechtype = Context.negotiated_mechtype - elif Context.requested_mechtypes: - mechtype = Context.requested_mechtypes[0] - elif rawToken and Context.supported_mechtypes: - mechtype = Context.supported_mechtypes[0] - else: - return None, GSS_S_BAD_MECH - try: - ssp = self.supported_ssps[mechtype.oid.val] - except KeyError: - return None, GSS_S_BAD_MECH - - if Context.ssp is not None: - # Detect resets - if Context.ssp != ssp: - Context.ssp = ssp - Context.sub_context = None + # Extract values from GSSAPI token + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + if input_token.mechTypes is not None: + Context.other_mechtypes = input_token.mechTypes + if input_token.mechToken: + input_token_inner = input_token.mechToken.value + elif isinstance(input_token, SPNEGO_negTokenResp): + if input_token.supportedMech is not None: + Context.other_mechtypes = [input_token.supportedMech] + if input_token.responseToken: + input_token_inner = input_token.responseToken.value else: - Context.ssp = ssp + # Raw. + input_token_inner = input_token + + if Context.other_mechtypes is None: + self.GuessOtherMechtypes(Context, input_token) + + # Uninitialized OR allowed mechtypes have changed + if Context.ssp is None or Context.ssp_mechtype not in Context.other_mechtypes: + try: + Context.negotiate_ssp() + except ValueError: + # Couldn't find common SSP + return Context, GSS_S_FAILURE # Passthrough - Context.sub_context, status = Context.ssp.GSS_Passive( - Context.sub_context, - token, + Context.ssp_context, status = Context.ssp.GSS_Passive( + Context.ssp_context, + input_token_inner, req_flags=req_flags, ) @@ -1151,8 +1292,8 @@ def GSS_Passive(self, Context: CONTEXT, token=None, req_flags=None): def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): Context.ssp.GSS_Passive_set_Direction( - Context.sub_context, IsAcceptor=IsAcceptor + Context.ssp_context, IsAcceptor=IsAcceptor ) def MaximumSignatureLength(self, Context: CONTEXT): - return Context.ssp.MaximumSignatureLength(Context.sub_context) + return Context.ssp.MaximumSignatureLength(Context.ssp_context) diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index b38f52ca073..075601848de 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -4,36 +4,58 @@ # Copyright (C) 2008 Arnaud Ebalard # # 2015, 2016, 2017 Maxence Tury +# 2022-2025 Gabriel Potter """ -High-level methods for PKI objects (X.509 certificates, CRLs, asymmetric keys). -Supports both RSA and ECDSA objects. +High-level methods for PKI objects (X.509 certificates, CRLs, asymmetric keys, CMS). +Supports both RSA, ECDSA and EDDSA objects. The classes below are wrappers for the ASN.1 objects defined in x509.py. + +Example 1: Certificate & Private key +____________________________________ + For instance, here is what you could do in order to modify the subject public key info of a 'cert' and then resign it with whatever 'key':: - from scapy.layers.tls.cert import * - cert = Cert("cert.der") - k = PrivKeyRSA() # generate a private key - cert.setSubjectPublicKeyFromPrivateKey(k) - cert.resignWith(k) - cert.export("newcert.pem") - k.export("mykey.pem") + >>> from scapy.layers.tls.cert import * + >>> cert = Cert("cert.der") + >>> k = PrivKeyRSA() # generate a private key + >>> cert.setSubjectPublicKeyFromPrivateKey(k) + >>> cert.resignWith(k) + >>> cert.export("newcert.pem") + >>> k.export("mykey.pem") One could also edit arguments like the serial number, as such:: - from scapy.layers.tls.cert import * - c = Cert("mycert.pem") - c.tbsCertificate.serialNumber = 0x4B1D - k = PrivKey("mykey.pem") # import an existing private key - c.resignWith(k) - c.export("newcert.pem") + >>> from scapy.layers.tls.cert import * + >>> c = Cert("mycert.pem") + >>> c.tbsCertificate.serialNumber = 0x4B1D + >>> k = PrivKey("mykey.pem") # import an existing private key + >>> c.resignWith(k) + >>> c.export("newcert.pem") To export the public key of a private key:: - k = PrivKey("mykey.pem") - k.pubkey.export("mypubkey.pem") + >>> k = PrivKey("mykey.pem") + >>> k.pubkey.export("mypubkey.pem") + +Example 2: CertList and CertTree +________________________________ + +Load a .pem file that contains multiple certificates:: + + >>> l = CertList("ca_chain.pem") + >>> l.show() + 0000 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test CA...] + 0001 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test Client...] + +Use 'CertTree' to organize the certificates in a tree:: + + >>> tree = CertTree("ca_chain.pem") # or tree = CertTree(l) + >>> tree.show() + /C=Ulaanbaatar/OU=Scapy Test PKI/CN=Scapy Test CA [Self Signed] + /C=FR/OU=Scapy Test PKI/CN=Scapy Test Client [Not Self Signed] No need for obnoxious openssl tweaking anymore. :) """ @@ -43,30 +65,59 @@ import time from scapy.config import conf, crypto_validator +from scapy.compat import Self from scapy.error import warning from scapy.utils import binrepr -from scapy.asn1.asn1 import ASN1_BIT_STRING +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_NULL, + ASN1_OID, + ASN1_STRING, +) from scapy.asn1.mib import hash_by_oid +from scapy.packet import Packet from scapy.layers.x509 import ( + CMS_Attribute, + CMS_CertificateChoices, + CMS_ContentInfo, + CMS_EncapsulatedContentInfo, + CMS_IssuerAndSerialNumber, + CMS_RevocationInfoChoice, + CMS_SignedAttrsForSignature, + CMS_SignedData, + CMS_SignerInfo, ECDSAPrivateKey_OpenSSL, ECDSAPrivateKey, ECDSAPublicKey, - EdDSAPublicKey, EdDSAPrivateKey, + EdDSAPublicKey, RSAPrivateKey_OpenSSL, RSAPrivateKey, RSAPublicKey, + X509_AlgorithmIdentifier, X509_Cert, X509_CRL, X509_SubjectPublicKeyInfo, ) -from scapy.layers.tls.crypto.pkcs1 import pkcs_os2ip, _get_hash, \ - _EncryptAndVerifyRSA, _DecryptAndSignRSA -from scapy.compat import raw, bytes_encode +from scapy.layers.tls.crypto.pkcs1 import ( + _DecryptAndSignRSA, + _EncryptAndVerifyRSA, + _get_hash, + pkcs_os2ip, +) +from scapy.compat import bytes_encode + +# Typing imports +from typing import ( + List, + Optional, + Union, +) if conf.crypto_valid: from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa, ec, x25519 @@ -89,21 +140,24 @@ # loading huge file when importing a cert _MAX_KEY_SIZE = 50 * 1024 _MAX_CERT_SIZE = 50 * 1024 -_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big +_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big ##################################################################### # Some helpers ##################################################################### + @conf.commands.register def der2pem(der_string, obj="UNKNOWN"): """Convert DER octet string to PEM format (with optional header)""" # Encode a byte string in PEM format. Header advertises type. pem_string = "-----BEGIN %s-----\n" % obj base64_string = base64.b64encode(der_string).decode() - chunks = [base64_string[i:i + 64] for i in range(0, len(base64_string), 64)] # noqa: E501 - pem_string += '\n'.join(chunks) + chunks = [ + base64_string[i : i + 64] for i in range(0, len(base64_string), 64) + ] # noqa: E501 + pem_string += "\n".join(chunks) pem_string += "\n-----END %s-----\n" % obj return pem_string @@ -164,7 +218,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): raise Exception(error_msg) obj_path = bytes_encode(obj_path) - if (b'\x00' not in obj_path) and os.path.isfile(obj_path): + if (b"\x00" not in obj_path) and os.path.isfile(obj_path): _size = os.path.getsize(obj_path) if _size > obj_max_size: raise Exception(error_msg) @@ -181,7 +235,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): frmt = "PEM" pem = _raw der_list = split_pem(pem) - der = b''.join(map(pem2der, der_list)) + der = b"".join(map(pem2der, der_list)) else: frmt = "DER" der = _raw @@ -200,12 +254,14 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): # Public Keys # ############### + class _PubKeyFactory(_PKIObjMaker): """ Metaclass for PubKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ + def __call__(cls, key_path=None, cryptography_obj=None): # This allows to import cryptography objects directly if cryptography_obj is not None: @@ -275,12 +331,11 @@ class PubKey(metaclass=_PubKeyFactory): """ def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ + """Verifies either a Cert or an X509_Cert.""" + h = cert.getSignatureHashName() tbsCert = cert.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + sigVal = bytes(cert.signatureValue) + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") @property def pem(self): @@ -315,12 +370,20 @@ def export(self, filename, fmt=None): elif fmt == "PEM": return f.write(self.pem.encode()) + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + class PubKeyRSA(PubKey, _EncryptAndVerifyRSA): """ Wrapper for RSA keys based on _EncryptAndVerifyRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None): pubExp = pubExp or 65537 @@ -374,8 +437,7 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): return _EncryptAndVerifyRSA.encrypt(self, msg, t=t, h=h, mgf=mgf, L=L) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - return _EncryptAndVerifyRSA.verify( - self, msg, sig, t=t, h=h, mgf=mgf, L=L) + return _EncryptAndVerifyRSA.verify(self, msg, sig, t=t, h=h, mgf=mgf, L=L) class PubKeyECDSA(PubKey): @@ -383,6 +445,7 @@ class PubKeyECDSA(PubKey): Wrapper for ECDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -415,6 +478,7 @@ class PubKeyEdDSA(PubKey): Wrapper for EdDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or x25519.X25519PrivateKey @@ -445,12 +509,14 @@ def verify(self, msg, sig, **kwargs): # Private Keys # ################ + class _PrivKeyFactory(_PKIObjMaker): """ Metaclass for PrivKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ + def __call__(cls, key_path=None, cryptography_obj=None): """ key_path may be the path to either: @@ -472,11 +538,14 @@ def __call__(cls, key_path=None, cryptography_obj=None): if cryptography_obj is not None: # We (stupidly) need to go through the whole import process because RSA # does more than just importing the cryptography objects... - obj = _PKIObj("DER", cryptography_obj.private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() - )) + obj = _PKIObj( + "DER", + cryptography_obj.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ), + ) else: # Load from file obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) @@ -518,8 +587,10 @@ def __call__(cls, key_path=None, cryptography_obj=None): class _Raw_ASN1_BIT_STRING(ASN1_BIT_STRING): """A ASN1_BIT_STRING that ignores BER encoding""" + def __bytes__(self): return self.val_readable + __str__ = __bytes__ @@ -546,7 +617,7 @@ def signTBSCert(self, tbsCert, h="sha256"): """ sigAlg = tbsCert.signature h = h or hash_by_oid[sigAlg.algorithm.val] - sigVal = self.sign(raw(tbsCert), h=h, t='pkcs') + sigVal = self.sign(bytes(tbsCert), h=h, t="pkcs") c = X509_Cert() c.tbsCertificate = tbsCert c.signatureAlgorithm = sigAlg @@ -554,16 +625,16 @@ def signTBSCert(self, tbsCert, h="sha256"): return c def resignCert(self, cert): - """ Rewrite the signature of either a Cert or an X509_Cert. """ + """Rewrite the signature of either a Cert or an X509_Cert.""" return self.signTBSCert(cert.tbsCertificate, h=None) def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ + """Verifies either a Cert or an X509_Cert.""" tbsCert = cert.tbsCertificate sigAlg = tbsCert.signature h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + sigVal = bytes(cert.signatureValue) + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") @property def pem(self): @@ -574,7 +645,7 @@ def der(self): return self.key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) def export(self, filename, fmt=None): @@ -592,19 +663,50 @@ def export(self, filename, fmt=None): elif fmt == "PEM": return f.write(self.pem.encode()) + @crypto_validator + def sign(self, data, h="sha256", **kwargs): + """ + Sign data. + """ + raise NotImplementedError + + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + class PrivKeyRSA(PrivKey, _DecryptAndSignRSA): """ Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator - def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, - prime1=None, prime2=None, coefficient=None, - exponent1=None, exponent2=None, privExp=None): + def fill_and_store( + self, + modulus=None, + modulusLen=None, + pubExp=None, + prime1=None, + prime2=None, + coefficient=None, + exponent1=None, + exponent2=None, + privExp=None, + ): pubExp = pubExp or 65537 - if None in [modulus, prime1, prime2, coefficient, privExp, - exponent1, exponent2]: + if None in [ + modulus, + prime1, + prime2, + coefficient, + privExp, + exponent1, + exponent2, + ]: # note that the library requires every parameter # in order to call RSAPrivateNumbers(...) # if one of these is missing, we generate a whole new key @@ -627,10 +729,15 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, if modulusLen and real_modulusLen != modulusLen: warning("modulus and modulusLen do not match!") pubNum = rsa.RSAPublicNumbers(n=modulus, e=pubExp) - privNum = rsa.RSAPrivateNumbers(p=prime1, q=prime2, - dmp1=exponent1, dmq1=exponent2, - iqmp=coefficient, d=privExp, - public_numbers=pubNum) + privNum = rsa.RSAPrivateNumbers( + p=prime1, + q=prime2, + dmp1=exponent1, + dmq1=exponent2, + iqmp=coefficient, + d=privExp, + public_numbers=pubNum, + ) self.key = privNum.private_key(default_backend()) pubkey = self.key.public_key() @@ -653,10 +760,16 @@ def import_from_asn1pkt(self, privkey): exponent1 = privkey.exponent1.val exponent2 = privkey.exponent2.val coefficient = privkey.coefficient.val - self.fill_and_store(modulus=modulus, pubExp=pubExp, - privExp=privExp, prime1=prime1, prime2=prime2, - exponent1=exponent1, exponent2=exponent2, - coefficient=coefficient) + self.fill_and_store( + modulus=modulus, + pubExp=pubExp, + privExp=privExp, + prime1=prime1, + prime2=prime2, + exponent1=exponent1, + exponent2=exponent2, + coefficient=coefficient, + ) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): return self.pubkey.verify( @@ -677,6 +790,7 @@ class PrivKeyECDSA(PrivKey): Wrapper for ECDSA keys based on SigningKey from ecdsa library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -686,8 +800,9 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) # noqa: E501 self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) self.marker = "EC PRIVATE KEY" @@ -705,6 +820,7 @@ class PrivKeyEdDSA(PrivKey): Wrapper for EdDSA keys Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or x25519.X25519PrivateKey @@ -714,8 +830,9 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) # noqa: E501 self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) self.marker = "PRIVATE KEY" @@ -732,21 +849,25 @@ def sign(self, data, **kwargs): # Certificates # ################ + class _CertMaker(_PKIObjMaker): """ Metaclass for Cert creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path=None, cryptography_obj=None): # This allows to import cryptography objects directly if cryptography_obj is not None: - obj = _PKIObj("DER", cryptography_obj.public_bytes( - encoding=serialization.Encoding.DER, - )) + obj = _PKIObj( + "DER", + cryptography_obj.public_bytes( + encoding=serialization.Encoding.DER, + ), + ) else: # Load from file - obj = _PKIObjMaker.__call__(cls, cert_path, - _MAX_CERT_SIZE, "CERTIFICATE") + obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CERT_SIZE, "CERTIFICATE") obj.__class__ = Cert obj.marker = "CERTIFICATE" try: @@ -771,7 +892,6 @@ def import_from_asn1pkt(self, cert): self.x509Cert = cert tbsCert = cert.tbsCertificate - self.tbsCertificate = tbsCert if tbsCert.version: self.version = tbsCert.version.val + 1 @@ -801,7 +921,7 @@ def import_from_asn1pkt(self, cert): raise Exception(error_msg) self.notAfter_str_simple = time.strftime("%x", self.notAfter) - self.pubKey = PubKey(raw(tbsCert.subjectPublicKeyInfo)) + self.pubKey = PubKey(bytes(tbsCert.subjectPublicKeyInfo)) if tbsCert.extensions: for extn in tbsCert.extensions: @@ -816,7 +936,7 @@ def import_from_asn1pkt(self, cert): elif extn.extnID.oidname == "authorityKeyIdentifier": self.authorityKeyID = extn.extnValue.keyIdentifier.val - self.signatureValue = raw(cert.signatureValue) + self.signatureValue = bytes(cert.signatureValue) self.signatureLen = len(self.signatureValue) def isIssuerCert(self, other): @@ -846,14 +966,19 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): return self.pubKey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) - def getSignatureHash(self): + def getSignatureHashName(self): """ - Return the hash used by the 'signatureAlgorithm' + Return the hash name used by the 'signatureAlgorithm'. """ tbsCert = self.tbsCertificate sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - return _get_hash(h) + return hash_by_oid[sigAlg.algorithm.val] + + def getSignatureHash(self): + """ + Return the hash cryptography object used by the 'signatureAlgorithm' + """ + return _get_hash(self.getSignatureHashName()) def setSubjectPublicKeyFromPrivateKey(self, key): """ @@ -901,17 +1026,19 @@ def remainingDays(self, now=None): now = time.localtime() elif isinstance(now, str): try: - if '/' in now: - now = time.strptime(now, '%m/%d/%y') + if "/" in now: + now = time.strptime(now, "%m/%d/%y") else: - now = time.strptime(now, '%b %d %H:%M:%S %Y %Z') + now = time.strptime(now, "%b %d %H:%M:%S %Y %Z") except Exception: - warning("Bad time string provided, will use localtime() instead.") # noqa: E501 + warning( + "Bad time string provided, will use localtime() instead." + ) # noqa: E501 now = time.localtime() now = time.mktime(now) nft = time.mktime(self.notAfter) - diff = (nft - now) / (24. * 3600) + diff = (nft - now) / (24.0 * 3600) return diff def isRevoked(self, crl_list): @@ -931,14 +1058,20 @@ def isRevoked(self, crl_list): Cert. Otherwise, the issuers are simply compared. """ for c in crl_list: - if (self.authorityKeyID is not None and - c.authorityKeyID is not None and - self.authorityKeyID == c.authorityKeyID): + if ( + self.authorityKeyID is not None + and c.authorityKeyID is not None + and self.authorityKeyID == c.authorityKeyID + ): return self.serial in (x[0] for x in c.revoked_cert_serials) elif self.issuer == c.issuer: return self.serial in (x[0] for x in c.revoked_cert_serials) return False + @property + def tbsCertificate(self): + return self.x509Cert.tbsCertificate + @property def pem(self): return der2pem(self.der, self.marker) @@ -947,6 +1080,12 @@ def pem(self): def der(self): return bytes(self.x509Cert) + def __eq__(self, other): + return self.der == other.der + + def __hash__(self): + return hash(self.der) + def export(self, filename, fmt=None): """ Export certificate in 'fmt' format (DER or PEM) to file 'filename' @@ -969,18 +1108,23 @@ def show(self): print("Validity: %s to %s" % (self.notBefore_str, self.notAfter_str)) def __repr__(self): - return "[X.509 Cert. Subject:%s, Issuer:%s]" % (self.subject_str, self.issuer_str) # noqa: E501 + return "[X.509 Cert. Subject:%s, Issuer:%s]" % ( + self.subject_str, + self.issuer_str, + ) # noqa: E501 ################################ # Certificate Revocation Lists # ################################ + class _CRLMaker(_PKIObjMaker): """ Metaclass for CRL creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path): obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CRL_SIZE, "X509 CRL") obj.__class__ = CRL @@ -1004,7 +1148,7 @@ def import_from_asn1pkt(self, crl): self.x509CRL = crl tbsCertList = crl.tbsCertList - self.tbsCertList = raw(tbsCertList) + self.tbsCertList = bytes(tbsCertList) if tbsCertList.version: self.version = tbsCertList.version.val + 1 @@ -1057,7 +1201,7 @@ def import_from_asn1pkt(self, crl): revoked.append((serial, date)) self.revoked_cert_serials = revoked - self.signatureValue = raw(crl.signatureValue) + self.signatureValue = bytes(crl.signatureValue) self.signatureLen = len(self.signatureValue) def isIssuerCert(self, other): @@ -1078,140 +1222,466 @@ def show(self): print("nextUpdate: %s" % self.nextUpdate_str) +#################### +# Certificate list # +#################### + + +class CertList(list): + """ + An object that can store a list of Cert objects, load them and export them + into DER/PEM format. + """ + + def __init__( + self, + certList: Union[Self, List[Cert], Cert, str], + ): + """ + Construct a list of certificates/CRLs to be used as list of ROOT certificates. + """ + # Parse the certificate list / CA + if isinstance(certList, str): + # It's a path. First get the _PKIObj + obj = _PKIObjMaker.__call__( + CertList, certList, _MAX_CERT_SIZE, "CERTIFICATE" + ) + + # Then parse the der until there's nothing left + certList = [] + payload = obj._der + while payload: + cert = X509_Cert(payload) + if conf.raw_layer in cert.payload: + payload = cert.payload.load + else: + payload = None + cert.remove_payload() + certList.append(Cert(cert)) + + self.frmt = obj.frmt + elif isinstance(certList, Cert): + certList = [certList] + self.frmt = "PEM" + else: + self.frmt = "PEM" + + super(CertList, self).__init__(certList) + + def findCertByIssuer(self, issuer): + """ + Find a certificate in the list by issuer. + """ + for cert in self: + if cert.issuer == issuer: + return cert + raise KeyError("Certificate not found !") + + def export(self, filename, fmt=None): + """ + Export a list of certificates 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + @property + def der(self): + return b"".join(x.der for x in self) + + @property + def pem(self): + return "".join(x.pem for x in self) + + def __repr__(self): + return "" % (len(self),) + + def show(self): + for i, c in enumerate(self): + print(conf.color_theme.id(i, fmt="%04i"), end=" ") + print(repr(c)) + + ###################### # Certificate chains # ###################### -class Chain(list): + +class CertTree(CertList): """ - Basically, an enhanced array of Cert. + An extension to CertList that additionally has a list of ROOT CAs + that are trusted. + + Example:: + + >>> tree = CertTree("ca_chain.pem") + >>> tree.show() + /CN=DOMAIN-DC1-CA/dc=DOMAIN [Self Signed] + /CN=Administrator/dc=DOMAIN [Not Self Signed] """ - def __init__(self, certList, cert0=None): + __slots__ = ["frmt", "rootCAs"] + + def __init__( + self, + certList: Union[List[Cert], CertList, str], + rootCAs: Union[List[Cert], CertList, Cert, str, None] = None, + ): """ - Construct a chain of certificates starting with a self-signed - certificate (or any certificate submitted by the user) - and following issuer/subject matching and signature validity. - If there is exactly one chain to be constructed, it will be, - but if there are multiple potential chains, there is no guarantee - that the retained one will be the longest one. - As Cert and CRL classes both share an isIssuerCert() method, - the trailing element of a Chain may alternatively be a CRL. + Construct a chain of certificates that follows issuer/subject matching and + respects signature validity. Note that we do not check AKID/{SKID/issuer/serial} matching, nor the presence of keyCertSign in keyUsage extension (if present). + + :param certList: a list of Cert/CRL objects (or path to PEM/DER file containing + multiple certs/CRL) to try to chain. + :param rootCAs: (optional) a list of certificates to trust. If not provided, + trusts any self-signed certificates from the certList. """ - list.__init__(self, ()) - if cert0: - self.append(cert0) + # Parse the certificate list + certList = CertList(certList) + + # Find the ROOT CAs if store isn't specified + if not rootCAs: + # Build cert store. + self.rootCAs = CertList([x for x in certList if x.isSelfSigned()]) + # And remove those certs from the list + for cert in self.rootCAs: + certList.remove(cert) else: - for root_candidate in certList: - if root_candidate.isSelfSigned(): - self.append(root_candidate) - certList.remove(root_candidate) - break - - if len(self) > 0: - while certList: - tmp_len = len(self) - for c in certList: - if c.isIssuerCert(self[-1]): - self.append(c) - certList.remove(c) - break - if len(self) == tmp_len: - # no new certificate appended to self - break - - def verifyChain(self, anchors, untrusted=None): + # Store cert store. + self.rootCAs = CertList(rootCAs) + # And remove those certs from the list if present (remove dups) + for cert in self.rootCAs: + if cert in certList: + certList.remove(cert) + + # Append our root CAs to the certList + certList.extend(self.rootCAs) + + # Super instantiate + super(CertTree, self).__init__(certList) + + @property + def tree(self): """ - Perform verification of certificate chains for that certificate. - A list of anchors is required. The certificates in the optional - untrusted list may be used as additional elements to the final chain. - On par with chain instantiation, only one chain constructed with the - untrusted candidates will be retained. Eventually, dates are checked. + Get a tree-like object of the certificate list """ - untrusted = untrusted or [] - for a in anchors: - chain = Chain(self + untrusted, a) - if len(chain) == 1: # anchor only - continue - # check that the chain does not exclusively rely on untrusted - if any(c in chain[1:] for c in self): - for c in chain: - if c.remainingDays() < 0: - break - if c is chain[-1]: # we got to the end of the chain - return chain - return None - - def verifyChainFromCAFile(self, cafile, untrusted_file=None): + # We store the tree object as a dictionary that contains children. + tree = [(x, []) for x in self.rootCAs] + + # We'll empty this list eventually + certList = list(self) + + # We make a list of certificates we have to search children for, and iterate + # through it until it's empty. + todo = list(tree) + + # Iterate + while todo: + cert, children = todo.pop() + for c in certList: + # Check if this certificate matches the one we're looking at + if c.isIssuerCert(cert) and c != cert: + item = (c, []) + children.append(item) + certList.remove(c) + todo.append(item) + + return tree + + def getchain(self, cert): """ - Does the same job as .verifyChain() but using the list of anchors - from the cafile. As for .verifyChain(), a list of untrusted - certificates can be passed (as a file, this time). + Return a chain of certificate that points from a ROOT CA to a certificate. """ - try: - with open(cafile, "rb") as f: - ca_certs = f.read() - except Exception: - raise Exception("Could not read from cafile") - anchors = [Cert(c) for c in split_pem(ca_certs)] + def _rec_getchain(chain, curtree): + # See if an element of the current tree signs the cert, if so add it to + # the chain, else recurse. + for c, subtree in curtree: + curchain = chain + [c] + # If 'cert' is issued by c + if cert.isIssuerCert(c): + # Final node of the chain ! + # (add the final cert if not self signed) + if c != cert: + curchain += [cert] + return curchain + else: + # Not the final node of the chain ! Recurse. + curchain = _rec_getchain(curchain, subtree) + if curchain: + return curchain + return None + + chain = _rec_getchain([], self.tree) + if chain is not None: + return CertTree(chain) + else: + return None - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + def verify(self, cert): + """ + Verify that a certificate is properly signed. + """ + # Check that we can find a chain to this certificate + if not self.getchain(cert): + raise ValueError("Certificate verification failed !") + + def show(self, ret: bool = False): + """ + Return the CertTree as a string certificate tree + """ + + def _rec_show(c, children, lvl=0): + s = "" + # Process the current CA + if c: + if not c.isSelfSigned(): + s += "%s [Not Self Signed]\n" % c.subject_str + else: + s += "%s [Self Signed]\n" % c.subject_str + s = lvl * " " + s + lvl += 1 + # Process all sub-CAs at a lower level + for child, subchildren in children: + s += _rec_show(child, subchildren, lvl=lvl) + return s + + showed = _rec_show(None, self.tree) + if ret: + return showed + else: + print(showed) + + def __repr__(self): + return "" % ( + len(self), + len(self.rootCAs), + ) + + +####### +# CMS # +####### + +# RFC3852 - return self.verifyChain(anchors, untrusted) - def verifyChainFromCAPath(self, capath, untrusted_file=None): +class CMS_Engine: + """ + A utility class to perform CMS/PKCS7 operations, as specified by RFC3852. + + :param store: a ROOT CA certificate list to trust. + :param crls: a list of CRLs to include. This is currently not checked. + """ + + def __init__( + self, + store: CertList, + crls: List[X509_CRL] = [], + ): + self.store = store + self.crls = crls + + def sign( + self, + message: Union[bytes, Packet], + eContentType: ASN1_OID, + cert: Cert, + key: PrivKey, + h: Optional[str] = None, + ): """ - Does the same job as .verifyChainFromCAFile() but using the list - of anchors in capath directory. The directory should (only) contain - certificates files in PEM format. As for .verifyChainFromCAFile(), - a list of untrusted certificates can be passed as a file - (concatenation of the certificates in PEM format). + Sign a message using CMS. + + :param message: the inner content to sign. + :param eContentType: the OID of the inner content. + :param cert: the certificate whose key to use use for signing. + :param key: the private key to use for signing. + :param h: the hash to use (default: same as the certificate's signature) + + We currently only support X.509 certificates ! """ - try: - anchors = [] - for cafile in os.listdir(capath): - with open(os.path.join(capath, cafile), "rb") as fd: - anchors.append(Cert(fd.read())) - except Exception: - raise Exception("capath provided is not a valid cert path") + # RFC3852 - 5.4. Message Digest Calculation Process + h = h or cert.getSignatureHashName() + hash = hashes.Hash(_get_hash(h)) + hash.update(bytes(message)) + hashed_message = hash.finalize() + + # 5.5. Signature Generation Process + signerInfo = CMS_SignerInfo( + version=1, + sid=CMS_IssuerAndSerialNumber( + issuer=cert.tbsCertificate.issuer, + serialNumber=cert.tbsCertificate.serialNumber, + ), + digestAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(h), + parameters=ASN1_NULL(0), + ), + signedAttrs=[ + CMS_Attribute( + attrType=ASN1_OID("contentType"), + attrValues=[ + eContentType, + ], + ), + CMS_Attribute( + attrType=ASN1_OID("messageDigest"), + # "A message-digest attribute MUST have a single attribute value" + attrValues=[ + ASN1_STRING(hashed_message), + ], + ), + ], + signatureAlgorithm=cert.tbsCertificate.signature, + ) + signerInfo.signature = ASN1_STRING( + key.sign( + bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + h=h, + ) + ) - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + # Build a chain of X509_Cert to ship (but skip the ROOT certificate) + certTree = CertTree(cert, self.store) + certificates = [x.x509Cert for x in certTree if not x.isSelfSigned()] + + # Build final structure + return CMS_ContentInfo( + contentType=ASN1_OID("id-signedData"), + content=CMS_SignedData( + version=3 if certificates else 1, + digestAlgorithms=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(h), + parameters=ASN1_NULL(0), + ), + encapContentInfo=CMS_EncapsulatedContentInfo( + eContentType=eContentType, + eContent=message, + ), + certificates=( + [CMS_CertificateChoices(certificate=cert) for cert in certificates] + if certificates + else None + ), + crls=( + [CMS_RevocationInfoChoice(crl=crl) for crl in self.crls] + if self.crls + else None + ), + signerInfos=[ + signerInfo, + ], + ), + ) + + def verify( + self, + contentInfo: CMS_ContentInfo, + eContentType: Optional[ASN1_OID] = None, + ): + """ + Verify a CMS message against the list of trusted certificates, + and return the unpacked message if the verification succeeds. - return self.verifyChain(anchors, untrusted) + :param contentInfo: the ContentInfo whose signature to verify + :param eContentType: if provided, verifies that the content type is valid + """ + if contentInfo.contentType.oidname != "id-signedData": + raise ValueError("ContentInfo isn't signed !") - def __repr__(self): - llen = len(self) - 1 - if llen < 0: - return "" - c = self[0] - s = "__ " - if not c.isSelfSigned(): - s += "%s [Not Self Signed]\n" % c.subject_str - else: - s += "%s [Self Signed]\n" % c.subject_str - idx = 1 - while idx <= llen: - c = self[idx] - s += "%s_ %s" % (" " * idx * 2, c.subject_str) - if idx != llen: - s += "\n" - idx += 1 - return s + signeddata = contentInfo.content + + # Build the certificate chain + certificates = [Cert(x.certificate) for x in signeddata.certificates] + certTree = CertTree(certificates, self.store) + + # Check there's at least one signature + if not signeddata.signerInfos: + raise ValueError("ContentInfo contained no signature !") + + # Check all signatures + for signerInfo in signeddata.signerInfos: + # Find certificate in the chain that did this + cert: Cert = certTree.findCertByIssuer(signerInfo.sid.get_issuer()) + + # Verify certificate signature + certTree.verify(cert) + + # Verify the message hash + if signerInfo.signedAttrs: + # Verify the contentType + try: + contentType = next( + x.attrValues[0] + for x in signerInfo.signedAttrs + if x.attrType.oidname == "contentType" + ) + + if contentType != signeddata.encapContentInfo.eContentType: + raise ValueError( + "Inconsistent 'contentType' was detected in packet !" + ) + + if eContentType is not None and eContentType != contentType: + raise ValueError( + "Expected '%s' but got '%s' contentType !" + % ( + eContentType, + contentType, + ) + ) + except StopIteration: + raise ValueError("Missing contentType in signedAttrs !") + + # Verify the messageDigest value + try: + # "A message-digest attribute MUST have a single attribute value" + messageDigest = next( + x.attrValues[0].val + for x in signerInfo.signedAttrs + if x.attrType.oidname == "messageDigest" + ) + + # Re-calculate hash + h = signerInfo.digestAlgorithm.algorithm.oidname + hash = hashes.Hash(_get_hash(h)) + hash.update(bytes(signeddata.encapContentInfo.eContent)) + hashed_message = hash.finalize() + + if hashed_message != messageDigest: + raise ValueError("Invalid messageDigest value !") + except StopIteration: + raise ValueError("Missing messageDigest in signedAttrs !") + + # Verify the signature + cert.verify( + msg=bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + sig=signerInfo.signature.val, + ) + else: + cert.verify( + msg=bytes(signeddata.encapContentInfo), + sig=signerInfo.signature.val, + ) + + # Return the content + return signeddata.encapContentInfo.eContent diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 1f1afe4f9c9..b9280be9660 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -7,14 +7,13 @@ # Cool history about this file: http://natisbad.org/scapy/index.html """ -X.509 certificates and other crypto-related ASN.1 structures +X.509 certificates, OCSP, CRL, CMS and other crypto-related ASN.1 structures """ from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1.asn1 import ( ASN1_Codecs, ASN1_IA5_STRING, - ASN1_NULL, ASN1_OID, ASN1_PRINTABLE_STRING, ASN1_UTC_TIME, @@ -37,6 +36,7 @@ ASN1F_ISO646_STRING, ASN1F_NULL, ASN1F_OID, + ASN1F_omit, ASN1F_optional, ASN1F_PACKET, ASN1F_PRINTABLE_STRING, @@ -218,6 +218,24 @@ class ECDSASignature(ASN1_Packet): ASN1F_INTEGER("s", 0)) +#################################### +# Diffie Hellman Exchange Packets # +#################################### +# based on PKCS#3 + +# PKCS#3 sect 9 + +class DHParameter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("p", 0), + ASN1F_INTEGER("g", 0), + ASN1F_optional( + ASN1F_INTEGER("l", 0) # aka. 'privateValueLength' + ), + ) + + #################################### # x25519/x448 packets # #################################### @@ -847,13 +865,66 @@ class X509_AlgorithmIdentifier(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("algorithm", "1.2.840.113549.1.1.11"), - ASN1F_optional( - ASN1F_CHOICE( - "parameters", ASN1_NULL(0), - ASN1F_NULL, - ECParameters, - DomainParameters, - ) + MultipleTypeField( + [ + ( + # RFC4055: + # "The correct encoding is to omit the parameters field" + # "All implementations MUST accept both NULL and absent + # parameters as legal and equivalent encodings." + + # RFC8017: + # "should generally be omitted, but if present, it shall have a + # value of type NULL." + ASN1F_optional(ASN1F_NULL("parameters", None)), + lambda pkt: ( + pkt.algorithm.val[:19] == "1.2.840.113549.1.1." or + pkt.algorithm.val[:21] == "2.16.840.1.101.3.4.2." or + pkt.algorithm.val[:11] == "1.3.14.3.2." + ) + ), + ( + # RFC5758: + # "the encoding MUST omit the parameters field" + + # RFC8410: + # "For all of the OIDs, the parameters MUST be absent." + ASN1F_omit("parameters", None), + lambda pkt: ( + pkt.algorithm.val[:16] == "1.2.840.10045.4." or + pkt.algorithm.val in ["1.3.101.112", "1.3.101.113"] + ) + ), + # RFC5480 + ( + ASN1F_PACKET( + "parameters", + ECParameters(), + ECParameters, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.10045.2.1", + ), + # RFC3279 + ( + ASN1F_PACKET( + "parameters", + DomainParameters(), + DomainParameters, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.10046.2.1", + ), + # PKCS#3 + ( + ASN1F_PACKET( + "parameters", + DHParameter(), + DHParameter, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.113549.1.3.1", + ), + ], + # Default: fail, probably. This is most likely unimplemented. + ASN1F_NULL("parameters", 0), ) ) @@ -969,6 +1040,33 @@ class ECDSAPrivateKey_OpenSSL(Packet): ] +class _IssuerUtils: + def get_issuer(self): + attrs = self.issuer + attrsDict = {} + for attr in attrs: + # we assume there is only one name in each rdn ASN1_SET + attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 + return attrsDict + + def get_issuer_str(self): + """ + Returns a one-line string containing every type/value + in a rather specific order. sorted() built-in ensures unicity. + """ + name_str = "" + attrsDict = self.get_issuer() + for attrType, attrSymbol in _attrName_mapping: + if attrType in attrsDict: + name_str += "/" + attrSymbol + "=" + name_str += attrsDict[attrType] + for attrType in sorted(attrsDict): + if attrType not in _attrName_specials: + name_str += "/" + attrType + "=" + name_str += attrsDict[attrType] + return name_str + + class X509_Validity(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( @@ -991,7 +1089,7 @@ class X509_Validity(ASN1_Packet): _attrName_specials = [name for name, symbol in _attrName_mapping] -class X509_TBSCertificate(ASN1_Packet): +class X509_TBSCertificate(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( @@ -1021,31 +1119,6 @@ class X509_TBSCertificate(ASN1_Packet): X509_Extension, explicit_tag=0xa3))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - def get_subject(self): attrs = self.subject attrsDict = {} @@ -1105,7 +1178,7 @@ class X509_RevokedCertificate(ASN1_Packet): None, X509_Extension))) -class X509_TBSCertList(ASN1_Packet): +class X509_TBSCertList(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( @@ -1125,31 +1198,6 @@ class X509_TBSCertList(ASN1_Packet): X509_Extension, explicit_tag=0xa0))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - class ASN1F_X509_CRL(ASN1F_SEQUENCE): def __init__(self, **kargs): @@ -1213,7 +1261,7 @@ class CMS_EncapsulatedContentInfo(ASN1_Packet): ASN1F_optional( _EncapsulatedContent_Field("eContent", None, explicit_tag=0xA0), - ) + ), ) @@ -1241,10 +1289,10 @@ class CMS_CertificateChoices(ASN1_Packet): # RFC3852 sect 10.2.4 -class CMS_IssuerAndSerialNumber(ASN1_Packet): +class CMS_IssuerAndSerialNumber(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_PACKET("issuer", X509_DirectoryName(), X509_DirectoryName), + ASN1F_SEQUENCE_OF("issuer", _default_issuer, X509_RDN), ASN1F_INTEGER("serialNumber", 0) ) @@ -1289,6 +1337,17 @@ class CMS_SignerInfo(ASN1_Packet): ) +# RFC3852 sect 5.4 + +class CMS_SignedAttrsForSignature(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF( + "signedAttrs", + None, + CMS_Attribute, + ) + + # RFC3852 sect 5.1 class CMS_SignedData(ASN1_Packet): diff --git a/scapy/libs/rfc3961.py b/scapy/libs/rfc3961.py index ed6581ceaff..e07d00c1df9 100644 --- a/scapy/libs/rfc3961.py +++ b/scapy/libs/rfc3961.py @@ -1445,3 +1445,35 @@ def prfplus(key, pepper): ) ), ) + + +############ +# RFC 4556 # +############ + +def octetstring2key(etype: EncryptionType, x: bytes) -> Key: + """ + RFC4556 octetstring2key:: + + octetstring2key(x) == random-to-key(K-truncate( + SHA1(0x00 | x) | + SHA1(0x01 | x) | + SHA1(0x02 | x) | + ... + )) + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + + out = b"" + count = 0 + while len(out) < ep.keysize: + out += Hash_SHA().digest(struct.pack("!B", count) + x) + count += 1 + + return Key.random_to_key( + etype=etype, + seed=out[:ep.keysize], + ) diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py index 87c591753bc..8f8e9bfdd6c 100644 --- a/scapy/modules/ticketer.py +++ b/scapy/modules/ticketer.py @@ -850,7 +850,7 @@ def get_cred(self, principal, etype=None): "Note principals are case sensitive, as on ktpass.exe" ) - def ssp(self, i): + def ssp(self, i, **kwargs): """ Create a KerberosSSP from a ticket or from the keystore. @@ -859,18 +859,31 @@ def ssp(self, i): """ if isinstance(i, int): ticket, sessionkey, upn, spn = self.export_krb(i) - return KerberosSSP( - ST=ticket, - KEY=sessionkey, - UPN=upn, - SPN=spn, - ) + if spn.startswith("krbtgt/"): + # It's a TGT + kwargs.setdefault("SPN", None) # Use target_name only + return KerberosSSP( + TGT=ticket, + KEY=sessionkey, + UPN=upn, + **kwargs, + ) + else: + # It's a ST + return KerberosSSP( + ST=ticket, + KEY=sessionkey, + UPN=upn, + SPN=spn, + **kwargs, + ) elif isinstance(i, str): spn = i key = self.get_cred(spn) return KerberosSSP( SPN=spn, KEY=key, + **kwargs, ) else: raise ValueError("Invalid 'i' value. Must be int or str") @@ -2424,6 +2437,9 @@ def request_tgt( fast=False, armor_with=None, spn=None, + x509=None, + x509key=None, + p12=None, **kwargs, ): """ @@ -2458,6 +2474,9 @@ def request_tgt( armor_ticket_upn=armor_ticket_upn, armor_ticket_skey=armor_ticket_skey, spn=spn, + x509=x509, + x509key=x509key, + p12=p12, **kwargs, ) if not res: @@ -2570,3 +2589,10 @@ def renew(self, i, ip=None, additional_tickets=[], **kwargs): return self.import_krb(res, _inplace=i) + + def iter_tickets(self): + """ + Iterate through the tickets in the ccache + """ + for i in range(len(self.ccache.credentials)): + yield self.export_krb(i) diff --git a/test/configs/cryptography.utsc b/test/configs/cryptography.utsc index 53b307d2897..b5267234bbf 100644 --- a/test/configs/cryptography.utsc +++ b/test/configs/cryptography.utsc @@ -15,7 +15,6 @@ "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" }, "kw_ko": [ - "mock", "needs_root" ] } diff --git a/test/contrib/ikev2.uts b/test/contrib/ikev2.uts index 9d51493daa1..21ef35fdfda 100644 --- a/test/contrib/ikev2.uts +++ b/test/contrib/ikev2.uts @@ -781,7 +781,7 @@ frames = [ subjectPublicKeyInfo=X509_SubjectPublicKeyInfo( signatureAlgorithm=X509_AlgorithmIdentifier( algorithm=ASN1_OID('ecPublicKey'), - parameters=ASN1_OID('prime256v1')), + parameters=ECParameters(curve=ASN1_OID('prime256v1'))), subjectPublicKey=ECDSAPublicKey( ecPoint=ASN1_BIT_STRING( '000001001011011101000101011100101010000110110101110111010001110' @@ -1125,7 +1125,7 @@ frames = [ subjectPublicKeyInfo=X509_SubjectPublicKeyInfo( signatureAlgorithm=X509_AlgorithmIdentifier( algorithm=ASN1_OID('ecPublicKey'), - parameters=ASN1_OID('prime256v1') + parameters=ECParameters(curve=ASN1_OID('prime256v1')), ), subjectPublicKey=ECDSAPublicKey( ecPoint=ASN1_BIT_STRING( diff --git a/test/contrib/send.uts b/test/contrib/send.uts index 80d892c0f02..c4ab0ee09cd 100644 --- a/test/contrib/send.uts +++ b/test/contrib/send.uts @@ -10,7 +10,7 @@ assert pkt[ICMPv6NDOptRsaSig].signature_pad == b"\x01" * 12 = ICMPv6NDOptCGA build and dissection -pkt = Ether()/IPv6()/ICMPv6ND_NS()/ICMPv6NDOptCGA(CGA_PARAMS=CGA_Params()) +pkt = Ether()/IPv6()/ICMPv6ND_NS()/ICMPv6NDOptCGA(CGA_PARAMS=CGA_Params(pubkey=X509_SubjectPublicKeyInfo(signatureAlgorithm=X509_AlgorithmIdentifier(parameters=0)))) pkt = Ether(raw(pkt)) assert ICMPv6NDOptCGA in pkt diff --git a/test/scapy/layers/dcerpc.uts b/test/scapy/layers/dcerpc.uts index e496b68719c..27e4d4a9f5c 100644 --- a/test/scapy/layers/dcerpc.uts +++ b/test/scapy/layers/dcerpc.uts @@ -878,6 +878,7 @@ rpcserver = MyRPCServer.spawn( iface=conf.loopback_name, port=12345, bg=True, + debug=4, ) = Functional: Connect to it with DCERPC_Client over NCACN_NP @@ -886,7 +887,7 @@ client = DCERPC_Client( DCERPC_Transport.NCACN_NP, ndr64=False, ) -client.connect(get_if_addr(conf.loopback_name), port=12345) +client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 4}) client.open_smbpipe("wkssvc") client.bind(find_dcerpc_interface("wkssvc")) @@ -931,6 +932,7 @@ rpcserver = MyRPCServer.spawn( ssp=ssp, port=12345, bg=True, + debug=4, ) = Functional: Connect to it with DCERPC_Client over NCACN_NP with NTLMSSP @@ -940,7 +942,7 @@ client = DCERPC_Client( ssp=ssp, ndr64=False, ) -client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 5}) +client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 4}) client.open_smbpipe("wkssvc") client.bind(find_dcerpc_interface("wkssvc")) diff --git a/test/scapy/layers/kerberos.uts b/test/scapy/layers/kerberos.uts index 0080964f995..9f6607bb076 100644 --- a/test/scapy/layers/kerberos.uts +++ b/test/scapy/layers/kerberos.uts @@ -32,6 +32,38 @@ assert pkt.root.eData.seq[3].padataValue == b"MIT" etype_info2 = pkt.root.eData.seq[1] assert etype_info2.padataValue.seq[0].salt == b'SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com' += Parse KRB-ERROR (2) + +# This one is a preauth one + +pkt = KerberosTCPHeader(b'\x00\x00\x01A~\x82\x01=0\x82\x019\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa4\x11\x18\x0f20251213001046Z\xa5\x05\x02\x03\x05F\x1f\xa6\x03\x02\x01\x19\xa9\x0e\x1b\x0cDOMAIN.LOCAL\xaa!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cdomain.local\xac\x81\xda\x04\x81\xd70\x81\xd40t\xa1\x03\x02\x01\x13\xa2m\x04k0i0/\xa0\x03\x02\x01\x12\xa1(\x1b&DOMAIN.LOCALhostcomputer1.domain.local0/\xa0\x03\x02\x01\x11\xa1(\x1b&DOMAIN.LOCALhostcomputer1.domain.local0\x05\xa0\x03\x02\x01\x170;\xa1\x03\x02\x01o\xa24\x042000\x0b\x06\t`\x86H\x01e\x03\x04\x02\x030\x0b\x06\t`\x86H\x01e\x03\x04\x02\x020\x0b\x06\t`\x86H\x01e\x03\x04\x02\x010\x07\x06\x05+\x0e\x03\x02\x1a0\t\xa1\x03\x02\x01\x02\xa2\x02\x04\x000\t\xa1\x03\x02\x01\x10\xa2\x02\x04\x000\t\xa1\x03\x02\x01\x0f\xa2\x02\x04\x00') + +assert Kerberos in pkt +assert len(pkt.root.eData.seq) == 5 +assert isinstance(pkt.root.eData.seq[0].padataValue, ETYPE_INFO2) +assert pkt.root.eData.seq[0].padataValue.seq[0].salt.val == b"DOMAIN.LOCALhostcomputer1.domain.local" +assert isinstance(pkt.root.eData.seq[1].padataValue, TD_CMS_DIGEST_ALGORITHMS) +assert [x.algorithm.oidname for x in pkt.root.eData.seq[1].padataValue.seq] == [ + "sha512", + "sha384", + "sha256", + "sha1", +] +assert pkt.root.eData.seq[2].padataType == 2 + += Parse KRB-ERROR (3) + +# This is a TKT EXPIRED + +pkt = KerberosTCPHeader(b'\x00\x00\x00{~y0w\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa4\x11\x18\x0f20251213001312Z\xa5\x05\x02\x03\r\xae\x86\xa6\x03\x02\x01 \xa9\x0e\x1b\x0cDOMAIN.LOCAL\xaa!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cDOMAIN.LOCAL\xac\x19\x04\x170\x15\xa1\x03\x02\x01\x03\xa2\x0e\x04\x0c3\x01\x00\xc0\x00\x00\x00\x00\x01\x00\x00\x00') + +assert Kerberos in pkt +assert pkt.root.errorCode == 0x20 +assert pkt.root.sname.nameString == [b"krbtgt", b"DOMAIN.LOCAL"] +assert isinstance(pkt.root.eData, KERB_ERROR_DATA) +assert pkt.root.eData.dataValue.status == 0xc0000133 +assert pkt.root.eData.dataValue.flags == 1 + = Parse AS-REP pkt = IP(b'E\x00\x05\x95\xff\xff@\x00\xff\x11\x00\x00\x7f\x00\x00\x15\x7f\x00\x00\x15\x00X;p\x05\x81\x00\x00k\x82\x05u0\x82\x05q\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0b\xa2H0F0D\xa1\x03\x02\x01\x13\xa2=\x04;0907\xa0\x03\x02\x01\x12\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com\xa3\x13\x1b\x11SAMBA.EXAMPLE.COM\xa4\x150\x13\xa0\x03\x02\x01\x00\xa1\x0c0\n\x1b\x08LOCALDC$\xa5\x82\x03\xafa\x82\x03\xab0\x82\x03\xa7\xa0\x03\x02\x01\x05\xa1\x13\x1b\x11SAMBA.EXAMPLE.COM\xa2&0$\xa0\x03\x02\x01\x02\xa1\x1d0\x1b\x1b\x06krbtgt\x1b\x11SAMBA.EXAMPLE.COM\xa3\x82\x03a0\x82\x03]\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x01\xa2\x82\x03O\x04\x82\x03K\t\x05\xd7\x91\xdc\x14\xaa\xe2\xfb\xcc\x85\x1f*?\xbau\xbc0\x0f\x80\x8bc\x87\xe5z\x1a4i\xa3\x9bL[-\xb1\xb7\xaa\xd9-\x01\xc2\xf2\xdfs\x17<\xf3&\x99\'1\xfa\x80\xd9\x02\xae\xf5\xb3S\x14\xc2L\xc3e\xc9\x94\x03dH\xe2\xa9\xfd\x9a\xc6\xffs\x10\xf3er\xbd\xa0\xfep[~\x82+\xde0\x91%tc\xdcx\xfe\xd0\xd8\xc4\xb6u\x91\xe7\xe1C\x00y\xb8\x15\xd9\x91j\x0f\xe7\xa0\xe24m\xd94\xe5.I\xc51\x8f\x1do\t\xe9\x98\xb8\xad\xa6\x92\xf3\x15f\xc98o\x92\x0ch\x08\\\x8f\xab\xfau\xaf\x19v\xcc\xcb!v\xb5v2\xeb(h\x1c+o\xea\xc3\x0b\xcf\x81\xc8\x89\xe8i\xdd?\xd1\xaa\x0f3\xc9\xe9\xf2\xd7\x8a\x93`\x02\x9d\xb2 LV\xda\x0f&>,~\xb3\xecK\xe76v\x9a\xc3\x88\xe3\rj\\/\xd6\x9e_X\x14z\xc2w\x1d.|\xbf\x18\x01\xc8`].\xd2\xc2\x1e\xd0\x89\x8f\xd2\x18\xb9U\xaf\x98\xe9V\xe2\x19\xa1\xbb\xc45\xd9\x16\x08c\xaf$\xef\xf2\xf4S\xeco\xa1\xa1\xe5)\x99\xc9b#[\xd1:O\xbej\xb91\xb3i\xbepb\x06\xd8\x14\xc3\xdf\xbb\x18\xbf]\xf1\x82+\x18*\x85D\xecy\x0eu_\xe2\xfa\xbcd\x82A>\x88p\xa2\xc1\xf6\x9c\x89Qj\xfdM\x99\xd1\x84r\x0fp\x06$\xab\xc2\xb5\xae4\xe8\xf1\xbb}\x98\xedWX\xe2*uB\x93\x11\x1c\xc7f\x1c\xce\xc9\xff\t\x88\x94\xddN\xcf\xa68O\x0c^I\x9ew\x81\xba\xc3\xbc\xa8\x07\x8b\xd4\xdf\x7f(\xc2\x15gX\xd0oN\x00u\x1aU@\xbd\xb8\xa9)Ur\x94\xc1\xcf\xa1\xd8k\xc1F\x19\xd3rR\xaa\x93\xe2\x06D#\x12\x07M\xe3\x15\xd6\xd0\xb3\xa6\x89\x0c\xfeLO6\xe6\xf0w\x1a\x80\x0f\xffO\xf2N\xf4(\n\xdb-\x96`\xa4\xb7\xd3g\x16\xbfY\xff\xad\x95\x19\xd9\x9cS\xaa\xe3\x06W\xf3\xc2\x18it5\xda\x1c\x99\x8a\xaf\xfa"MT\xc7$#j,P\x9b\xf9\r\xbbA\xd0w\x15.\xc3PC\xc4\xe7vL/\xca0h7\x1c4z\x8bS@\x0ej\xb4q\xde\x19\xd8so\x9c\xea\x8f^w7\x1e\x92\x1c\xcc\xe2\xa60\xe8\xce}\xee\xb1\x87F!n\x80\xe4l"\xed\xc2fI \xb9\t\x14\t\x8d\xect\xa4\xb48\xe0\xfd\xf3\xe5\x8es\xd2\x08;\x9f\xb2\xb8q\x1bX\xadd\xbb\x07z\x16\tZ\xb0z1+h\x0e\xf7\x98w\x0bX\xf0W\t\xa6\x86.\x1e\x9c\xc2\x9d\xac+\xca\xdf&\xa9\xf3\xcb\xa7\xca\x1fn\xe8\x8a]h\xf6\xeb\xe9\xd4\xa0\x16\x1b\xb4\x8d\xc7\xaf\xe3\xf0.\x85\x1e\xc2\xa5\xf2DhhgQ\xe0\xb8y\xb8\xbd\x98\xf8\xa0\rW\x93/\x07>0\xf5\x92Y\x15Y\x0bD\xdb\xd6\xac#\xd8z\xbdeY\x87\xf2\x97\xfdZ\x0c\x1d\xbc\xefXONv\xc9\xfdp\xdd^\x16\x83\xc3\xeb\x9e\x96+\xe8\xed\x0c<$\x83A\xeb\xc6e\x94\x0c\x11\x19\xb4\x99\xcd\x17\xeb\xcb.\x0b}\x01i\x88\x03R\xde\x1a\xea\x03\x10\xa9Z\x8e\xf7\x87\r\xa6\x08@\xf7\x96\xc8\xa5g\xde\x8dE\xf8\xb0\xe8\xe6T\x80=\x0cm\xe0z\xa5\x03\xa2X\xed\'\x17\x001O\xee\xfb\x87\xbe\xf7\xbbS\xc1p\xaeZ\x17\x92}\xc2\x07\x01\x81\xaew\xd9\xc5\x9c\xe5k\x8d+\x13\xd2\x00Q\xd4\xe5M\x9d\x06\xc7)\xac\x06\xb2+\xd1\x83\xcb\xfe\xb9\xf9\x0bbRN\x04\xe7\xd8\xa0\xf9\xe3\xc3m\x18\xc4\x108\xfa\xa6\x82\x01:0\x82\x016\xa0\x03\x02\x01\x12\xa2\x82\x01-\x04\x82\x01)/pDi\x13\xee\x0b\x8ehN2\x01P\x19|\xda\x1a\xde\xec\xde\rt\xcbe7\x00-sG&\x8b\xfc\xa4\x92~~[,\xd5\rAj\xd6[\xbe\xeeB\xf8X\\x\xa6$Z\x83\xf6\x1bq\xc5\x8fm\\\x94\xd7l\xc5\x89#\xcb\xcd\xaf\xff\x15\x1b\x8f;7\xb0\xc8u\x19\xb1\xd0\xb0\x93\xa7z\x9cz\x14\x0b\x86q\x01\xb8<\xa7\xa4\xceb\x1f\x88\x14\xe3S0\xe3]\xa5\x9b\xa0\x0e\x97#\x87\x9a\xe0\x90a\xdfj.\x1e6x\x87GV\xc0/\xa4\xab}\xdbS\xd5\xff\x03\x03\xae\x18n\x1aQ\r\x7fP\xdb\xfe\xe9\xeb\xab2\x9dws9\xf5\xcb\x94\xab\xc1\x9e\xbd\x08\x0f\xfcx\x18\x1b\xf8\x1f\xf2\'\x18-\xe4"\x93vuTf3\xbdj\xb6\x88%\x8a\x94\xd12\xfbY\x0f\x81R\xd3\xf1\x9b\xd5Z\x1f3o\xb7\xc3\x82\x14\t\x87\xac#\x89\x13M\x803\x88/\x92==S$\xa3\xe9\xf5C{\xd7\x0f\t^k\xb0\x0e\xe6\x8d\x8f!\x91+\x19\xb2y$\xc6\x1bN;\xfehA\x1f\x9f"\r\xe8\xda\xce\x00\xe7g\xb6b17\x06s\rM\xc8S\x9b0\x9f\xc7^l\xa4\xca\xe4p\xcd\xf1,\xc3\xcf\xb1\x91Hn>^\xb8\xc8\x07#\xb2\xb0G;\x07\xe4\xeaM8T\x87\xdd0=\xf2\xdb\x8d1\xf8\xc9\rS\xc4\xad\xcf9\xadx\xcfl\x85\xfb\xb8{LN\xe51\xa4,!3\xdf+\x03b\x13#t\xdf\x99T \xe4\xb2\xa6\xd1\xe1\x9dxy\xd5\x18e-Q\x01\xa3\x16\x96+\'\xb3\x88L\xb6}\x07W/\x96\xb9f\x8c\xa4,\xcas\x11\xa7\x15*\xc7\xc6\xd4\x92\x00\x91\x92\xfaJpy\x89\xe4;*\x10\xf1\x9eS^|\xf8\xaf\xda\xf6<\xe9\xa2\xa8\\\xe1\xbd\x17\xd8\x1c\xfev\xd2\xceWY\xa7\xfd\xbe\xffo\xb2y\xb8b\x0b\xc2\xc5\x18;$\xbe\x83\x1c~\xe1W\x11O\'\x00\xda!\x0b6\xed\xb7\xbd\xa7\xd9\x1a2\xf7\x94\x0b\xefC\x1cvW\x1c\xd4D\x99\xf7y\xccN\xbe\x82\x9f\xb3N\xea\xa1\xe4B$\rYb\xbd\xbc\xbc\x16\xc9b\x97KTn\x9c\xee8\r\xdaI\xf6Q\xac\xc5\xc5\x8a\xca\xe4\xad\x06\xd5~K\x91\xd8\xc5Use\xe8\xdd\xda~\xe9U\tc\xd7\rOV\xb4O\xc5\xa2n)\xb3l\xb2\x1d\x11"\x18%\xb5\xa2!|\xb1\xf1EM4\xd9J\x85\\\xb8`\xf2\xfeCh\x1e=0.~\x12Bs\xdd\x18\xb0O\xdd\xf6`\xb8\x85\x8e\x1ex\xd0"\xcc\x03\xf4g\xf3\xcf\x1an]\xf5;\xb81yEB\xb1\xd0\x8e8\xd3\xbf\xb0\xbf.[\xa6\xf7Z\x0fw\xd5k\xf2\x92K\x14O\xff<\x87\xeczW\xbf\xf3E\xee\x8aD\x96gm8\xc9E<8\xe6E!\xdb-\xe6\xd6E*\xa8\xf3\xda\x16u\x13N\x8d\x90\xcb\xb0\xd2t\xcea\x89V?\xd9\xa5nV\xa8\x00f\x1ex{\x089Pb05\xdd\xee\xb2\xfb\x84\xf6\xfb%\x07\xf2\xc1W\xe7N\x81\xa8\x19p\xe1\x14u\xce\x92n9:U\xb0kw\xc4D\xdb\xd26\x88\xe8\xa7|\x7f03xt\xfe\xf7\x87\xa1\x87\xfc\xaf\xd7:ZH7\xc8\xe3\xe6\x07\x120\x85\x97\xffr\xea.\xda\xe6\x9c\x94\x02\xadz\xe8\x1a\xbb>\x91\x00\xf0\xc8{\x99\xb2VBF\xbdV\xaf\x8em\x0e\xcf)(\xe5\x15\x12\x18\xf7\xe6\'\xc5e\xe1U@foO|\x0e\x93|-\x0e\x84x/\xcb\x1bS^YolN\n\xed|\x1d5\x0e\x16\x9d\x04_.\xaa\xa4\xbb/\x94\xcd\x14\x95v\xf85\xe5\xee\xcbD\x18g}\x04D\xe5\x1f\xaf\xcb\xed*\xfa\xc5\x0b\x1d2\x0b\xc2#\xd2b6\x01\xae\xe6\xdfj6:$)K\xfb;\x00\xf2f\x8d\xfc@N\x9f\xa1\x7f\xe96\xe6b\x07V\xa6\x91\x8f}\xe2\xde4?8\x0f\xab\x83\xfd\xe9\x11\x12K\xe5\x08\xa4\x82\x01\\0\x82\x01X\xa0\x03\x02\x01\x12\xa2\x82\x01O\x04\x82\x01K\\>\t\xe4\x1d8,a(\x7f\x1e\xd2\x8dHH\x9c\xa3\x03?&\xb9\xf4\xba\xef\xcf\xcf\xb6(8\x91\x0f\xa3lq\xc6 f&Ou\xd8Bk\xe84s\xf1\xec\xf6\x97wY\xc6Un;\xf5\xdeh\xb9J\xd6\xaf\xf4r\x00\x80\x17\x8d\xc4p\x81\xac\x89\xf1\xf6\x98\xef\x1f\xb3\xe5\x91}\xf5m\x1a\xbd\x08\x1d\x0217W0\x81\xddZ\xec,J%\xe2o\x86\xef{"a\xe0\xe2hBc\xeb^\x8b\xa3\x8c\xf7W\xf9F\xc6&\x1a\x041\x0c\xdf\xc3S\xaa>\x04\x90\xd7\x8a\xdd\xf3j\x80#4_\x95u\xaby3\x0f\x878\xe3\',t\xa7\xe9\xba7&\xd6\x82y\x1d9\x06\xf1\xff\xaf\xb33O\xdb\x00\xc5\x19\xd0\xb7\t\xe9\xeb\xe0iv\x08\xaa\xf4\x00\xcaG\xbb7\xb9P\xcd\xcf\xcbC\x9b\xec\xfdH\x1b\xbf\x89\x11\x96L\xa8\xb4\\6\xcf\x9a\xa6\x16\xf0\xfb,\xaf\x06.qj\xf0\x03\xfd\xc0 \x80\xb6\xb84\xcf\xec\tW~5\xad,\x14-\xf05\x04\xb2\xd4[o\xce\xa3\xf9\x06\x08\x0e\xeb\x1e\xbf2\xd7\xe4\xc2\x14\xabn_\x0c8j;#\r\xee\xce\xa6\x1f\xc3+\xed\x0c\xb7\xabdb\xb4\x8b\xb2\xd0\xe97\xa5P\xcd\xf1\x96\x8aT:=\xfc\xd9\x1e\xb6q\xcdM\x16\xead\x81\x84/\xab\xdd\xc8\xe1\xed\x17\xa3\xf5\x1c\xf1\x98\xf1\xf7\xbd\xbc\xc8\xdf' + = GSS_Accept_sec_context (SPNEGO_negTokenResp: KRB_AP_REQ->KRB_AP_REP) with KrbRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 0 +assert negState == 0 assert isinstance(tok, SPNEGO_negToken) tok = SPNEGO_negToken(bytes(tok)) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult == 0 +assert tok.token.negState == 0 assert tok.token.supportedMech.oid == '1.2.840.48018.1.2.2' assert isinstance(tok.token.responseToken, SPNEGO_Token) -assert tok.token.mechListMIC is not None +assert tok.token.mechListMIC is None ap_rep = tok.token.responseToken.value.root assert isinstance(ap_rep, KRB_AP_REP) @@ -1478,15 +1647,15 @@ assert apreppart.subkey.keytype == 17 # Hardcode (yes this will probably require updating this test) bytes(tok) -assert bytes(tok) == b'\xa1\x81\xa90\x81\xa6\xa0\x03\n\x01\x00\xa1\x0b\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\xa2r\x04pon0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x03\x02\x01\x12\xa2W\x04UaS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd\xd3\xc3\xd9\xadN`\xd2;\xd7{\xb7\xf4p.\xa9\x9a\xb1}D\xc6|_t\n\r"M\xcd\xe2\t\xf0Ri\xc7\xcf\xb5\xefr9\xf0`iS7N\x06qKP\x06\xde\xc4\x18\xd5_\xcb\x0ct\x03k\xbc\xb9\x1adT\x03\xc1\x8bM\xa3\x1e\x04\x1c\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x17F\x8al\x01c\x00\xcf4\x12oI' +assert bytes(tok) == b'\xa1\x81\x890\x81\x86\xa0\x03\n\x01\x00\xa1\x0b\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\xa2r\x04pon0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x03\x02\x01\x12\xa2W\x04UaS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd\xd3\xc3\xd9\xadN`\xd2;\xd7{\xb7\xf4p.\xa9\x9a\xb1}D\xc6|_t\n\r"M\xcd\xe2\t\xf0Ri\xc7\xcf\xb5\xefr9\xf0`iS7N\x06qKP\x06\xde\xc4\x18\xd5_\xcb\x0ct\x03k\xbc\xb9\x1adT\x03\xc1\x8bM' = GSS_Init_sec_context (SPNEGO_negToken: KRB_AP_REP->OK) with KrbRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) assert tok is None -assert negResult == 0 +assert negState == 0 assert clicontext.KrbSessionKey.key == srvcontext.KrbSessionKey.key assert srvcontext.KrbSessionKey.key == b'0000000000000000' @@ -1524,8 +1693,8 @@ sig = server.GSS_GetMICEx( ] ) assert isinstance(sig, KRB_InnerToken) and sig.TOK_ID == b"\x04\x04" -assert sig.root.SND_SEQ == 1 -assert bytes(sig) == b'\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x01G\x81\x93\xb9\x92\xd0NvHH\xf6\x9c' +assert sig.root.SND_SEQ == 0 +assert bytes(sig) == b'\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x98\xdeb<\x14\x1c\x9fe%{\x0e\xf7' client.GSS_VerifyMICEx( clicontext, [ @@ -1575,7 +1744,7 @@ server = KerberosSSP( = GSS_Init_sec_context (KRB_AP_REQ) - DCE_STYLE with KrbRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context( + clicontext, tok, negState = client.GSS_Init_sec_context( None, req_flags=( GSS_C_FLAGS.GSS_C_DCE_STYLE | @@ -1586,7 +1755,7 @@ with KrbRandomPatcher(): ) ) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, KRB_AP_REQ) ap_req = KRB_AP_REQ(bytes(tok)) assert isinstance(ap_req, KRB_AP_REQ) @@ -1611,9 +1780,9 @@ assert bytes(tok) == b'n\x82\x06\x1d0\x82\x06\x19\xa0\x03\x02\x01\x05\xa1\x03\x0 = GSS_Accept_sec_context (KRB_AP_REQ->KRB_AP_REP) - DCE_STYLE with KrbRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, KRB_AP_REP) ap_rep = KRB_AP_REP(bytes(tok)) @@ -1629,9 +1798,9 @@ assert bytes(tok) == b'on0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x = GSS_Init_sec_context (SPNEGO_negToken: KRB_AP_REP->KRB_AP_REP) - DCE_STYLE with KrbRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) -assert negResult == 0 +assert negState == 0 assert isinstance(tok, KRB_AP_REP) ap_rep = KRB_AP_REP(bytes(tok)) @@ -1645,9 +1814,9 @@ assert bytes(tok) == b'oQ0O\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2C0A\xa0\x = GSS_Accept_sec_context (KRB_AP_REP->OK) - DCE_STYLE with KrbRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(srvcontext, tok) -assert negResult == 0 +assert negState == 0 assert tok is None diff --git a/test/scapy/layers/msnrpc.uts b/test/scapy/layers/msnrpc.uts index 7b31bf85421..f678d7f6274 100644 --- a/test/scapy/layers/msnrpc.uts +++ b/test/scapy/layers/msnrpc.uts @@ -40,7 +40,7 @@ EncryptedMessage = bytes.fromhex("c930c9a079d95c78bea6a3150908c11f4b68e41219bcb9 # Perform the same operation using NetlogonSSP: client = NetlogonSSP(SessionKey=SessionKey, computername="DC1", domainname="DOMAIN", AES=True) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) with mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=lambda x: Confounder): _msgs, sig = client.GSS_WrapEx( @@ -67,7 +67,7 @@ FullNetlogonSignatureHeader = bytes.fromhex("13001a00ffff00005d69950dfde45ae9f09 # Perform the same operation using NetlogonSSP: client = NetlogonSSP(SessionKey=SessionKey, computername="DC1", domainname="DOMAIN", AES=True) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) with mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=lambda x: Confounder): _msgs, sig = client.GSS_WrapEx( @@ -287,9 +287,9 @@ server = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00", computernam = [NetlogonSSP] - RC4 - GSS_Init_sec_context (NL_AUTH_MESSAGE) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, NL_AUTH_MESSAGE) assert tok.MessageType == 0 assert tok.Flags == 3 @@ -299,9 +299,9 @@ assert bytes(tok) == b'\x00\x00\x00\x00\x03\x00\x00\x00DOMAIN\x00DC1\x00' = [NetlogonSSP] - RC4 - GSS_Accept_sec_context (NL_AUTH_MESSAGE->NL_AUTH_MESSAGE) -srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) +srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 0 +assert negState == 0 assert tok.MessageType == 1 bytes(tok) @@ -309,9 +309,9 @@ assert bytes(tok) == b'\x01\x00\x00\x00\x00\x00\x00\x00' = [NetlogonSSP] - RC4 - GSS_Init_sec_context (NL_AUTH_MESSAGE->OK) -clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) +clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) -assert negResult == 0 +assert negState == 0 assert tok is None = [NetlogonSSP] - RC4 - GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload @@ -400,9 +400,9 @@ server = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x = [NetlogonSSP] - AES - GSS_Init_sec_context (NL_AUTH_MESSAGE) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, NL_AUTH_MESSAGE) assert tok.MessageType == 0 assert tok.Flags == 3 @@ -412,9 +412,9 @@ assert bytes(tok) == b'\x00\x00\x00\x00\x03\x00\x00\x00DOMAIN\x00DC1\x00' = [NetlogonSSP] - AES - GSS_Accept_sec_context (NL_AUTH_MESSAGE->NL_AUTH_MESSAGE) -srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) +srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 0 +assert negState == 0 assert tok.MessageType == 1 bytes(tok) @@ -422,9 +422,9 @@ assert bytes(tok) == b'\x01\x00\x00\x00\x00\x00\x00\x00' = [NetlogonSSP] - AES - GSS_Init_sec_context (NL_AUTH_MESSAGE->OK) -clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) +clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) -assert negResult == 0 +assert negState == 0 assert tok is None = [NetlogonSSP] - AES - GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload diff --git a/test/scapy/layers/ntlm.uts b/test/scapy/layers/ntlm.uts index 83b66197297..8bc38dceb66 100644 --- a/test/scapy/layers/ntlm.uts +++ b/test/scapy/layers/ntlm.uts @@ -158,7 +158,7 @@ server = SPNEGOSSP([ = GSS_Init_sec_context (negTokenInit: NTLM_NEGOTIATE) -clicontext, tok, negResult = client.GSS_Init_sec_context( +clicontext, tok, negState = client.GSS_Init_sec_context( None, req_flags=( GSS_C_FLAGS.GSS_C_MUTUAL_FLAG | @@ -166,7 +166,7 @@ clicontext, tok, negResult = client.GSS_Init_sec_context( GSS_C_FLAGS.GSS_C_CONF_FLAG ) ) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, GSSAPI_BLOB) tok = GSSAPI_BLOB(bytes(tok)) assert tok.MechType.val == '1.3.6.1.5.5.2' @@ -192,13 +192,13 @@ assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x0 = GSS_Accept_sec_context (SPNEGO_negTokenResp: NTLM_NEGOTIATE->NTLM_CHALLENGE) with NTLMRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, SPNEGO_negToken) tok = SPNEGO_negToken(bytes(tok)) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult == 1 +assert tok.token.negState == 1 assert tok.token.supportedMech.oid == '1.3.6.1.4.1.311.2.2.10' assert isinstance(tok.token.responseToken, SPNEGO_Token) assert tok.token.mechListMIC is None @@ -216,41 +216,42 @@ assert ntlm_chall.getAv(0) = GSS_Init_sec_context (SPNEGO_negToken: NTLM_CHALLENGE->NTLM_AUTHENTICATE) with NTLMRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) assert isinstance(tok, SPNEGO_negToken) tok = SPNEGO_negToken(bytes(tok)) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult is None +assert tok.token.negState is None assert tok.token.supportedMech is None assert isinstance(tok.token.mechListMIC, SPNEGO_MechListMIC) -sig = NTLMSSP_MESSAGE_SIGNATURE(tok.token.mechListMIC.value.val) +sig = tok.token.mechListMIC.value +assert isinstance(sig, NTLMSSP_MESSAGE_SIGNATURE) assert sig.Version == 1 assert sig.SeqNum == 0 assert isinstance(tok.token.responseToken, SPNEGO_Token) -ntlm_auth = NTLM_Header(tok.token.responseToken.value.val) +ntlm_auth = tok.token.responseToken.value assert isinstance(ntlm_auth, NTLM_AUTHENTICATE_V2) assert ntlm_auth.NegotiateFlags == 0xe2898235 assert ntlm_auth.UserName == "User1" assert ntlm_auth.DomainName == "DOMAIN" assert ntlm_auth.Workstation == "WIN10" assert ntlm_chall.TargetInfo[:6] == ntlm_auth.NtChallengeResponse.AvPairs[:6] -assert ntlm_auth.NtChallengeResponse.TimeStamp == ntlm_chall.getAv(7).Value assert ntlm_auth.NtChallengeResponse.getAv(6).Value == 2 assert ntlm_auth.NtChallengeResponse.getAv(9).Value == "host/WIN10" = GSS_Accept_sec_context (SPNEGO_negToken: NTLM_AUTHENTICATE->OK) -srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok) -assert negResult == 0 # success :p +srvcontext, tok, negState = server.GSS_Accept_sec_context(srvcontext, tok) +assert negState == 0, negState # success :p assert isinstance(tok, SPNEGO_negToken) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult == 0 +assert tok.token.negState == 0 assert tok.token.supportedMech is None assert tok.token.responseToken is None assert isinstance(tok.token.mechListMIC, SPNEGO_MechListMIC) -sig = NTLMSSP_MESSAGE_SIGNATURE(tok.token.mechListMIC.value.val) +sig = tok.token.mechListMIC.value +assert isinstance(sig, NTLMSSP_MESSAGE_SIGNATURE) assert sig.Version == 1 assert sig.SeqNum == 0 @@ -390,7 +391,6 @@ server = SPNEGOSSP( }, ), ], - force_supported_mechtypes=tok0.innerToken.token.mechTypes ) = Real exchange - Parse token 1 from client @@ -402,8 +402,8 @@ b"\x04\x28\x4e\x54\x4c\x4d\x53\x53\x50\x00\x01\x00\x00\x00\x97\x82" \ b"\x08\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ b"\x00\x00\x0a\x00\x61\x4a\x00\x00\x00\x0f") -srvcontext, _, negResult = server.GSS_Accept_sec_context(None, tok1) -assert negResult == 1 +srvcontext, _, negState = server.GSS_Accept_sec_context(None, tok1) +assert negState == 1 = Real exchange - Inject token 2 from server @@ -425,7 +425,7 @@ b"\x00\x02\xea\x8e\xe8\xd2\x8d\xd9\x01\x00\x00\x00\x00") tok2.token.responseToken.value.show() # Inject challenge token -srvcontext.sub_context.chall_tok = tok2.token.responseToken.value +srvcontext.ssp_context.chall_tok = tok2.token.responseToken.value = Real exchange - Parse token 3 from client @@ -462,8 +462,8 @@ b"\x47\xdc\xcd\xb5\x5e\x13\x62\xa3\x12\x04\x10\x01\x00\x00\x00\x0f" \ b"\x96\x54\xbb\x55\xd0\x6c\xcb\x00\x00\x00\x00") # Parse auth -srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok3) -assert negResult == 0 +srvcontext, tok, negState = server.GSS_Accept_sec_context(srvcontext, tok3) +assert negState == 0 = Real exchange - Check mechListMIC against token 4 from server diff --git a/test/scapy/layers/smb.uts b/test/scapy/layers/smb.uts index 6afcd4e9c60..1b90f1c348d 100644 --- a/test/scapy/layers/smb.uts +++ b/test/scapy/layers/smb.uts @@ -105,7 +105,7 @@ from scapy.layers.ntlm import * smb_sax_resp_1 = Ether(b"\x00\x0c)a\xf5_\x00PV\xc0\x00\x01\x08\x00E\x00\x01,\x03I@\x00\x80\x06\xe6\xaa\xc0\xa8\xc7\x01\xc0\xa8\xc7\x85\x00\x8b\xc2\x08\x10]}F\xd7\xcb\xefiP\x18\x00\xff\xeb)\x00\x00\x00\x00\x01\x00\xffSMBs\x16\x00\x00\xc0\x98\x07\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xfe\x00\x08\x10\x00\x04\xff\x00\x00\x01\x00\x00\x93\x00\xd5\x00\xa1\x81\x900\x81\x8d\xa0\x03\n\x01\x01\xa1\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2x\x04vNTLMSSP\x00\x02\x00\x00\x00\x06\x00\x06\x008\x00\x00\x00\x15\x82\x8a\xe2\x88\xbc\x9bX4\xbe7\r\x00\x00\x00\x00\x00\x00\x00\x008\x008\x00>\x00\x00\x00\x06\x03\x80%\x00\x00\x00\x0fS\x00C\x00V\x00\x02\x00\x06\x00S\x00C\x00V\x00\x01\x00\x06\x00S\x00C\x00V\x00\x04\x00\x06\x00S\x00C\x00V\x00\x03\x00\x06\x00S\x00C\x00V\x00\x07\x00\x08\x00\xd5\x9d6\x9b\x84'\xd2\x01\x00\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x009\x006\x000\x000\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x006\x00.\x003\x00\x00\x00") assert SMBSession_Setup_AndX_Response_Extended_Security in smb_sax_resp_1 assert smb_sax_resp_1.AndXCommand == 255 -assert smb_sax_resp_1.SecurityBlob.token.negResult == 1 +assert smb_sax_resp_1.SecurityBlob.token.negState == 1 assert isinstance(smb_sax_resp_1.SecurityBlob.token.responseToken.value, NTLM_CHALLENGE) ntlm_challenge = smb_sax_resp_1.SecurityBlob.token.responseToken.value assert len(ntlm_challenge.Payload) == 2 @@ -130,8 +130,8 @@ assert SMBSession_Setup_AndX_Request_Extended_Security in smb_sax_req_2 assert smb_sax_req_2.Flags2.EXTENDED_SECURITY assert smb_sax_req_2.Flags2.UNICODE assert smb_sax_req_2.AndXCommand == 255 -assert smb_sax_req_2.SecurityBlob.token.negResult == 1 -ntlm_authenticate = NTLM_Header(smb_sax_req_2.SecurityBlob.token.responseToken.value.val) +assert smb_sax_req_2.SecurityBlob.token.negState == 1 +ntlm_authenticate = smb_sax_req_2.SecurityBlob.token.responseToken.value assert isinstance(ntlm_authenticate, NTLM_AUTHENTICATE) assert len(ntlm_authenticate.Payload) == 3 assert ntlm_authenticate.Payload[0] == ('Workstation', 'DESKTOP-V1FA0UQ') @@ -144,8 +144,10 @@ assert ntlm_authenticate.Payload[2][1] == b'/\t\x13+\x81\xa6\x15\x14\xb9\x11\x8b smb_sax_resp_2 = Ether(b'\x00\x0c)a\xf5_\x00PV\xc0\x00\x01\x08\x00E\x00\x00\xb6\x03J@\x00\x80\x06\xe7\x1f\xc0\xa8\xc7\x01\xc0\xa8\xc7\x85\x00\x8b\xc2\x08\x10]~J\xd7\xcb\xf0YP\x18\x00\xfeB\x10\x00\x00\x00\x00\x00\x8a\xffSMBs\x00\x00\x00\x00\x98\x07\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xfe\x00\x08 \x00\x04\xff\x00\x8a\x00\x00\x00\x1d\x00_\x00\xa1\x1b0\x19\xa0\x03\n\x01\x00\xa3\x12\x04\x10\x01\x00\x00\x00\xee\t\x91S\xab\x7f]\xe6\x00\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x009\x006\x000\x000\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x006\x00.\x003\x00\x00\x00') assert SMBSession_Setup_AndX_Response_Extended_Security in smb_sax_resp_2 -assert smb_sax_resp_2.SecurityBlob.token.negResult == 0 -assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.val == b'\x01\x00\x00\x00\xee\t\x91S\xab\x7f]\xe6\x00\x00\x00\x00' +assert smb_sax_resp_2.SecurityBlob.token.negState == 0 +assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.Version == 1 +assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.Checksum == b'\xee\t\x91S\xab\x7f]\xe6' +assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.SeqNum == 0 assert smb_sax_resp_2.NativeOS == 'Windows 8.1 9600' assert smb_sax_resp_2.NativeLanMan == 'Windows 8.1 6.3' diff --git a/test/scapy/layers/spnego.uts b/test/scapy/layers/spnego.uts new file mode 100644 index 00000000000..ff04b448091 --- /dev/null +++ b/test/scapy/layers/spnego.uts @@ -0,0 +1,317 @@ +% SPNEGO unit tests + ++ Special SPNEGO tests + += SPNEGOSSP - test raw fallback + +% A SPNEGOSSP server talking to a non SPNEGOSSP client should work. + +srvssp = SPNEGOSSP([KerberosSSP(), NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")})]) +clissp = NTLMSSP(UPN="User1", PASSWORD="Password123!") + +clictx, tok, status = clissp.GSS_Init_sec_context(None) +assert status == GSS_S_CONTINUE_NEEDED, status +srvctx, tok, status = srvssp.GSS_Accept_sec_context(None, tok) +assert status == GSS_S_CONTINUE_NEEDED, status +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_COMPLETE, status +srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) +assert status == GSS_S_COMPLETE, status +assert tok is None, repr(tok) + += SPNEGOSSP - SSP negotiation + mechListMIC + +% 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!"), +]) + +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) == 1 +assert tok.innerToken.token.mechTypes[0].oid.val == '1.3.6.1.4.1.311.2.2.10' +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 isinstance(tok.token.responseToken.value, NTLM_CHALLENGE) + +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 + +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 - SSP negotiation + mechListMIC - NegTokenInit2 + +% Same but with NegTokenInit2 + +srvssp = SPNEGOSSP([ + KerberosSSP(), + NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) +]) +clissp = SPNEGOSSP([ + NTLMSSP(UPN="User1", PASSWORD="Password123!"), +]) + +srvctx, tok = srvssp.NegTokenInit2() +assert tok.MechType.val == '1.3.6.1.5.5.2' +assert [x.oid.val for x in tok.innerToken.token.mechTypes] == [ + '1.2.840.48018.1.2.2', + '1.2.840.113554.1.2.2', + '1.3.6.1.4.1.311.2.2.10', +] +assert tok.innerToken.token.reqFlags is None +assert tok.innerToken.token.mechToken is None +assert tok.innerToken.token.negHints.hintName.val == "not_defined_in_RFC4178@please_ignore" +assert tok.innerToken.token.mechListMIC is None +assert tok.innerToken.token._mechListMIC is None + +clictx, tok, status = clissp.GSS_Init_sec_context(None, tok) +assert clictx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert len(tok.innerToken.token.mechTypes) == 1 +assert tok.innerToken.token.mechTypes[0].oid.val == '1.3.6.1.4.1.311.2.2.10' +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(srvctx, 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 isinstance(tok.token.responseToken.value, NTLM_CHALLENGE) + +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 + +# INJECT FAULT: drop mechListMIC here, and make sure that the server doesn't let it go through. +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.from_cli_arguments - Utils + +from unittest import mock + +# Detect password prompts +def password_failure(*args, **kwargs): + raise ValueError("Password was prompted unexpectedly !") + +def password_input(*args, **kwargs): + return "Password" + + +def test_pwfail(**kwargs): + """Password means failure""" + with mock.patch('prompt_toolkit.prompt', side_effect=password_failure): + return SPNEGOSSP.from_cli_arguments(**kwargs) + + +def test_pwinput(**kwargs): + """Password is entered""" + with mock.patch('prompt_toolkit.prompt', side_effect=password_input): + return SPNEGOSSP.from_cli_arguments(**kwargs) + += SPNEGOSSP.from_cli_arguments - Username + Password - With input + +ssp = test_pwinput( + UPN="Administrator", + target="machine.domain.local", +) +assert isinstance(ssp, SPNEGOSSP) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' + += SPNEGOSSP.from_cli_arguments - Username + Password - With prompt + +try: + test_pwfail( + UPN="Administrator", + target="machine.domain.local", + ) + assert False, "Should have prompted for password !" +except ValueError: + pass + += SPNEGOSSP.from_cli_arguments - Username + Password - No input + +ssp = test_pwfail( + UPN="Administrator", + target="machine.domain.local", + password="Password", +) +assert isinstance(ssp, SPNEGOSSP) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' + += SPNEGOSSP.from_cli_arguments - UPN + Password - With input + +ssp = test_pwinput( + UPN="Administrator@domain.local", + target="machine.domain.local", +) +assert isinstance(ssp, SPNEGOSSP) +assert len(ssp.ssps) == 2 +assert isinstance(ssp.ssps[0], KerberosSSP) +assert ssp.ssps[0].UPN == "Administrator@domain.local" +assert isinstance(ssp.ssps[1], NTLMSSP) +assert ssp.ssps[1].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' + += SPNEGOSSP.from_cli_arguments - UPN + CCache - Prepare + +import os, base64 +from scapy.utils import get_temp_file + +# Create CCACHE +DATA = """ +BQQAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAADERPTUFJTi5MT0NBTAAAAA1BZG1pbmlzdHJhdG9y +AAAAAgAAAAIAAAAMRE9NQUlOLkxPQ0FMAAAABmtyYnRndAAAAAxET01BSU4uTE9DQUwAEgAAACAb +BwocJhrPafZNOEpgJ0Ex7+bIGgYmV1xIOINqhSFV12ktpDBpLaQwaS4wy2kuMMsAQOEAAAAAAAAA +AAAAAAAE2GGCBNQwggTQoAMCAQWhDhsMRE9NQUlOLkxPQ0FMoiEwH6ADAgECoRgwFhsGa3JidGd0 +GwxET01BSU4uTE9DQUyjggSUMIIEkKADAgESoQMCAQKiggSCBIIEfhztXzlAS96FcY2W1vT3dfYk +skGMQuNRwWGyCKReTQQoSNuN+HXmtGgTlEAtf/L0QS5TCAzJKKbnvK6uNw19q/fYd/PJJMbOibmO +Ga1AWrt66Unrcq+AS/iMNgWYtW1qk+Kz7GmkwP/+seilbgZVZPK1JVg0m5oAQn8k8l53Sq6dPvDX +SB7eGtE0UzAM5a5CrpdKALtgbpkjSX2Y8QGmNEC3fVag2k7NP8ZHLd6qLoAmuUDB660vFFIXloRw +RZUe+wpeKX/d3pwcUyJiH0KJlEtPLldgo3EmBo9bUSzxul1MZ6s4oJNWX6MCOVwuTpDnJakBlmH5 +XAFGtxi0Ip7hGpgh4E8AOuhzEJhKaZK4VofcZQAU3KiGq1uOv/4Ema+TxXL83lbdpHX2T3D6naZZ +LOom6cOyMaYzWLs7UGmXtKKubIC5ePlCeV/lrFrEX0zOc86rxdEPw7DXvn4RfukTSjW74+9uiQYv +foqZTB6RIa+OmBg5SOWnceTnwC9P78jNLS5guOjOgBZ0xAMYeXydNloVW3h+XyngNdxiT3qCO+II +rl4uB9ugCQnod1PsvU6cJ6t1OfvhsB+6hXkoloA+RpssC/aMyzWE5985xSBoc91j4P4U6ZJWaCdr +3CaquJVVvIEgAQchlf6aWLI71CYCM+T9dXuzXTbtap7tsYq8/9hWBNs7rwIb7Mok0Zrn74WyU1tB +0fHXLIJqk4wEK4+Kp1w+vSvjULyXhhX1T9IGoTHXKUaXFc5MmLxG9P0jwA4VhrKI6thxK5MRN7gK +xw1OkGDzISTLtr6J4Po6b5ghI4hbxk7AA6y0PwN7DHhIl9OiZPqMcvv5byX6sUc0OSGaFGa0A1uz +/sdsYopfnD0zKBaWXBo9B8MHQ1RQnYjydwCJ78J0few83ZBE8vcb52ngkeIppaEnRuiMCZd0+bsv +X19xsbIXnq08jxrzdn2aqLuWQxHMr/sddfbe5blmGS1JFuwms/m45Ha1T3wK65Efcm6Xtn7qWZOh +GDmptGmM93V/tXpbTEfD18EchMDGxx+LMDOa1nCzOeTXeyEfg4sJp6oOc2+8K7GbwPWdjIomp95R +m/OcgN3DThRC7uELcpLcep5hAdqrPvKYovZeiYsPLl0mdyJ2dWjcOaPg+S3m/T5BOsNSVF4yEWEc +kE7Ahy5QDvag0UFs9vGjkdeKTXk00fQTBCMNLQSO42afxJOoOaYN8gJu81cut1h4ZJm9RngDI+8C +Q+1Yxf9eP/PChFVaL6WL2nsZOqdDjJ4/19qqBK9eDgMzaOqggR91i9m7Tb4AYvb8LnyKh+UE0VBC +lfUM3RD2MA65+OZaEvVDfsWMNdJS1QY9LaW39Dh5n6gV76YmAv0zc1qHux0Z2mOASr3d2aezAFpo +rhcKMZz5YuxbWTB559eoGZNGjRi1gmjVRVTe+mt92Ww8u1eDXV64aH4zc5n7uZpqsWnyRz8K2jjE +slXWBjQr9vLT3ChFnSuH9qKhE+W7vTcdy3k1VuMHL6831nqB17sXR/cZYt0Ajc+L71oAAAAAAAAA +AQAAAAEAAAAMRE9NQUlOLkxPQ0FMAAAADUFkbWluaXN0cmF0b3IAAAADAAAAAgAAAAxET01BSU4u +TE9DQUwAAAAEY2lmcwAAABBEQzEuRE9NQUlOLkxPQ0FMABIAAAAgxahEIPO0srYHJe89OfcWetLT +G6WLKdDHKMTn0+wtykZpLaQwaS2kPGkuMMtpLjDLAEClAAAAAAAAAAAAAAAABPphggT2MIIE8qAD +AgEFoQ4bDERPTUFJTi5MT0NBTKIjMCGgAwIBA6EaMBgbBGNpZnMbEERDMS5ET01BSU4uTE9DQUyj +ggS0MIIEsKADAgESoQMCAQOiggSiBIIEnragYfz/CVtO/WA8R5S6DwhWbd1cxVKg7KnLMrqqbcwx +3USZktAVxuPeLpoUMDLfs5D5ADUo4jHlLJrEAbGsWdFj7DgMYIHIWftRNIvGcCQqjG3/gvL/16+C +GU6ghCUuVKpq16J2KRiHf97QnCAL79PK2d52L+k+f106GI+pRqWlpvrDEHd4Xtve/OW37sXRM3ar +NYUfwjR4uVK7FzHWzisKb8DjgoqZJHt83LVh7Zk2Qxc6p0PMThwWLEI7RB9l8ll30C5cq1qH5kvh +olIipAuAFxNniqE6UZl5GByGg9ck7KDrVrtz9p111BiCxnspfGdPuswjakiSNViSmCV7IsqH16gd +9Z9VBlNNU//mLJd93qsdSxbLclY6F7D7TCAbyv4fgMrDeQ6GVqgjEDG8xtp7T5LUMZPwSgM0pVol +kAWwSbmUh8i4OXQIzI0EAv2aNi0BsCWg1sb9Ri0NVQT5wSaFGHVpinxqrNVd5/mC2a4QgeQ2fOx9 +3fJmShdsrVjVPfcqvedk0L1xw0992l1K18KmtPFu7BhgfkJPOR+FfHJa2zPfnIGsbvuC282vBCbD +krDOug/Uqn01WUmUiwwGBWSTWOOfVDBFy6ETxXJvIkwV8n6Q1wMi8LgcBKc4LdHjbEqc8xJ8yvhA +YJ00xOQNkCu/XK6R4gV5ZkhMs3tB7FoKYbizyAKSuhow3f8Bej/+Lp4VH6gqY33us3jImFizDPmG +lcOrvTl2l0l8ZnQwpT/qP46yD34EIIvujZImf+gFv27F6SFhPkUmi0xISRCJU7XwYdZjNNhnsuom +lGeBvDYhGQtJZ44ZXM7cRggQ+46y60KsHhZHucx5fIzrWrTWUur/gyzf4/ExB3YHX8k4WqzLbt0H +t31LviTZf2a1A2ODwZTp2K8Q506qwr/e+wDRr+uNBOBo04c/tlpvSdi+lrbZODNMHGVIkuCo01Ei +r68jRWaqmTrasXC5tmWyXiH3egN1BkUXqieXNBWYowTc7qr+820TbsOkMTPrxJje0cbvppT3NmB7 +EwyldUoxKDbrtOVr1VvnQWB8IHA2UwRDeuiHP2lRUGHyAHYDH2tlcpGhpk5jqrh4ok93mzZQ1EUz +qbc9tNIRFJCGJlRnf8F5Vy1Xr7o/RfiVooOFXLktC8COr+lwccV1xQfhKEDLOgvqvVHjaQAvlp5v +3Ce5973nwaQ3ttJakXXX5xk94Jzr9JeP/WIoVVHAnl661Zpd01KHIh8Belk+q2xRbJYKLRVmaoG3 +jZmMYkEyP0W0KF3BBFMwRSXJkmyCojpebxKUPBeLelD+l7f2LY/limNhq3F/yju3HAGnuKRPybOu +haMfIiGCaH3FgEqFrudK+KQq4T5CZT/PoGsdmIK+WCElYahwGM6tueVa4RHhBHlSbi0Uyx7KexjL +UHk7A8VRQvSMuQ0S6mj3rOp2w03ZeN+eHcj02cECUx0Sv2MQ5ds5o839X3Z/NsdquJ+83gx7SEHo +7ziAcW28wWcCS1m+eRtxJA2rHILASEwsJbhXQVmllqRY3IuYGztLbKpPKUzveq/2JVBHYZPgKb56 +UJ8RjD9bppHbawAAAAA= +""" +ccache_file = get_temp_file() +with open(ccache_file, "wb") as fd: + fd.write(base64.b64decode(DATA.strip())) + +os.environ["KRB5CCNAME"] = ccache_file + += SPNEGOSSP.from_cli_arguments - UPN + CCache - TGT from KRB5CCNAME + +ssp = test_pwfail( + UPN="Administrator@domain.local", + target="machine.domain.local", + use_krb5ccname=True, +) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].TGT +assert not ssp.ssps[0].ST + += SPNEGOSSP.from_cli_arguments - UPN + CCache - TGT from ccache + +ssp = test_pwfail( + UPN="Administrator@domain.local", + target="machine.domain.local", + ccache=ccache_file +) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].TGT +assert not ssp.ssps[0].ST + += SPNEGOSSP.from_cli_arguments - UPN + CCache - ST from ccache + +ssp = test_pwfail( + UPN="Administrator@domain.local", + target="dc1.domain.local", + ccache=ccache_file +) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].ST +assert not ssp.ssps[0].TGT + += SPNEGOSSP.from_cli_arguments - UPN + CCache - Failure + +try: + test_pwfail( + UPN="Administrator@domain.local", + target="machine.domain.local", + ) + assert False, "Should have prompted for password !" +except ValueError: + pass + += SPNEGOSSP.from_cli_arguments - UPN + CCache - Bad UPN + +try: + test_pwfail( + UPN="toto@domain.local", + target="machine.domain.local", + ccache=ccache_file + ) + assert False, "Should have failed !" +except ValueError: + pass diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts index f0a258e4db4..237c4f9aeaa 100644 --- a/test/scapy/layers/tls/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -614,130 +614,53 @@ pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- """) -c0.isIssuerCert(c1) and c1.isIssuerCert(c2) and not c0.isIssuerCert(c2) +assert c0.isIssuerCert(c1) and c1.isIssuerCert(c2) and not c0.isIssuerCert(c2) = Cert class : Checking isSelfSigned() -c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() +assert c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() = PubKey class : Checking verifyCert() -c2.pubKey.verifyCert(c2) and c1.pubKey.verifyCert(c0) +assert c2.pubKey.verifyCert(c2) and c1.pubKey.verifyCert(c0) -= Chain class : Checking chain construction -assert len(Chain([c0, c1, c2])) == 3 -assert len(Chain([c0], c1)) == 2 -len(Chain([c0], c2)) == 1 += CertTree class : Checking verification of chain +chain0 = CertTree([c0, c1, c2]).getchain(c0) +assert len(chain0) == 3 +assert chain0[0] == c1 +assert chain0[1] == c0 +assert chain0[2] == c2 +chain1 = CertTree([c2, c1, c0]).getchain(c1) +assert len(chain1) == 2 +assert chain1[0] == c1 +assert chain1[1] == c2 +chain2 = CertTree([c0, c2, c1]).getchain(c2) +assert len(chain2) == 1 +assert chain2[0] == c2 -= Chain class : repr += CertTree class : show() -expected_repr = """__ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed] - _ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 - _ /OU=Domain Control Validated/CN=*.tools.ietf.org""" -assert str(Chain([c0, c1, c2])) == expected_repr +expected_repr = '/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed]\n /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 [Not Self Signed]\n /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' +assert CertTree([c0, c1, c2]).show(ret=True) == expected_repr -= Chain class : Checking chain verification -assert Chain([], c0).verifyChain([c2], [c1]) -not Chain([c1]).verifyChain([c0]) +repr_str = CertTree([], c0).show(ret=True) +assert repr_str == '/OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' -= Chain class: Checking chain verification with file += CertTree class : verify -import tempfile - -tf_folder = tempfile.mkdtemp() +CertTree([c1, c2]).verify(c0) +CertTree([c2]).verify(c1) try: - os.makedirs(tf_folder) -except: + CertTree([c1]).verify(c0) + assert False +except ValueError: pass -tf = os.path.join(tf_folder, "trusted") -utf = os.path.join(tf_folder, "untrusted") - -tf -utf - -# Create files -trusted = open(tf, "w") -trusted.write(""" ------BEGIN CERTIFICATE----- -MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw -MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk -dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg -Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF -pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE -3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV -Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+ -MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX -v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+ -zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB -BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo -LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo -LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF -BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv -MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN -QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0 -rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO -eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ -sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ -7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7 ------END CERTIFICATE----- -""") -trusted.close() - -untrusted = open(utf, "w") -untrusted.write(""" ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- -""") -untrusted.close() - -assert Chain([], c0).verifyChainFromCAFile(tf, untrusted_file=utf) -assert Chain([], c0).verifyChainFromCAPath(tf_folder, untrusted_file=utf) - -= Clear files - try: - os.remove("./certs_test_ca/trusted") - os.remove("./certs_test_ca/untrusted") -except: + CertTree([c2]).verify(c0) + assert False +except ValueError: pass -try: - os.rmdir("././certs_test_ca") -except: - pass - -= Test __repr__ - -repr_str = Chain([], c0).__repr__() -assert repr_str == '__ /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' = Test GeneralizedTime