From ab585931f739c8fe1c469f6f075579c92c90e0a1 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:26:57 +0200 Subject: [PATCH 01/22] Implement CMS signing/verification --- scapy/layers/kerberos.py | 26 +- scapy/layers/tls/cert.py | 418 ++++++++++++++++++++++++++++++--- scapy/layers/x509.py | 106 ++++----- test/scapy/layers/kerberos.uts | 34 ++- 4 files changed, 489 insertions(+), 95 deletions(-) diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index c8a3320d6e7..7128077c1aa 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -145,7 +145,11 @@ 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, + PrivKey, + CMS_Engine, +) from scapy.layers.x509 import ( _CMS_ENCAPSULATED, CMS_ContentInfo, @@ -2013,7 +2017,7 @@ 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, 41, 60, 62]: # 6: KDC_ERR_C_PRINCIPAL_UNKNOWN # 7: KDC_ERR_S_PRINCIPAL_UNKNOWN # 12: KDC_ERR_POLICY @@ -2022,6 +2026,7 @@ def m2i(self, pkt, s): # 29: KDC_ERR_SVC_UNAVAILABLE # 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 +2117,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 +2133,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", @@ -3318,7 +3329,10 @@ def as_req(self): if self.x509: # Special PKINIT (RFC4556) factor pafactor = PADATA( - padataType=16, padataValue=PA_PK_AS_REQ() # PA-PK-AS-REQ + padataType=16, # PA-PK-AS-REQ + padataValue=PA_PK_AS_REQ( + + ), ) raise NotImplementedError("PKINIT isn't implemented yet !") else: diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index b38f52ca073..397c02c2e03 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -6,8 +6,8 @@ # 2015, 2016, 2017 Maxence Tury """ -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. For instance, here is what you could do in order to modify the subject public @@ -45,28 +45,56 @@ from scapy.config import conf, crypto_validator 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 @@ -276,11 +304,10 @@ class PubKey(metaclass=_PubKeyFactory): def verifyCert(self, 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,6 +342,13 @@ 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): """ @@ -546,7 +580,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 @@ -562,8 +596,8 @@ def verifyCert(self, 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): @@ -592,6 +626,20 @@ 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): """ @@ -686,7 +734,7 @@ 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, + 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" @@ -714,7 +762,7 @@ 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, + 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" @@ -771,7 +819,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 +848,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 +863,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 +893,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): """ @@ -939,6 +991,10 @@ def isRevoked(self, crl_list): 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) @@ -1004,7 +1060,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 +1113,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): @@ -1084,14 +1140,18 @@ def show(self): class Chain(list): """ - Basically, an enhanced array of Cert. + An enhanced array of Cert. """ - def __init__(self, certList, cert0=None): + def __init__( + self, + certList: Union[List[Cert], str], + cert0: Union[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. + Construct a chain of certificates that follows issuer/subject matching and + respects 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. @@ -1100,8 +1160,39 @@ def __init__(self, certList, cert0=None): Note that we do not check AKID/{SKID/issuer/serial} matching, nor the presence of keyCertSign in keyUsage extension (if present). + + :param certList: either a list of certificates, or a path to a file containing + a list of certificates. + :param cert0: if provided, force the ROOT CA of the chain. """ - list.__init__(self, ()) + super(Chain, self).__init__(()) + + # Parse the certificate list / CA + if isinstance(certList, str): + # It's a path. First get the _PKIObj + obj = _PKIObjMaker.__call__(Chain, 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 + else: + self.frmt = "PEM" + + if isinstance(cert0, str): + cert0 = Cert(cert0) + + # Find the ROOT CA if cert0: self.append(cert0) else: @@ -1111,7 +1202,8 @@ def __init__(self, certList, cert0=None): certList.remove(root_candidate) break - if len(self) > 0: + # Build the chain + if self: while certList: tmp_len = len(self) for c in certList: @@ -1197,6 +1289,38 @@ def verifyChainFromCAPath(self, capath, untrusted_file=None): return self.verifyChain(anchors, untrusted) + def findCertByIssuer(self, issuer): + """ + Find a certificate in the chain 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 chain 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): llen = len(self) - 1 if llen < 0: @@ -1215,3 +1339,233 @@ def __repr__(self): s += "\n" idx += 1 return s + + +####### +# CMS # +####### + +# RFC3852 + + +class CMS_Engine: + """ + A utility class to perform CMS/PKCS7 operations, as specified by RFC3852. + + :param chain: a certificates chain to sign or validate messages against. + :param crls: a list of CRLs to include. This is currently not checked. + """ + + def __init__( + self, + chain: Chain, + crls: List[X509_CRL] = [], + ): + self.chain = chain + self.crls = crls + + def sign( + self, + message: Union[bytes, Packet], + eContentType: ASN1_OID, + cert: Cert, + key: PrivKey, + h: Optional[str] = None, + ): + """ + 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 ! + """ + # RFC3852 sect 5.1 - SignedData Type version + if self.chain: + version = 3 + else: + version = 1 + + # 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, + ) + ) + + # Build a list of X509_Cert to ship (no ROOT certificate) + certificates = [ + x for x in + self.chain + if not x.isSelfSigned() + ] + if cert.x509Cert not in certificates: + certificates.append(cert.x509Cert) + + # Build final structure + return CMS_ContentInfo( + contentType=ASN1_OID("id-signedData"), + content=CMS_SignedData( + version=version, + 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. + + :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 !") + + signeddata = contentInfo.content + + # Build the certificate chain + certificates = [ + Cert(x.certificate) + for x in signeddata.certificates + ] + chain = Chain(self.chain + certificates) + + # 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 = chain.findCertByIssuer(signerInfo.sid.get_issuer()) + + # 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..cf031cc70c4 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -7,7 +7,7 @@ # 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 @@ -136,7 +136,8 @@ class DomainParameters(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_INTEGER("p", 0), ASN1F_INTEGER("g", 0), - ASN1F_INTEGER("q", 0), + # BUG: 'q' isn't supposed to be optional, yet Windows skipts it sometimes... + ASN1F_optional(ASN1F_INTEGER("q", 0)), ASN1F_optional(ASN1F_INTEGER("j", 0)), ASN1F_optional( ASN1F_PACKET("validationParms", None, ValidationParms), @@ -969,6 +970,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 +1019,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 +1049,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 +1108,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 +1128,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,6 +1191,11 @@ class CMS_EncapsulatedContentInfo(ASN1_Packet): ASN1F_optional( _EncapsulatedContent_Field("eContent", None, explicit_tag=0xA0), + ), + # BUG: some Windows versions incorrectly use an implicit octet string. + ASN1F_optional( + _EncapsulatedContent_Field("_eContent", None, + implicit_tag=0xA0), ) ) @@ -1241,10 +1224,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 +1272,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/test/scapy/layers/kerberos.uts b/test/scapy/layers/kerberos.uts index 0080964f995..36224702ecd 100644 --- a/test/scapy/layers/kerberos.uts +++ b/test/scapy/layers/kerberos.uts @@ -201,7 +201,8 @@ assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[2].rdn[0].type. assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[2].rdn[0].value.val == b"DOMAIN-DC1-CA" assert pk_preauth.trustedCertifiers[0].issuerAndSerialNumber.serialNumber.val == 142762589450708598374370602088381230866 -authpack = pk_preauth.signedAuthpack.content.encapContentInfo.eContent +signedauthpack = pk_preauth.signedAuthpack +authpack = signedauthpack.content.encapContentInfo.eContent assert [x.algorithm.oidname for x in authpack.supportedCMSTypes] == [ 'ecdsa-with-SHA512', 'ecdsa-with-SHA256', @@ -214,6 +215,37 @@ assert authpack.pkAuthenticator.freshnessToken is None assert authpack.pkAuthenticator.paChecksum2.checksum.val.hex() == "5aeb03e889e99fcd6c205ef484b9dd7b462b9e94c3fe68b115a71cd287fcd775" assert authpack.pkAuthenticator.paChecksum2.algorithmIdentifier.algorithm.oidname == "sha256" += PKINIT - Verify CMS signature and extract + +from scapy.layers.tls.cert import Cert, PrivKey, Chain, CMS_Engine + +# Get root CA +ca = Cert(bytes.fromhex('3082036930820251a00302010202106b671318bb858b8e437e4229b0d32f12300d06092a864886f70d01010b0500304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341301e170d3235303932303232313034365a170d3330303932303232323034365a304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d434130820122300d06092a864886f70d01010105000382010f003082010a0282010100d502f47f909c951c87f2e8e6ac1c6f86d555b3311e5ef6086b588fb5eeb66277f63d18f04e65ba07570999bcc7cca3e0fa70914fcfa8acd81d4fbf4bb570a089b1b897cf3e07abc9fa75417bcb7171aaa95e20df12add93fada7df5447210820c1de12e356b248b7fe169019b7cf254c5be50571da26ff4219b8680fa249c14673bf743ef37b46c740353cb88097a099fbc7ca41a79c2cd9bc3a663003edfd12678c88b3970fdc211e38b985d6795d57041de0f3182873670bfee903069f59d3f0ff1634bf57f122ef7d1511775c47fdc574f632c9a1e8af305c81077af542f5499977870d8b0bce0d1fd8088636814d7847e0863ceb0ebe8bb0bd4e47eed01d0203010001a351304f300b0603551d0f040403020186300f0603551d130101ff040530030101ff301d0603551d0e04160414ab14d5ae948281f079726970b3b8f97003aa760c301006092b06010401823715010403020100300d06092a864886f70d01010b05000382010100763c9c93d6f0dd98d6ee5269f1d5f8b83fa14e62a9513806f6f978769208ff65f263f1809743f42b6b70cc77f93f5278e62e4d1da2ae5285e8da155951aa5207cea519d373a202d889e37a9fdde6c79e7a574d2dacd3ea695fde5980d16f91b14cd8f3944cc6a5d3d4c5d95e12f863857fe733285ac04d43fdb0ee52dc8ae5c8d1dd6e32405df2f835bd1681dbf5af9fc523cfe31c31fcde16a07f90733f48cff0392a0a18a1787b91d6b67441d78f507043acfb99c64eebc77717a21cf85ec160411a8f8244f8ef493ad22e5bbdb73d647fc6d911b040d373740b11fa65df5f2a8087ae63f69da5fc14e2e320f6d3e013d319a15762ec6ee2eb3cdf9763a523')) + +# Build CMS engine to verify the authpack +cms = CMS_Engine(Chain([ca])) + +# Verify signature +authpack = cms.verify(signedauthpack, ASN1_OID('id-pkinit-authData')) +assert isinstance(authpack, AuthPack) + += PKINIT - Resign AuthPack and re-verify signature + +# Get cert/key +cert = Cert(bytes.fromhex('3082062030820508a00302010202131b000000028b4c5c90b3392fca000000000002300d06092a864886f70d01010b0500304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341301e170d3235303932303232313135385a170d3236303932303232313135385a305731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e310e300c060355040313055573657273311630140603550403130d41646d696e6973747261746f7230820122300d06092a864886f70d01010105000382010f003082010a02820101009edc4865105bdbe4843dcb43a1ed273630d4bb84e2c6096cb8ef4d111da3dfc8ad78ff7a02a6ea6da16f2ecd0a7e4a85c7b685b02286298493834f8361a318864bea2f2faa92a3236cd1e373eb2874ff8e09468762de9af0a0881ea098fbeadccb9573e53c90da8398a9992e6e6a46081e23c31527453f9540ab4bca93d7b139a97c3a0392d8c035832005cc1ae2fdbfe098381e62b37cd6b94ea638fd06d2e2dfb4c1c35896d717188fa8c472a42aaf65c04ff1f2a55dbb0b02dcec1f9e07d7dd930ddec43947cf229324bfa5189bfc5a34a59864c95fa2351b506979cf1bc3529a7933be0f2004932490d1a250735bd692af367f5ca326d392c28c99bde1210203010001a38202f3308202ef301706092b0601040182371402040a1e08005500730065007230290603551d2504223020060a2b0601040182370a030406082b0601050507030406082b06010505070302300e0603551d0f0101ff0404030205a0304406092a864886f70d01090f04373035300e06082a864886f70d030202020080300e06082a864886f70d030402020080300706052b0e030207300a06082a864886f70d0307301d0603551d0e041604140a63d8a405fe59c3f3abbef3111f6f6a6a08a973301f0603551d23041830168014ab14d5ae948281f079726970b3b8f97003aa760c3081c80603551d1f0481c03081bd3081baa081b7a081b48681b16c6461703a2f2f2f434e3d444f4d41494e2d4443312d43412c434e3d4443312c434e3d4344502c434e3d5075626c69632532304b657925323053657276696365732c434e3d53657276696365732c434e3d436f6e66696775726174696f6e2c44433d444f4d41494e2c44433d4c4f43414c3f63657274696669636174655265766f636174696f6e4c6973743f626173653f6f626a656374436c6173733d63524c446973747269627574696f6e506f696e743081c006082b060105050701010481b33081b03081ad06082b060105050730028681a06c6461703a2f2f2f434e3d444f4d41494e2d4443312d43412c434e3d4149412c434e3d5075626c69632532304b657925323053657276696365732c434e3d53657276696365732c434e3d436f6e66696775726174696f6e2c44433d444f4d41494e2c44433d4c4f43414c3f634143657274696669636174653f626173653f6f626a656374436c6173733d63657274696669636174696f6e417574686f7269747930350603551d11042e302ca02a060a2b060104018237140203a01c0c1a41646d696e6973747261746f7240444f4d41494e2e4c4f43414c304e06092b06010401823719020441303fa03d060a2b060104018237190201a02f042d532d312d352d32312d313332323235373836362d343033353133333636322d313134303736393232322d353030300d06092a864886f70d01010b050003820101005b76869c48c9e4f28043253b8552a6017dc25f9dc990da86a79210f334c1a7e50b6125ab176bc7bb194b96a02736c9838117071d533e99467bf24219228bb40b6d410c8fb23f129010b68777acb83944842a0af694673206be22c0a0078ee0543962b31bae8d809ef553dbe858cd063a7a06f1ea7d026394ace39f294ad5d8c1b077e58e7d17f86eea918aa88ac09cf55ffcf147aa14a4c64f4216211e45fd8794b2906a29b97bcbd47a0b213768f5403f9aa08fd23ea92664fb9a0246ae75e34f939102fad7c48b8c5bb650203aa48b48bed4635bff4e3386e694d57a4e7e65939c5a5a72997176b5d0e50bd369e78bbf0cda53db204fbf37839223daff3a06')) +key = PrivKey(bytes.fromhex('308204bd020100300d06092a864886f70d0101010500048204a7308204a302010002820101009edc4865105bdbe4843dcb43a1ed273630d4bb84e2c6096cb8ef4d111da3dfc8ad78ff7a02a6ea6da16f2ecd0a7e4a85c7b685b02286298493834f8361a318864bea2f2faa92a3236cd1e373eb2874ff8e09468762de9af0a0881ea098fbeadccb9573e53c90da8398a9992e6e6a46081e23c31527453f9540ab4bca93d7b139a97c3a0392d8c035832005cc1ae2fdbfe098381e62b37cd6b94ea638fd06d2e2dfb4c1c35896d717188fa8c472a42aaf65c04ff1f2a55dbb0b02dcec1f9e07d7dd930ddec43947cf229324bfa5189bfc5a34a59864c95fa2351b506979cf1bc3529a7933be0f2004932490d1a250735bd692af367f5ca326d392c28c99bde12102030100010282010075a71d72c407d4364cfe5b010ef6cdb8a3b799dd93fa2956bd2c75be3c5e76c9703891b5322b9ea96d0b23f535554d2a013c1b8cd434daa0d68344ab3fef83a54aa9f9226b48c8cbdeb71fa6653e045094482854f2937cdac379ac7d3270388427bedb23a6947d51430a3069a3dacf5d09bd60a8d4f9c35a6d97afbd2b7b6e43e46458433c45c75b87d85830547fd8bfe5ba9119be096833c660b3f4395296a10d2bcdb17ac22d9566aeb602656b715ece5401ef3f4f4731bcbb5316b38a881531a94e36807cc2ef6311e876b41c4fc1053c0d221ad5150ac52b1645aeb6a89861dcbb7faff3350cfa2027a6042681c692ffa3a3a54ef45dc51dadeb132086a502818100d1969cbf231b1e1a73d611fc6d6c60504ccf8c161c49b63b3d40adaeee6540d402c29dfa7f0538a2a4d8870b8bb3e04066423dbdefadf8eadcc9d4bfc2d30654d382eaa70be32fa108ff1bb816abb224d99fffc21cae781fd1637045b7e533614691f42b026ee83dc492e21271bf2fd65e34b4fb31ad522f1e64dc8eaa62b59f02818100c209f373b928c87ae60089f258ee4710983cfcd5586df3aa3bdbb46bf7357681c293328500fafb7daf9ad0c41cf17d3801136424cdee252f036a8033755959f6ba4d5207402619e35f8bc1cd41956d1f921b5b814ffbe4571a1da43007e9ab34b38224cbe98b713a968755e7b956a93dd9ee335888b79a9d4ef9ee2711b8713f0281807a131ba148b556c75988ea58f8f312f6328700b5302ccef39a2dbdfc11e6efe78ce406580cfbe18cfa2f141969798fb872d74a5702ef75f8763928adb8b06913a74ead96369a50f79ee1d827552d1449da6812f3e0f8ce06da52ece5eec29536a7800393b98b17c24268bb3cbafbfcc50381f79807cb47ff21d8e58e4337d3490281803c8da66fe2c49b6bdf032409813f3ae62edc397acad1e54ca6c975908be11f4e774e4061c96089c33b5df0f082a7ca100425ed069f4d464559a78ec28048960ead2d1c002f40b4ab8451b4f53d1648aba588ec117ac87d05c19ca67466c3c12dfd270c1ca69161908b1148f9bb9913cfbd86dc7730933ba903d07345b5fdfd3902818100852917f4d9244d06f54572f7c837069bfb3541e420444315cf3759d65d038d45135869c3bd97ab02c9697cdc971eaef6d5089adce124d69862d6040dbffb13d08b97f2b2ba74a673c6a3d327e07aeece4c72de22844ffdc5d989308552ca0d324c381fbdb8675f8408f26200dcd8c756778b46a80fcea2b60ba3017380871ba4')) + +# Resign +signed = cms.sign( + authpack, + ASN1_OID('id-pkinit-authData'), + cert, + key, +) + +authpack = cms.verify(signed, ASN1_OID('id-pkinit-authData')) +assert isinstance(authpack, AuthPack) + = PKINIT - Parse AS-REP with CMS structures (MIT Kerberos) from scapy.layers.tls.cert import Cert From f8e39d5c03dd2383cb1cf6ea2910af812de895f6 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:29:29 +0200 Subject: [PATCH 02/22] Implement PKINIT in AS-REQ + Improve SPNEGOSSP getter --- scapy/asn1/mib.py | 11 +- scapy/layers/kerberos.py | 468 +++++++++++++++++++++++++-------- scapy/layers/smbclient.py | 2 +- scapy/layers/spnego.py | 17 +- scapy/layers/tls/cert.py | 407 ++++++++++++++++------------ scapy/layers/x509.py | 64 ++++- scapy/libs/rfc3961.py | 32 +++ scapy/modules/ticketer.py | 6 + test/configs/cryptography.utsc | 1 - test/scapy/layers/kerberos.uts | 112 +++++++- test/scapy/layers/tls/cert.uts | 100 ------- 11 files changed, 816 insertions(+), 404 deletions(-) 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/layers/kerberos.py b/scapy/layers/kerberos.py index 7128077c1aa..a4485a14ab2 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, @@ -147,9 +148,18 @@ from scapy.layers.smb2 import STATUS_ERREF from scapy.layers.tls.cert import ( Cert, - PrivKey, + 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, @@ -158,17 +168,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, ) @@ -454,8 +484,6 @@ class EncryptionKey(ASN1_Packet): ) def toKey(self): - from scapy.libs.rfc3961 import Key - return Key( etype=self.keytype.val, key=self.keyvalue.val, @@ -523,7 +551,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 @@ -536,7 +564,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 @@ -1252,7 +1280,7 @@ class PA_PK_AS_REQ(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "trustedCertifiers", - [ExternalPrincipalIdentifier()], + None, ExternalPrincipalIdentifier, explicit_tag=0xA1, ), @@ -1281,11 +1309,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), @@ -1296,14 +1372,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): + """ + Verifiy paChecksum and paChecksum2 + """ + if self.paChecksum.val != Hash_SHA().digest(text): + raise ValueError("Bad paChecksum checksum !") + + self.paChecksum2.verify(text) + # RFC8636 sect 6 @@ -1318,13 +1414,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( @@ -1344,7 +1440,7 @@ class AuthPack(ASN1_Packet): ), ), ASN1F_optional( - ASN1F_STRING("clientDCNonce", None, explicit_tag=0xA3), + ASN1F_STRING("clientDHNonce", None, explicit_tag=0xA3), ), # RFC8636 extension ASN1F_optional( @@ -1353,7 +1449,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 @@ -2868,6 +2964,11 @@ def select(sockets, remain=None): # Util functions +class PKINIT_KEX_METHOD(IntEnum): + DIFFIE_HELLMAN = 1 + PUBLIC_KEY = 2 + + class KerberosClient(Automaton): """ Implementation of a Kerberos client. @@ -2901,8 +3002,12 @@ class KerberosClient(Automaton): :param x509: a X509 certificate to use for PKINIT AS_REQ or S4U2Proxy :param x509key: the private key of the X509 certificate (in an AS_REQ) + :param ca: the CA list that verifies the peer (KDC) certificate. Typically + only the ROOT CA is required. :param p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case, 'password' is the password of the p12. + :param pkinit_kex_method: (advanced) whether to use the DIFFIE-HELLMAN method or the + Certificate based one for PKINIT. TGS-REQ only: @@ -2927,33 +3032,35 @@ class MODE(IntEnum): def __init__( self, mode=MODE.AS_REQ, - ip=None, - upn=None, - password=None, - key=None, - realm=None, - x509=None, - x509key=None, - p12=None, - spn=None, - ticket=None, - host=None, - renew=False, - additional_tickets=[], - u2u=False, - for_user=None, - s4u2proxy=False, - dmsa=False, - kdc_proxy=None, - kdc_proxy_no_check_certificate=False, - fast=False, - armor_ticket=None, - armor_ticket_upn=None, - armor_ticket_skey=None, - key_list_req=[], - etypes=None, - port=88, - timeout=5, + ip: Optional[str] = None, + upn: Optional[str] = None, + password: Optional[str] = None, + key: Optional[Key] = None, + realm: Optional[str] = None, + x509: Optional[Union[Cert, str]] = None, + x509key: Optional[Union[PrivKey, str]] = None, + ca: Optional[Union[CertTree, str]] = None, + p12: Optional[str] = None, + spn: Optional[str] = None, + ticket: Optional[KRB_Ticket] = None, + host: Optional[str] = None, + renew: bool = False, + additional_tickets: List[KRB_Ticket] = [], + u2u: bool = False, + for_user: Optional[str] = None, + s4u2proxy: bool = False, + dmsa: bool = False, + kdc_proxy: Optional[str] = None, + kdc_proxy_no_check_certificate: bool = False, + fast: bool = False, + armor_ticket: KRB_Ticket = None, + armor_ticket_upn: Optional[str] = None, + armor_ticket_skey: Optional[Key] = None, + key_list_req: List[EncryptionType] = [], + etypes: Optional[List[EncryptionType]] = None, + pkinit_kex_method: PKINIT_KEX_METHOD = PKINIT_KEX_METHOD.DIFFIE_HELLMAN, + port: int = 88, + timeout: int = 5, **kwargs, ): import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 @@ -2977,29 +3084,50 @@ def __init__( # PKINIT checks if p12 is not None: - from cryptography.hazmat.primitives.serialization import pkcs12 - # password should be None or bytes if isinstance(password, str): password = password.encode() - # Read p12/pfx - with open(p12, "rb") as fd: - x509key, x509, _ = pkcs12.load_key_and_certificates( - fd.read(), - password=password, - ) - x509 = Cert(cryptography_obj=x509) - x509key = PrivKey(cryptography_obj=x509key) + # Read p12/pfx. If it fails and no password was provided, prompt and + # retry once. + while True: + try: + with open(p12, "rb") as fd: + x509key, x509, _ = pkcs12.load_key_and_certificates( + fd.read(), + password=password, + ) + break + except ValueError as ex: + if password is None: + # We don't have a password. Prompt and retry. + try: + from prompt_toolkit import prompt + + password = prompt( + "Enter PKCS12 password: ", is_password=True + ) + except ImportError: + password = input("Enter PKCS12 password: ") + password = password.encode() + else: + raise ex + + x509 = Cert(cryptography_obj=x509) + x509key = PrivKey(cryptography_obj=x509key) elif x509 and x509key: - x509 = Cert(x509) - x509key = PrivKey(x509key) + if not isinstance(x509, Cert): + x509 = Cert(x509) + if not isinstance(x509key, PrivKey): + x509key = PrivKey(x509key) + if not isinstance(ca, CertList): + ca = CertList(ca) if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: if not host: raise ValueError("Invalid host") - if (x509 is None) ^ (x509key is None): - raise ValueError("Must provide both 'x509' and 'x509key' !") + if x509 is None and (not x509key or not ca): + raise ValueError("Must provide both 'x509', 'x509key' and 'ca' !") elif mode == self.MODE.TGS_REQ: if not ticket: raise ValueError("Invalid ticket") @@ -3035,15 +3163,11 @@ def __init__( if etypes is not None: raise ValueError("Cannot specify etypes in GET_SALT mode !") - from scapy.libs.rfc3961 import EncryptionType - etypes = [ EncryptionType.AES256_CTS_HMAC_SHA1_96, EncryptionType.AES128_CTS_HMAC_SHA1_96, ] elif etypes is None: - from scapy.libs.rfc3961 import EncryptionType - etypes = [ EncryptionType.AES256_CTS_HMAC_SHA1_96, EncryptionType.AES128_CTS_HMAC_SHA1_96, @@ -3071,6 +3195,7 @@ def __init__( self.realm = realm.upper() self.x509 = x509 self.x509key = x509key + self.pkinit_kex_method = pkinit_kex_method self.ticket = ticket self.fast = fast self.armor_ticket = armor_ticket @@ -3098,6 +3223,11 @@ def __init__( self.fast_skey = None # The random subkey used for fast self.fast_armorkey = None # The armor key self.fxcookie = None + self.pkinit_dh_key = None + if ca is not None: + self.pkinit_cms = CMS_Engine(ca) + else: + self.pkinit_cms = None sock = self._connect() super(KerberosClient, self).__init__( @@ -3155,8 +3285,6 @@ def calc_fast_armorkey(self): Calculate and return the FAST armorkey """ # Generate a random key of the same type than ticket_skey - from scapy.libs.rfc3961 import Key, KRB_FX_CF2 - if self.mode == self.MODE.AS_REQ: # AS-REQ mode self.fast_skey = Key.new_random_key(self.armor_ticket_skey.etype) @@ -3328,20 +3456,101 @@ def as_req(self): if self.pre_auth: if self.x509: # Special PKINIT (RFC4556) factor + + # RFC4556 - 3.2.1. Generation of Client Request + + # RFC4556 - 3.2.1 - (5) AuthPack + authpack = KRB_AuthPack( + pkAuthenticator=KRB_PKAuthenticator( + ctime=ASN1_GENERALIZED_TIME(now_time), + cusec=ASN1_INTEGER(0), + nonce=ASN1_INTEGER(RandNum(0, 0x7FFFFFFF)._fix()), + ), + clientPublicValue=None, # Used only in DH mode + supportedCMSTypes=None, + clientDHNonce=None, + supportedKDFs=None, + ) + + if self.pkinit_kex_method == PKINIT_KEX_METHOD.DIFFIE_HELLMAN: + # RFC4556 - 3.2.3.1. Diffie-Hellman Key Exchange + + # We use modp2048 + dh_parameters = _ffdh_groups["modp2048"][0] + self.pkinit_dh_key = dh_parameters.generate_private_key() + numbers = dh_parameters.parameter_numbers() + + # We can't use 'public_bytes' because it's the PKCS#3 format, + # and we want the DomainParameters format. + authpack.clientPublicValue = X509_SubjectPublicKeyInfo( + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID("dhpublicnumber"), + parameters=DomainParameters( + p=ASN1_INTEGER(numbers.p), + g=ASN1_INTEGER(numbers.g), + # q: see ERRATA 1 of RFC4556 + q=ASN1_INTEGER(numbers.q or (numbers.p - 1) // 2), + ), + ), + subjectPublicKey=DHPublicKey( + y=ASN1_INTEGER( + self.pkinit_dh_key.public_key().public_numbers().y + ), + ), + ) + elif self.pkinit_kex_method == PKINIT_KEX_METHOD.PUBLIC_KEY: + # RFC4556 - 3.2.3.2. - Public Key Encryption + + # Set supportedCMSTypes, supportedKDFs + authpack.supportedCMSTypes = [ + X509_AlgorithmIdentifier(algorithm=ASN1_OID(x)) + for x in [ + "ecdsa-with-SHA512", + "ecdsa-with-SHA256", + "sha512WithRSAEncryption", + "sha256WithRSAEncryption", + ] + ] + authpack.supportedKDFs = [ + KDFAlgorithmId(kdfId=ASN1_OID(x)) + for x in [ + "id-pkinit-kdf-sha256", + "id-pkinit-kdf-sha1", + "id-pkinit-kdf-sha512", + ] + ] + + # XXX UNFINISHED + raise NotImplementedError + else: + raise ValueError + + # Populate paChecksum and PAChecksum2 + authpack.pkAuthenticator.make_checksum(bytes(kdc_req)) + + # Sign the AuthPack + signedAuthpack = self.pkinit_cms.sign( + authpack, + ASN1_OID("id-pkinit-authData"), + self.x509, + self.x509key, + ) + + # Build PA-DATA pafactor = PADATA( padataType=16, # PA-PK-AS-REQ padataValue=PA_PK_AS_REQ( - + signedAuthpack=signedAuthpack, + trustedCertifiers=None, + kdcPkId=None, ), ) - raise NotImplementedError("PKINIT isn't implemented yet !") else: # Key-based factor if self.fast: # Special FAST factor # RFC6113 sect 5.4.6 - from scapy.libs.rfc3961 import KRB_FX_CF2 # Calculate the 'challenge key' ts_key = KRB_FX_CF2( @@ -3421,8 +3630,6 @@ def tgs_req(self): # [MS-SFU] FOR-USER extension if self.for_user is not None: - from scapy.libs.rfc3961 import ChecksumType, EncryptionType - # [MS-SFU] note 4: # "Windows Vista, Windows Server 2008, Windows 7, and Windows Server # 2008 R2 send the PA-S4U-X509-USER padata type alone if the user's @@ -3614,31 +3821,84 @@ def SENT_AS_REQ(self): def SENT_TGS_REQ(self): pass - def _process_padatas_and_key(self, padatas): - from scapy.libs.rfc3961 import EncryptionType, Key, KRB_FX_CF2 + def _process_padatas_and_key(self, padatas, etype: EncryptionType = None): + """ + Process the PADATA, and generate missing keys if required. - etype = None + :param etype: (optional) If provided, the EncryptionType to use. + """ salt = b"" + + if etype is not None and etype not in self.etypes: + raise ValueError("The answered 'etype' key isn't supported by us !") + # 1. Process pa-data if padatas is not None: for padata in padatas: if padata.padataType == 0x13 and etype is None: # PA-ETYPE-INFO2 + # We obtain the salt for hash types that need it elt = padata.padataValue.seq[0] if elt.etype.val in self.etypes: etype = elt.etype.val if etype != EncryptionType.RC4_HMAC: salt = elt.salt.val + + elif padata.padataType == 0x11: # PA-PK-AS-REP + # PKINIT handling + + # The steps are as follows: + # 1. Verify and extract the CMS response. The expected type + # is different depending on the used method. + # 2. Compute the replykey + + if self.pkinit_kex_method == PKINIT_KEX_METHOD.DIFFIE_HELLMAN: + # Unpack KDCDHKeyInfo + keyinfo = self.pkinit_cms.verify( + padata.padataValue.rep.dhSignedData, + eContentType=ASN1_OID("id-pkinit-DHKeyData"), + ) + + # If 'etype' is None, we're in an error. Since we verified + # the CMS successfully, end here. + if etype is None: + continue + + # Extract crypto parameters + y = keyinfo.subjectPublicKey.y.val + + # Import into cryptography + params = self.pkinit_dh_key.parameters().parameter_numbers() + pubkey = dh.DHPublicNumbers(y, params).public_key() + + # Calculate DHSharedSecret + DHSharedSecret = self.pkinit_dh_key.exchange(pubkey) + + # RFC4556 3.2.3.1 - AS reply key is derived as follows + self.replykey = octetstring2key( + etype, + DHSharedSecret, + ) + + else: + raise ValueError + elif padata.padataType == 133: # PA-FX-COOKIE + # Get cookie and store it self.fxcookie = padata.padataValue + elif padata.padataType == 136: # PA-FX-FAST + # FAST handling: get the actual inner message and decrypt it if isinstance(padata.padataValue, PA_FX_FAST_REPLY): self.fast_rep = ( padata.padataValue.armoredData.encFastRep.decrypt( self.fast_armorkey, ) ) + elif padata.padataType == 137: # PA-FX-ERROR + # Get error and store it self.fast_error = padata.padataValue + elif padata.padataType == 130: # PA-FOR-X509-USER # Verify S4U checksum key_usage_number = None @@ -3653,17 +3913,17 @@ def _process_padatas_and_key(self, padatas): key_usage_number=key_usage_number, ) - # 2. Update the current key if necessary + # 2. Update the current keys if necessary - # Compute key if not already provided - if self.key is None and etype is not None: + # Compute client key if not already provided + if self.key is None and etype is not None and self.x509 is None: self.key = Key.string_to_key( etype, self.password, salt, ) - # Update the key with the fast reply, if necessary + # Strengthen the reply key with the fast reply, if necessary if self.fast_rep and self.fast_rep.strengthenKey: # "The strengthen-key field MAY be set in an AS reply" self.replykey = KRB_FX_CF2( @@ -3718,7 +3978,7 @@ def receive_krb_error_as_req(self, pkt): return if pkt.root.errorCode == 25: # KDC_ERR_PREAUTH_REQUIRED - if not self.key: + if not self.key and not self.x509: log_runtime.error( "Got 'KDC_ERR_PREAUTH_REQUIRED', " "but no possible key could be computed." @@ -3750,7 +4010,12 @@ def retry_after_eof_in_apreq(self): @ATMT.action(receive_as_rep) def decrypt_as_rep(self, pkt): - self._process_padatas_and_key(pkt.root.padata) + # Process PADATAs. This is important for FAST and PKINIT + self._process_padatas_and_key( + pkt.root.padata, + etype=pkt.root.encPart.etype.val, + ) + if not self.pre_auth: log_runtime.warning("Pre-authentication was disabled for this account !") @@ -3765,6 +4030,10 @@ def decrypt_as_rep(self, pkt): elif self.fast: raise ValueError("Answer was not FAST ! Is it supported?") + # Check for PKINIT + if self.x509 and self.replykey is None: + raise ValueError("PKINIT was used but no valid PA-PK-AS-REP was found !") + # Decrypt AS-REP response enc = pkt.root.encPart res = enc.decrypt(self.replykey) @@ -3892,16 +4161,16 @@ def _spn_are_equal(spn1, spn2): def krb_as_req( - upn, - spn=None, - ip=None, - key=None, - password=None, - realm=None, - host="WIN10", - p12=None, - x509=None, - x509key=None, + upn: str, + spn: Optional[str] = None, + ip: Optional[str] = None, + key: Optional[Key] = None, + password: Optional[str] = None, + realm: Optional[str] = None, + host: str = "WIN10", + p12: Optional[str] = None, + x509: Optional[Union[str, Cert]] = None, + x509key: Optional[Union[str, PrivKey]] = None, **kwargs, ): r""" @@ -3916,7 +4185,7 @@ def krb_as_req( :param key: (optional) pass the Key object. :param password: (optional) otherwise, pass the user's password :param x509: (optional) pass a x509 certificate for PKINIT. - :param x509key: (optional) pass the key of the x509 certificate for PKINIT. + :param x509key: (optional) pass the private key of the x509 certificate for PKINIT. :param p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case, 'password' is the password of the p12. :param realm: (optional) the realm to use. Otherwise use the one from UPN. @@ -4364,6 +4633,8 @@ def __init__( debug=0, **kwargs, ): + import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 + self.ST = ST self.UPN = UPN self.KEY = KEY @@ -4374,8 +4645,6 @@ def __init__( self.DC_IP = DC_IP self.debug = debug if SKEY_TYPE is None: - from scapy.libs.rfc3961 import EncryptionType - SKEY_TYPE = EncryptionType.AES128_CTS_HMAC_SHA1_96 self.SKEY_TYPE = SKEY_TYPE super(KerberosSSP, self).__init__(**kwargs) @@ -4527,13 +4796,6 @@ def GSS_WrapEx(self, Context, msgs, qop_req=0): tok.root.Data = strrot(Data, tok.root.RRC) return msgs, tok elif Context.KrbSessionKey.etype in [23, 24]: # RC4 - from scapy.libs.rfc3961 import ( - Cipher, - Hmac_MD5, - _rfc1964pad, - decrepit_algorithms, - ) - # Build token seq = struct.pack(">I", Context.SendSeqNum) tok = KRB_InnerToken( @@ -4697,13 +4959,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 @@ -4770,8 +5025,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 @@ -4909,7 +5162,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) + ), ) ), ), @@ -5018,7 +5273,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: diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 360acbce824..2239bc792c1 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -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/spnego.py b/scapy/layers/spnego.py index 3afb73268ed..b9959078f14 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -641,6 +641,7 @@ def from_cli_arguments( kerberos_required: bool = False, ST=None, KEY=None, + ccache: str = None, debug: int = 0, ): """ @@ -657,6 +658,7 @@ def from_cli_arguments( :param ST: if provided, the service ticket to use (Kerberos) :param KEY: if ST provided, the session key associated to the ticket (Kerberos). Else, the user secret key. + :param ccache: (str) if provided, a path to a CCACHE (Kerberos) """ kerberos = True hostname = None @@ -700,7 +702,20 @@ 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 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 ticketer that we'll use + raise NotImplementedError + + + ssps.append(t.ssp()) + elif ST is None: # In this case, KEY is supposed to be the user's key. from scapy.libs.rfc3961 import Key, EncryptionType diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 397c02c2e03..ed2df5a853d 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, 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,6 +65,7 @@ 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 ( @@ -1003,6 +1026,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' @@ -1134,45 +1163,29 @@ def show(self): print("nextUpdate: %s" % self.nextUpdate_str) -###################### -# Certificate chains # -###################### +#################### +# Certificate list # +#################### -class Chain(list): +class CertList(list): """ - An enhanced array of Cert. + An object that can store a list of Cert objects, load them and export them + into DER/PEM format. """ def __init__( self, - certList: Union[List[Cert], str], - cert0: Union[Cert, str, None] = None, + certList: Union[Self, List[Cert], Cert, str], ): """ - Construct a chain of certificates that follows issuer/subject matching and - respects 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. - - Note that we do not check AKID/{SKID/issuer/serial} matching, - nor the presence of keyCertSign in keyUsage extension (if present). - - :param certList: either a list of certificates, or a path to a file containing - a list of certificates. - :param cert0: if provided, force the ROOT CA of the chain. + Construct a list of certificates/CRLs to be used as list of ROOT certificates. """ - super(Chain, self).__init__(()) - # Parse the certificate list / CA if isinstance(certList, str): # It's a path. First get the _PKIObj - obj = _PKIObjMaker.__call__(Chain, certList, _MAX_CERT_SIZE, + obj = _PKIObjMaker.__call__(CertList, certList, _MAX_CERT_SIZE, "CERTIFICATE") - + # Then parse the der until there's nothing left certList = [] payload = obj._der @@ -1186,112 +1199,17 @@ def __init__( certList.append(Cert(cert)) self.frmt = obj.frmt - else: + elif isinstance(certList, Cert): + certList = [certList] self.frmt = "PEM" - - if isinstance(cert0, str): - cert0 = Cert(cert0) - - # Find the ROOT CA - if cert0: - self.append(cert0) else: - for root_candidate in certList: - if root_candidate.isSelfSigned(): - self.append(root_candidate) - certList.remove(root_candidate) - break - - # Build the chain - if self: - 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): - """ - 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. - """ - 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): - """ - 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). - """ - 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)] - - 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)] - - return self.verifyChain(anchors, untrusted) - - def verifyChainFromCAPath(self, capath, untrusted_file=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). - """ - 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") - - 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)] + self.frmt = "PEM" - return self.verifyChain(anchors, untrusted) + super(CertList, self).__init__(certList) def findCertByIssuer(self, issuer): """ - Find a certificate in the chain by issuer. + Find a certificate in the list by issuer. """ for cert in self: if cert.issuer == issuer: @@ -1300,7 +1218,7 @@ def findCertByIssuer(self, issuer): def export(self, filename, fmt=None): """ - Export a chain of certificates 'fmt' format (DER or PEM) to file 'filename' + Export a list of certificates 'fmt' format (DER or PEM) to file 'filename' """ if fmt is None: if filename.endswith(".pem"): @@ -1322,24 +1240,167 @@ def pem(self): return "".join(x.pem for x in self) 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 + 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 CertTree(CertList): + """ + 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] + """ + + __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 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. + """ + # 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: - 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 + self.rootCAs = CertList(rootCAs) + + # Append our root CAs to the certList + certList.extend(self.rootCAs) + # Super instantiate + super(CertTree, self).__init__(certList) + + @property + def tree(self): + """ + Get a tree-like object of the certificate list + """ + # 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 emtpy. + 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): + """ + Return a chain of certificate that points from a ROOT CA to a certificate. + """ + 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.isIssuerCert(c): + return curchain + else: + curchain = _rec_getchain(curchain, subtree) + if curchain: + return curchain + return None + + chain = _rec_getchain([], self.tree) + if chain is not None: + return CertTree(cert, chain) + else: + return None + + 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 # @@ -1352,16 +1413,16 @@ class CMS_Engine: """ A utility class to perform CMS/PKCS7 operations, as specified by RFC3852. - :param chain: a certificates chain to sign or validate messages against. + :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, - chain: Chain, + store: CertList, crls: List[X509_CRL] = [], ): - self.chain = chain + self.store = store self.crls = crls def sign( @@ -1383,12 +1444,6 @@ def sign( We currently only support X.509 certificates ! """ - # RFC3852 sect 5.1 - SignedData Type version - if self.chain: - version = 3 - else: - version = 1 - # RFC3852 - 5.4. Message Digest Calculation Process h = h or cert.getSignatureHashName() hash = hashes.Hash(_get_hash(h)) @@ -1434,20 +1489,19 @@ def sign( ) ) - # Build a list of X509_Cert to ship (no ROOT certificate) + # Build a chain of X509_Cert to ship (but skip the ROOT certificate) + certTree = CertTree(cert, self.store) certificates = [ - x for x in - self.chain + x.x509Cert + for x in certTree if not x.isSelfSigned() ] - if cert.x509Cert not in certificates: - certificates.append(cert.x509Cert) # Build final structure return CMS_ContentInfo( contentType=ASN1_OID("id-signedData"), content=CMS_SignedData( - version=version, + version=3 if certificates else 1, digestAlgorithms=X509_AlgorithmIdentifier( algorithm=ASN1_OID(h), parameters=ASN1_NULL(0), @@ -1500,7 +1554,7 @@ def verify( Cert(x.certificate) for x in signeddata.certificates ] - chain = Chain(self.chain + certificates) + certTree = CertTree(certificates, self.store) # Check there's at least one signature if not signeddata.signerInfos: @@ -1509,7 +1563,10 @@ def verify( # Check all signatures for signerInfo in signeddata.signerInfos: # Find certificate in the chain that did this - cert: Cert = chain.findCertByIssuer(signerInfo.sid.get_issuer()) + cert: Cert = certTree.findCertByIssuer(signerInfo.sid.get_issuer()) + + # Verify certificate signature + certTree.verify(cert) # Verify the message hash if signerInfo.signedAttrs: diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index cf031cc70c4..5ca30babd0a 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -136,8 +136,7 @@ class DomainParameters(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_INTEGER("p", 0), ASN1F_INTEGER("g", 0), - # BUG: 'q' isn't supposed to be optional, yet Windows skipts it sometimes... - ASN1F_optional(ASN1F_INTEGER("q", 0)), + ASN1F_INTEGER("q", 0), ASN1F_optional(ASN1F_INTEGER("j", 0)), ASN1F_optional( ASN1F_PACKET("validationParms", None, ValidationParms), @@ -219,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 # #################################### @@ -848,13 +865,37 @@ 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( + [ + # 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", + ), + ], + ASN1F_optional(ASN1F_NULL("parameters", None)), ) ) @@ -1192,11 +1233,6 @@ class CMS_EncapsulatedContentInfo(ASN1_Packet): _EncapsulatedContent_Field("eContent", None, explicit_tag=0xA0), ), - # BUG: some Windows versions incorrectly use an implicit octet string. - ASN1F_optional( - _EncapsulatedContent_Field("_eContent", None, - implicit_tag=0xA0), - ) ) diff --git a/scapy/libs/rfc3961.py b/scapy/libs/rfc3961.py index ed6581ceaff..bc7e2e8aee5 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) -> bytes: + """ + 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..78f1c7e234d 100644 --- a/scapy/modules/ticketer.py +++ b/scapy/modules/ticketer.py @@ -2424,6 +2424,9 @@ def request_tgt( fast=False, armor_with=None, spn=None, + x509=None, + x509key=None, + p12=None, **kwargs, ): """ @@ -2458,6 +2461,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: 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/scapy/layers/kerberos.uts b/test/scapy/layers/kerberos.uts index 36224702ecd..b8a44f28169 100644 --- a/test/scapy/layers/kerberos.uts +++ b/test/scapy/layers/kerberos.uts @@ -217,17 +217,17 @@ assert authpack.pkAuthenticator.paChecksum2.algorithmIdentifier.algorithm.oidnam = PKINIT - Verify CMS signature and extract -from scapy.layers.tls.cert import Cert, PrivKey, Chain, CMS_Engine +from scapy.layers.tls.cert import Cert, PrivKey, CertList, CMS_Engine # Get root CA ca = Cert(bytes.fromhex('3082036930820251a00302010202106b671318bb858b8e437e4229b0d32f12300d06092a864886f70d01010b0500304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341301e170d3235303932303232313034365a170d3330303932303232323034365a304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d434130820122300d06092a864886f70d01010105000382010f003082010a0282010100d502f47f909c951c87f2e8e6ac1c6f86d555b3311e5ef6086b588fb5eeb66277f63d18f04e65ba07570999bcc7cca3e0fa70914fcfa8acd81d4fbf4bb570a089b1b897cf3e07abc9fa75417bcb7171aaa95e20df12add93fada7df5447210820c1de12e356b248b7fe169019b7cf254c5be50571da26ff4219b8680fa249c14673bf743ef37b46c740353cb88097a099fbc7ca41a79c2cd9bc3a663003edfd12678c88b3970fdc211e38b985d6795d57041de0f3182873670bfee903069f59d3f0ff1634bf57f122ef7d1511775c47fdc574f632c9a1e8af305c81077af542f5499977870d8b0bce0d1fd8088636814d7847e0863ceb0ebe8bb0bd4e47eed01d0203010001a351304f300b0603551d0f040403020186300f0603551d130101ff040530030101ff301d0603551d0e04160414ab14d5ae948281f079726970b3b8f97003aa760c301006092b06010401823715010403020100300d06092a864886f70d01010b05000382010100763c9c93d6f0dd98d6ee5269f1d5f8b83fa14e62a9513806f6f978769208ff65f263f1809743f42b6b70cc77f93f5278e62e4d1da2ae5285e8da155951aa5207cea519d373a202d889e37a9fdde6c79e7a574d2dacd3ea695fde5980d16f91b14cd8f3944cc6a5d3d4c5d95e12f863857fe733285ac04d43fdb0ee52dc8ae5c8d1dd6e32405df2f835bd1681dbf5af9fc523cfe31c31fcde16a07f90733f48cff0392a0a18a1787b91d6b67441d78f507043acfb99c64eebc77717a21cf85ec160411a8f8244f8ef493ad22e5bbdb73d647fc6d911b040d373740b11fa65df5f2a8087ae63f69da5fc14e2e320f6d3e013d319a15762ec6ee2eb3cdf9763a523')) # Build CMS engine to verify the authpack -cms = CMS_Engine(Chain([ca])) +cms = CMS_Engine(CertList([ca])) # Verify signature authpack = cms.verify(signedauthpack, ASN1_OID('id-pkinit-authData')) -assert isinstance(authpack, AuthPack) +assert isinstance(authpack, KRB_AuthPack) = PKINIT - Resign AuthPack and re-verify signature @@ -244,7 +244,7 @@ signed = cms.sign( ) authpack = cms.verify(signed, ASN1_OID('id-pkinit-authData')) -assert isinstance(authpack, AuthPack) +assert isinstance(authpack, KRB_AuthPack) = PKINIT - Parse AS-REP with CMS structures (MIT Kerberos) @@ -1389,6 +1389,110 @@ _msgs = ssp.GSS_UnwrapEx( assert _msgs[0].data.hex() == "112233445566778899aabbccddeeff" ++ RFC4556 test vectors +~ mock + += RFC4556 Test Vectors - octetstring2key - Utils + +from scapy.libs.rfc3961 import EncryptionType, octetstring2key + +def _strip(x): + return bytes.fromhex(x.replace(" ", "").replace("\n", "")) + +def _k_truncate_output(etype, input): + with mock.patch('scapy.libs.rfc3961.Key.random_to_key', side_effect=Bunch): + result = octetstring2key(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT) + return result.seed + += RFC4556 Test Vectors - octetstring2key - Set 1 + +INPUT = _strip(""" +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +""") + +RESULT = _strip(""" +5e e5 0d 67 5c 80 9f e5 9e 4a 77 62 c5 4b 65 83 +75 47 ea fb 15 9b d8 cd c7 5f fc a5 91 1e 4c 41 +""") + +hexdiff(_k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT), RESULT) +assert _k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT) == RESULT + += RFC4556 Test Vectors - octetstring2key - Set 2 + +INPUT = _strip(""" +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +""") + +RESULT = _strip(""" +ac f7 70 7c 08 97 3d df db 27 cd 36 14 42 cc fb +a3 55 c8 88 4c b4 72 f3 7d a6 36 d0 7d 56 78 7e +""") + +hexdiff(_k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT), RESULT) +assert _k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT) == RESULT + += RFC4556 Test Vectors - octetstring2key - Set 3 + +INPUT = _strip(""" +00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f +10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e +0f 10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d +0e 0f 10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c +0d 0e 0f 10 00 01 02 03 04 05 06 07 08 09 0a 0b +0c 0d 0e 0f 10 00 01 02 03 04 05 06 07 08 09 0a +0b 0c 0d 0e 0f 10 00 01 02 03 04 05 06 07 08 09 +0a 0b 0c 0d 0e 0f 10 00 01 02 03 04 05 06 07 08 +""") + +RESULT = _strip(""" +c4 42 da 58 5f cb 80 e4 3b 47 94 6f 25 40 93 e3 +73 29 d9 90 01 38 0d b7 83 71 db 3a cf 5c 79 7e +""") + +hexdiff(_k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT), RESULT) +assert _k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT) == RESULT + += RFC4556 Test Vectors - octetstring2key - Set 4 + +INPUT = _strip(""" +00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f +10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e +0f 10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d +0e 0f 10 00 01 02 03 04 05 06 07 08 09 0a 0b 0c +0d 0e 0f 10 00 01 02 03 04 05 06 07 08 +""") + +RESULT = _strip(""" +00 53 95 3b 84 c8 96 f4 eb 38 5c 3f 2e 75 1c 4a +59 0e d6 ff ad ca 6f f6 4f 47 eb eb 8d 78 0f fc +""") + +hexdiff(_k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT), RESULT) +assert _k_truncate_output(EncryptionType.AES256_CTS_HMAC_SHA1_96, INPUT) == RESULT + + GSS-API KerberosSSP tests ~ mock diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts index f0a258e4db4..ace1b75e1dc 100644 --- a/test/scapy/layers/tls/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -634,106 +634,6 @@ expected_repr = """__ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, In _ /OU=Domain Control Validated/CN=*.tools.ietf.org""" assert str(Chain([c0, c1, c2])) == expected_repr -= Chain class : Checking chain verification -assert Chain([], c0).verifyChain([c2], [c1]) -not Chain([c1]).verifyChain([c0]) - -= Chain class: Checking chain verification with file - -import tempfile - -tf_folder = tempfile.mkdtemp() - -try: - os.makedirs(tf_folder) -except: - 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: - pass - -try: - os.rmdir("././certs_test_ca") -except: - pass - = Test __repr__ repr_str = Chain([], c0).__repr__() From a2bb2a0db5f959ac52f23e762d8717288cb16289 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:29:55 +0200 Subject: [PATCH 03/22] NTLM: add old variant support --- scapy/layers/ntlm.py | 151 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 127 insertions(+), 24 deletions(-) diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 556faee0ea5..1f6ab17a50a 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -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 @@ -488,10 +535,18 @@ class _NTLM_Version(Packet): # Sect 2.2.1.1 -class NTLM_NEGOTIATE(_NTLMPayloadPacket): +class NTLM_NEGOTIATE(_NTLM_VARIANT_Packet): name = "NTLM Negotiate" + __slots__ = ["VARIANT"] MessageType = 1 - OFFSET = lambda pkt: (((pkt.DomainNameBufferOffset or 40) > 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 +565,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 ] @@ -628,10 +686,18 @@ def default_payload_class(self, payload): return conf.padding_layer -class NTLM_CHALLENGE(_NTLMPayloadPacket): +class NTLM_CHALLENGE(_NTLM_VARIANT_Packet): name = "NTLM Challenge" + __slots__ = ["VARIANT"] MessageType = 2 - OFFSET = lambda pkt: (((pkt.TargetInfoBufferOffset or 56) > 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 +719,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 +839,23 @@ def computeNTProofStr(self, ResponseKeyNT, ServerChallenge): return HMAC_MD5(ResponseKeyNT, ServerChallenge + temp) -class NTLM_AUTHENTICATE(_NTLMPayloadPacket): +class NTLM_AUTHENTICATE(_NTLM_VARIANT_Packet): 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 +892,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 +905,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( @@ -1247,6 +1331,7 @@ def __init__( HASHNT=None, PASSWORD=None, USE_MIC=True, + VARIANT: NTLM_VARIANT = NTLM_VARIANT.RECENT, NTLM_VALUES={}, DOMAIN_FQDN=None, DOMAIN_NB_NAME=None, @@ -1261,7 +1346,14 @@ 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: from scapy.layers.kerberos import _parse_upn @@ -1399,6 +1491,7 @@ def GSS_Init_sec_context( # Client: negotiate # Create a default token tok = NTLM_NEGOTIATE( + VARIANT=self.VARIANT, NegotiateFlags="+".join( [ "NEGOTIATE_UNICODE", @@ -1408,10 +1501,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", @@ -1466,6 +1563,7 @@ def GSS_Init_sec_context( return Context, None, GSS_S_DEFECTIVE_TOKEN # Take a default token tok = NTLM_AUTHENTICATE_V2( + VARIANT=self.VARIANT, NegotiateFlags=chall_tok.NegotiateFlags, ProductMajorVersion=10, ProductMinorVersion=0, @@ -1618,6 +1716,7 @@ def GSS_Accept_sec_context( # Take a default token currentTime = (time.time() + 11644473600) * 1e7 tok = NTLM_CHALLENGE( + VARIANT=self.VARIANT, ServerChallenge=self.SERVER_CHALLENGE or os.urandom(8), NegotiateFlags="+".join( [ @@ -1628,11 +1727,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 From d931590aa993ef3f45560ddf516960f91cd1d81d Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:57:49 +0200 Subject: [PATCH 04/22] DCE/RPC: improve context handling --- scapy/layers/dcerpc.py | 11 +- scapy/layers/msrpce/rpcclient.py | 215 +++++++++++++++++++++---------- 2 files changed, 158 insertions(+), 68 deletions(-) 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/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index a25f6587126..b4c0544210e 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, + # re-use 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,7 +574,11 @@ 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: @@ -491,6 +588,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls 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, ), ) @@ -535,17 +633,17 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls ) 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 From 755b70073d7120c3b8c5bb367ae143025024d3ec Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:58:43 +0200 Subject: [PATCH 05/22] Kerberos: fix passive with DCE/RPC + improve deleg --- scapy/layers/kerberos.py | 157 +++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 46 deletions(-) diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index a4485a14ab2..28622c47526 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -390,6 +390,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 ): @@ -982,9 +985,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", @@ -1809,6 +1813,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 @@ -2420,11 +2430,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=" Date: Sun, 19 Oct 2025 14:58:56 +0200 Subject: [PATCH 06/22] MS-NRPC: support Kerberos secure channel --- scapy/layers/msrpce/msnrpc.py | 206 ++++++++++++++++++++++------------ scapy/layers/ntlm.py | 29 +++-- 2 files changed, 152 insertions(+), 83 deletions(-) diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index fef1007e562..e7610a20cc9 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, @@ -114,15 +118,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 @@ -569,8 +575,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 +589,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,9 +658,12 @@ 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, ): @@ -667,39 +675,34 @@ def establish_secure_channel( :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) + + 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 +715,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 +781,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 +796,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 +824,44 @@ 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 netr_server_authkerb_response.status != 0: + # An error occured + 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/ntlm.py b/scapy/layers/ntlm.py index 1f6ab17a50a..c0ec9ffd2d0 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -1356,6 +1356,7 @@ def __init__( 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: @@ -2001,8 +2002,9 @@ 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:: @@ -2035,16 +2037,21 @@ 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: + self.DC_HOST = kwargs.pop("DC_HOST", None) + self.DC_NB_NAME = kwargs.pop("DC_NB_NAME", None) + if self.DC_HOST is None: # Get DC_IP 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_HOST = dc.ip + self.DC_FQDN = dc.samlogon.DnsHostName.decode().rstrip(".") + elif self.DC_NB_NAME is None: + raise ValueError("When providing DC_HOST, must provide DC_NB_NAME !") # If logging in via Kerberos self.ssp = ssp @@ -2074,7 +2081,7 @@ def _getSessionBaseKey(self, Context, ntlm): # Create NetlogonClient with PRIVACY client = NetlogonClient() - client.connect_and_bind(self.DC_IP) + client.connect(self.DC_HOST) # Establish the Netlogon secure channel (this will bind) try: @@ -2082,15 +2089,17 @@ def _getSessionBaseKey(self, Context, ntlm): # Login via classic NetlogonSSP client.establish_secure_channel( mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, - computername=self.COMPUTER_NB_NAME, - domainname=self.DOMAIN_NB_NAME, + 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, + ssp=self.ssp, ) except ValueError: log_runtime.warning( From cd4b7a5187a5879def8d9ee7d9a61963bb252beb Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 2 Nov 2025 12:34:21 +0100 Subject: [PATCH 07/22] Fix case in Kerberos check --- scapy/layers/kerberos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index 28622c47526..fdac3745922 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -3136,7 +3136,7 @@ def __init__( if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: if not host: raise ValueError("Invalid host") - if x509 is None and (not x509key or not ca): + if x509 is not None and (not x509key or not ca): raise ValueError("Must provide both 'x509', 'x509key' and 'ca' !") elif mode == self.MODE.TGS_REQ: if not ticket: From 8d9d1f3ea93f065808207b6c1d2ead075862444a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:47:28 +0100 Subject: [PATCH 08/22] HTTP client: allow to drop channel bindings --- scapy/layers/http.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scapy/layers/http.py b/scapy/layers/http.py index 016337738fc..577230e8bf4 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, From 60291c7a94560966b0cc2b08654818b61ba9c805 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:57:16 +0100 Subject: [PATCH 09/22] Add Kerberos doc --- doc/scapy/layers/kerberos.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index 168e2d7ecab..c6af2b34da4 100644 --- a/doc/scapy/layers/kerberos.rst +++ b/doc/scapy/layers/kerberos.rst @@ -68,6 +68,18 @@ 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") + - **Renew a TGT or ST**: .. code:: From 8fe1193a7a2cfb0b830a33527f1a4314bb3ca5c4 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:15:04 +0100 Subject: [PATCH 10/22] PEP8 fixes --- scapy/layers/kerberos.py | 10 +- scapy/layers/msrpce/msnrpc.py | 12 +- scapy/layers/spnego.py | 3 +- scapy/layers/tls/cert.py | 250 ++++++++++++++++++++-------------- 4 files changed, 165 insertions(+), 110 deletions(-) diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index fdac3745922..efc0c4dc211 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -5100,7 +5100,7 @@ def GSS_Init_sec_context( # Update UPN (could have been canonicalized) self.UPN = res.upn - # Store TGT, + # Store TGT, self.TGT = res.asrep.ticket self.TGTSessionKey = res.sessionkey else: @@ -5134,7 +5134,9 @@ def GSS_Init_sec_context( 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 !") + raise ValueError( + "Cannot use GSS_C_DELEG_FLAG when passed a service ticket !" + ) # Save ServerHostname if len(self.ST.sname.nameString) == 2: @@ -5199,7 +5201,6 @@ def GSS_Init_sec_context( # ) # ) - # Build and encrypt the full KRB_Authenticator ap_req.authenticator.encrypt( Context.STSessionKey, @@ -5207,8 +5208,7 @@ def GSS_Init_sec_context( crealm=crealm, cname=PrincipalName.fromUPN(self.UPN), cksum=Checksum( - cksumtype="KRB-AUTHENTICATOR", - checksum=authenticator_checksum + cksumtype="KRB-AUTHENTICATOR", checksum=authenticator_checksum ), ctime=ASN1_GENERALIZED_TIME(now_time), cusec=ASN1_INTEGER(0), diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index e7610a20cc9..edfb5352360 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -676,7 +676,7 @@ def establish_secure_channel( :param mode: one of NETLOGON_SECURE_CHANNEL_METHOD. This defines which method to use to establish the secure channel. :param UPN: the UPN of the computer account name that is used to establish - the secure channel. (e.g. WIN10@domain.local) + 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: @@ -687,6 +687,10 @@ def establish_secure_channel( :param ssp: a KerberosSSP to use (in Kerberos mode) """ 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: @@ -858,7 +862,11 @@ def establish_secure_channel( ndrendian=self.ndrendian, ) ) - if netr_server_authkerb_response.status != 0: + if ( + NetrServerAuthenticateKerberos_Response + not in netr_server_authkerb_response + or netr_server_authkerb_response.status != 0 + ): # An error occured netr_server_authkerb_response.show() raise ValueError("NetrServerAuthenticateKerberos failed !") diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index b9959078f14..aebebe88293 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -705,7 +705,7 @@ def from_cli_arguments( if ST 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) @@ -713,7 +713,6 @@ def from_cli_arguments( # Look for the ticketer that we'll use raise NotImplementedError - ssps.append(t.ssp()) elif ST is None: # In this case, KEY is supposed to be the user's key. diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index ed2df5a853d..a8d99f27bc9 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -140,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 @@ -215,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) @@ -232,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 @@ -251,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: @@ -326,11 +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 sigVal = bytes(cert.signatureValue) - return self.verify(bytes(tbsCert), sigVal, h=h, t='pkcs') + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") @property def pem(self): @@ -378,6 +383,7 @@ 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 @@ -431,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): @@ -440,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 @@ -472,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 @@ -502,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: @@ -529,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) @@ -575,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__ @@ -603,7 +617,7 @@ def signTBSCert(self, tbsCert, h="sha256"): """ sigAlg = tbsCert.signature h = h or hash_by_oid[sigAlg.algorithm.val] - sigVal = self.sign(bytes(tbsCert), h=h, t='pkcs') + sigVal = self.sign(bytes(tbsCert), h=h, t="pkcs") c = X509_Cert() c.tbsCertificate = tbsCert c.signatureAlgorithm = sigAlg @@ -611,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 = bytes(cert.signatureValue) - return self.verify(bytes(tbsCert), sigVal, h=h, t='pkcs') + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") @property def pem(self): @@ -631,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): @@ -655,7 +669,7 @@ def sign(self, data, h="sha256", **kwargs): Sign data. """ raise NotImplementedError - + @crypto_validator def verify(self, msg, sig, h="sha256", **kwargs): """ @@ -669,13 +683,30 @@ 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 @@ -698,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() @@ -724,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( @@ -748,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 @@ -757,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(bytes(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" @@ -776,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 @@ -785,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(bytes(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" @@ -803,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: @@ -976,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): @@ -1006,9 +1058,11 @@ 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) @@ -1028,7 +1082,7 @@ def der(self): def __eq__(self, other): return self.der == other.der - + def __hash__(self): return hash(self.der) @@ -1054,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 @@ -1167,6 +1226,7 @@ def show(self): # Certificate list # #################### + class CertList(list): """ An object that can store a list of Cert objects, load them and export them @@ -1183,8 +1243,9 @@ def __init__( # 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") + obj = _PKIObjMaker.__call__( + CertList, certList, _MAX_CERT_SIZE, "CERTIFICATE" + ) # Then parse the der until there's nothing left certList = [] @@ -1234,19 +1295,17 @@ def export(self, filename, fmt=None): @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), - ) + return "" % (len(self),) def show(self): for i, c in enumerate(self): - print(conf.color_theme.id(i, fmt="%04i"), end=' ') + print(conf.color_theme.id(i, fmt="%04i"), end=" ") print(repr(c)) @@ -1254,6 +1313,7 @@ def show(self): # Certificate chains # ###################### + class CertTree(CertList): """ An extension to CertList that additionally has a list of ROOT CAs @@ -1292,11 +1352,7 @@ def __init__( # 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() - ]) + 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) @@ -1315,10 +1371,7 @@ def tree(self): Get a tree-like object of the certificate list """ # We store the tree object as a dictionary that contains children. - tree = [ - (x, []) - for x in self.rootCAs - ] + tree = [(x, []) for x in self.rootCAs] # We'll empty this list eventually certList = list(self) @@ -1326,7 +1379,7 @@ def tree(self): # We make a list of certificates we have to search children for, and iterate # through it until it's emtpy. todo = list(tree) - + # Iterate while todo: cert, children = todo.pop() @@ -1344,6 +1397,7 @@ def getchain(self, cert): """ Return a chain of certificate that points from a ROOT CA to a certificate. """ + 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. @@ -1356,7 +1410,7 @@ def _rec_getchain(chain, curtree): if curchain: return curchain return None - + chain = _rec_getchain([], self.tree) if chain is not None: return CertTree(cert, chain) @@ -1375,6 +1429,7 @@ 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 @@ -1402,6 +1457,7 @@ def __repr__(self): len(self.rootCAs), ) + ####### # CMS # ####### @@ -1466,15 +1522,15 @@ def sign( 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, ) @@ -1491,11 +1547,7 @@ def sign( # 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() - ] + certificates = [x.x509Cert for x in certTree if not x.isSelfSigned()] # Build final structure return CMS_ContentInfo( @@ -1511,27 +1563,21 @@ def sign( eContent=message, ), certificates=( - [ - CMS_CertificateChoices( - certificate=cert - ) - for cert in certificates - ] if certificates else None + [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 + [CMS_RevocationInfoChoice(crl=crl) for crl in self.crls] + if self.crls + else None ), signerInfos=[ signerInfo, ], - ) + ), ) - + def verify( self, contentInfo: CMS_ContentInfo, @@ -1550,10 +1596,7 @@ def verify( signeddata = contentInfo.content # Build the certificate chain - certificates = [ - Cert(x.certificate) - for x in signeddata.certificates - ] + certificates = [Cert(x.certificate) for x in signeddata.certificates] certTree = CertTree(certificates, self.store) # Check there's at least one signature @@ -1579,13 +1622,18 @@ def verify( ) if contentType != signeddata.encapContentInfo.eContentType: - raise ValueError("Inconsistent 'contentType' was detected in packet !") + 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, - )) + raise ValueError( + "Expected '%s' but got '%s' contentType !" + % ( + eContentType, + contentType, + ) + ) except StopIteration: raise ValueError("Missing contentType in signedAttrs !") From 5ab0ae24ab5444b99ab39df3830a8521a78429e3 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:00:38 +0100 Subject: [PATCH 11/22] Cert.py: fix TreeChain tests --- scapy/layers/tls/cert.py | 13 +++++++- scapy/layers/x509.py | 1 - test/scapy/layers/tls/cert.uts | 61 +++++++++++++++++++++++----------- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index a8d99f27bc9..39a471bb68a 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -1357,7 +1357,12 @@ def __init__( for cert in self.rootCAs: certList.remove(cert) else: + # 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) @@ -1403,9 +1408,15 @@ def _rec_getchain(chain, curtree): # 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 @@ -1413,7 +1424,7 @@ def _rec_getchain(chain, curtree): chain = _rec_getchain([], self.tree) if chain is not None: - return CertTree(cert, chain) + return CertTree(chain) else: return None diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 5ca30babd0a..892af6941e7 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -14,7 +14,6 @@ from scapy.asn1.asn1 import ( ASN1_Codecs, ASN1_IA5_STRING, - ASN1_NULL, ASN1_OID, ASN1_PRINTABLE_STRING, ASN1_UTC_TIME, diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts index ace1b75e1dc..237c4f9aeaa 100644 --- a/test/scapy/layers/tls/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -614,30 +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) + += 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 + += CertTree class : show() + +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 + +repr_str = CertTree([], c0).show(ret=True) +assert repr_str == '/OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' + += CertTree class : verify + +CertTree([c1, c2]).verify(c0) +CertTree([c2]).verify(c1) + +try: + CertTree([c1]).verify(c0) + assert False +except ValueError: + pass + +try: + CertTree([c2]).verify(c0) + assert False +except ValueError: + pass -= Chain class : Checking chain construction -assert len(Chain([c0, c1, c2])) == 3 -assert len(Chain([c0], c1)) == 2 -len(Chain([c0], c2)) == 1 - -= Chain class : repr - -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 - -= Test __repr__ - -repr_str = Chain([], c0).__repr__() -assert repr_str == '__ /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' = Test GeneralizedTime From 8fff3d951af19267409247abc2431cfc67f1b589 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:41:08 +0100 Subject: [PATCH 12/22] Add missing NETLOGON flags --- scapy/layers/smb.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index 676021e1d6b..115de5f7473 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", From 144df4e8f780da79a0846dbc028534ae90cbd0c4 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:41:17 +0100 Subject: [PATCH 13/22] SPNEGO: support reading KRB5CCNAME --- scapy/layers/spnego.py | 49 +++++++++++++++++++++++++++++++++------ scapy/modules/ticketer.py | 29 ++++++++++++++++++----- 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index aebebe88293..2ce9500b045 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -16,6 +16,7 @@ `GSSAPI `_ """ +import os import struct from uuid import UUID @@ -640,9 +641,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. @@ -656,9 +659,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 @@ -682,6 +688,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 @@ -691,6 +701,7 @@ def from_cli_arguments( HashNt, HashAes256Sha96, HashAes128Sha96, + ccache, ] ): # yes. @@ -702,7 +713,7 @@ def from_cli_arguments( # Kerberos if kerberos and hostname: # Get ticket if we don't already have one. - if ST is None and ccache is not 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 @@ -710,11 +721,34 @@ def from_cli_arguments( t = Ticketer() t.open_ccache(ccache) - # Look for the ticketer that we'll use - raise NotImplementedError - - ssps.append(t.ssp()) - elif ST is None: + # 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()): + # 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 in spn: + # 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 @@ -748,6 +782,7 @@ def from_cli_arguments( KerberosSSP( UPN=UPN, ST=ST, + TGT=TGT, KEY=KEY, debug=debug, ) diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py index 78f1c7e234d..9d5a45b821f 100644 --- a/scapy/modules/ticketer.py +++ b/scapy/modules/ticketer.py @@ -859,12 +859,22 @@ 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 + return KerberosSSP( + TGT=ticket, + KEY=sessionkey, + UPN=upn, + SPN=None, # Use target_name only + ) + else: + # It's a ST + return KerberosSSP( + ST=ticket, + KEY=sessionkey, + UPN=upn, + SPN=spn, + ) elif isinstance(i, str): spn = i key = self.get_cred(spn) @@ -2576,3 +2586,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) From 17199717c191825e8addbb737e60a02c41657580 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:11:39 +0100 Subject: [PATCH 14/22] SPNEGO: add tests & fix bugs --- scapy/layers/spnego.py | 13 ++- scapy/libs/rfc3961.py | 2 +- test/scapy/layers/spnego.uts | 193 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 test/scapy/layers/spnego.uts diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index 2ce9500b045..9cbd85acec9 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -83,6 +83,7 @@ from scapy.layers.kerberos import ( Kerberos, KerberosSSP, + _parse_spn, _parse_upn, ) from scapy.layers.ntlm import ( @@ -672,11 +673,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: @@ -726,13 +725,15 @@ def from_cli_arguments( # - 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 in spn: + elif hostname.lower() == spn_host.lower(): # ST. We're done ! ssps.append(t.ssp(i)) break @@ -797,7 +798,11 @@ 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) diff --git a/scapy/libs/rfc3961.py b/scapy/libs/rfc3961.py index bc7e2e8aee5..e07d00c1df9 100644 --- a/scapy/libs/rfc3961.py +++ b/scapy/libs/rfc3961.py @@ -1451,7 +1451,7 @@ def prfplus(key, pepper): # RFC 4556 # ############ -def octetstring2key(etype: EncryptionType, x: bytes) -> bytes: +def octetstring2key(etype: EncryptionType, x: bytes) -> Key: """ RFC4556 octetstring2key:: diff --git a/test/scapy/layers/spnego.uts b/test/scapy/layers/spnego.uts new file mode 100644 index 00000000000..46844b59d49 --- /dev/null +++ b/test/scapy/layers/spnego.uts @@ -0,0 +1,193 @@ +% SPNEGO unit tests + ++ Special SPNEGO tests + += SPNEGOSSP.from_cli_arguments - Utils + +from unittest import mock + +NTLM = '1.3.6.1.4.1.311.2.2.10' +KERBEROS = '1.2.840.113554.1.2.2' + +# 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.supported_ssps) == 1 +assert ssp.supported_ssps[NTLM].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.supported_ssps) == 1 +assert ssp.supported_ssps[NTLM].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.supported_ssps) == 3 +assert ssp.supported_ssps[NTLM].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' +assert ssp.supported_ssps[KERBEROS].UPN == "Administrator@domain.local" + += 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.supported_ssps) == 2 +assert ssp.supported_ssps[KERBEROS].TGT +assert not ssp.supported_ssps[KERBEROS].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.supported_ssps) == 2 +assert ssp.supported_ssps[KERBEROS].TGT +assert not ssp.supported_ssps[KERBEROS].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.supported_ssps) == 2 +assert ssp.supported_ssps[KERBEROS].ST +assert not ssp.supported_ssps[KERBEROS].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 From d14731c258ab80f124a133995d3f939f821c6f9d Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:53:24 +0100 Subject: [PATCH 15/22] Update IKEv2 tests with new X509_AlgorithmIdentifier --- scapy/asn1fields.py | 12 ++++++------ scapy/layers/smb.py | 6 +++--- scapy/layers/spnego.py | 2 -- scapy/layers/x509.py | 4 +++- test/contrib/ikev2.uts | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index f2d8613af37..195fad9f775 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, @@ -489,6 +488,7 @@ def m2i(self, pkt, s): obj.set_val(pkt, None) else: for obj in self.seq: + # DEBUG: print(repr(obj), repr) try: s = obj.dissect(pkt, s) except ASN1F_badsequence: @@ -599,7 +599,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): @@ -769,7 +769,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 +852,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 +897,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/smb.py b/scapy/layers/smb.py index 115de5f7473..541ab10c292 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -1003,9 +1003,9 @@ class NETLOGON_SAM_LOGON_RESPONSE_NT40(NETLOGON): 0x00002000: "WS", 0x00004000: "DS_8", # >=2008R2 0x00008000: "DS_9", # >=2012 - 0x00010000: "DS_10", # >=2016 - 0x00020000: "DS_11", # >=2019 - 0x00040000: "DS_12", # >=2025 + 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/spnego.py b/scapy/layers/spnego.py index 9cbd85acec9..84136997a59 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -39,7 +39,6 @@ ASN1F_optional, ) from scapy.asn1packet import ASN1_Packet -from scapy.base_classes import Net from scapy.fields import ( FieldListField, LEIntEnumField, @@ -63,7 +62,6 @@ valid_ip6, ) -from scapy.layers.inet6 import Net6 from scapy.layers.gssapi import ( GSSAPI_BLOB, GSSAPI_BLOB_SIGNATURE, diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 892af6941e7..2fe7e72703e 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -893,8 +893,10 @@ class X509_AlgorithmIdentifier(ASN1_Packet): ), lambda pkt: pkt.algorithm.val == "1.2.840.113549.1.3.1", ), + ], - ASN1F_optional(ASN1F_NULL("parameters", None)), + # RFC4055 (=1.2.840.113549.1.1.11) / Default + ASN1F_optional(ASN1F_NULL("parameters", 0)), ) ) 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( From 591555427217d2c8cf0cb29a9dcc309a032eb248 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:21:49 +0100 Subject: [PATCH 16/22] doc: add FAST documentation --- doc/scapy/layers/kerberos.rst | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index c6af2b34da4..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 @@ -80,12 +80,29 @@ This section tries to give many usage examples, but isn't exhaustive. For more d >>> # 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**: From 5981bbbde5c4b3a6e8446bee78d7792e70c3d0c9 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:57:39 +0100 Subject: [PATCH 17/22] More correct omit --- scapy/asn1fields.py | 18 +++++++++++++++++- scapy/layers/x509.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 195fad9f775..c95c0841bdb 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -488,7 +488,6 @@ def m2i(self, pkt, s): obj.set_val(pkt, None) else: for obj in self.seq: - # DEBUG: print(repr(obj), repr) try: s = obj.dissect(pkt, s) except ASN1F_badsequence: @@ -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 ommited 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 x + + _CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 2fe7e72703e..7c50930ec81 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -36,6 +36,7 @@ ASN1F_ISO646_STRING, ASN1F_NULL, ASN1F_OID, + ASN1F_omit, ASN1F_optional, ASN1F_PACKET, ASN1F_PRINTABLE_STRING, @@ -866,6 +867,33 @@ class X509_AlgorithmIdentifier(ASN1_Packet): ASN1F_OID("algorithm", "1.2.840.113549.1.1.11"), 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." + ) + ), + ( + # 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( @@ -893,10 +921,9 @@ class X509_AlgorithmIdentifier(ASN1_Packet): ), lambda pkt: pkt.algorithm.val == "1.2.840.113549.1.3.1", ), - ], - # RFC4055 (=1.2.840.113549.1.1.11) / Default - ASN1F_optional(ASN1F_NULL("parameters", 0)), + # Default: fail, probably. This is most likely unimplemented. + ASN1F_NULL("parameters", 0), ) ) From 064221d794acf39199434da779360338a33dacea Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:57:50 +0100 Subject: [PATCH 18/22] PEP8 & CI --- .github/workflows/cifuzz.yml | 2 +- scapy/asn1fields.py | 4 ++-- scapy/layers/kerberos.py | 4 ++-- scapy/layers/msrpce/msnrpc.py | 2 +- scapy/layers/msrpce/rpcclient.py | 2 +- scapy/layers/tls/cert.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) 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/scapy/asn1fields.py b/scapy/asn1fields.py index c95c0841bdb..9a5284bddfc 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -686,7 +686,7 @@ def i2repr(self, pkt, x): class ASN1F_omit(ASN1F_field[None, None]): """ - ASN.1 field that is not specified. This is simply ommited on the network. + 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): @@ -695,7 +695,7 @@ def m2i(self, pkt, s): def i2m(self, pkt, x): # type: (ASN1_Packet, Optional[bytes]) -> bytes - return x + return b"" _CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index efc0c4dc211..7691a86adad 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -1397,7 +1397,7 @@ def make_checksum(self, text, h="sha256"): def verify_checksum(self, text): """ - Verifiy paChecksum and paChecksum2 + Verify paChecksum and paChecksum2 """ if self.paChecksum.val != Hash_SHA().digest(text): raise ValueError("Bad paChecksum checksum !") @@ -1438,7 +1438,7 @@ class KRB_AuthPack(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "supportedCMSTypes", - [], + None, X509_AlgorithmIdentifier, explicit_tag=0xA2, ), diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index edfb5352360..78c811840b1 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -867,7 +867,7 @@ def establish_secure_channel( not in netr_server_authkerb_response or netr_server_authkerb_response.status != 0 ): - # An error occured + # An error occurred netr_server_authkerb_response.show() raise ValueError("NetrServerAuthenticateKerberos failed !") diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index b4c0544210e..c092ffae24c 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -407,7 +407,7 @@ def _get_bind_context(self, interface): """ if interface in self.contexts: # We have already found acceptable contexts for this interface, - # re-use that. + # reuse that. return self.contexts[interface] # NDR 2.0 diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 39a471bb68a..075601848de 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -1382,7 +1382,7 @@ def tree(self): certList = list(self) # We make a list of certificates we have to search children for, and iterate - # through it until it's emtpy. + # through it until it's empty. todo = list(tree) # Iterate From b8daa97edeff0d8ca641170960745be9f5b2879f Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:19:51 +0100 Subject: [PATCH 19/22] Fix send test --- test/contrib/send.uts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7ea254091b01f36477340f31833dfcb45d03e78b Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:33:49 +0100 Subject: [PATCH 20/22] Handle missing cryptography --- scapy/layers/kerberos.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index 7691a86adad..b48a52898a0 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -3045,7 +3045,7 @@ def __init__( ip: Optional[str] = None, upn: Optional[str] = None, password: Optional[str] = None, - key: Optional[Key] = None, + key: Optional["Key"] = None, realm: Optional[str] = None, x509: Optional[Union[Cert, str]] = None, x509key: Optional[Union[PrivKey, str]] = None, @@ -3065,9 +3065,9 @@ def __init__( fast: bool = False, armor_ticket: KRB_Ticket = None, armor_ticket_upn: Optional[str] = None, - armor_ticket_skey: Optional[Key] = None, - key_list_req: List[EncryptionType] = [], - etypes: Optional[List[EncryptionType]] = None, + armor_ticket_skey: Optional['Key'] = None, + key_list_req: List['EncryptionType'] = [], + etypes: Optional[List['EncryptionType']] = None, pkinit_kex_method: PKINIT_KEX_METHOD = PKINIT_KEX_METHOD.DIFFIE_HELLMAN, port: int = 88, timeout: int = 5, @@ -3831,7 +3831,7 @@ def SENT_AS_REQ(self): def SENT_TGS_REQ(self): pass - def _process_padatas_and_key(self, padatas, etype: EncryptionType = None): + def _process_padatas_and_key(self, padatas, etype: "EncryptionType" = None): """ Process the PADATA, and generate missing keys if required. @@ -4184,7 +4184,7 @@ def krb_as_req( upn: str, spn: Optional[str] = None, ip: Optional[str] = None, - key: Optional[Key] = None, + key: Optional["Key"] = None, password: Optional[str] = None, realm: Optional[str] = None, host: str = "WIN10", From a2077665f0685c2367058ab84265c26f00ef3552 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:51:41 +0100 Subject: [PATCH 21/22] SPNEGO: big refactor --- scapy/layers/gssapi.py | 61 ++- scapy/layers/http.py | 2 +- scapy/layers/kerberos.py | 134 +++-- scapy/layers/ldap.py | 4 +- scapy/layers/msrpce/msnrpc.py | 30 +- scapy/layers/msrpce/rpcclient.py | 4 +- scapy/layers/msrpce/rpcserver.py | 5 +- scapy/layers/ntlm.py | 308 ++++++++---- scapy/layers/smb2.py | 6 +- scapy/layers/smbclient.py | 12 +- scapy/layers/smbserver.py | 3 +- scapy/layers/spnego.py | 827 +++++++++++++++++-------------- scapy/layers/x509.py | 3 +- scapy/modules/ticketer.py | 7 +- test/scapy/layers/kerberos.uts | 71 ++- test/scapy/layers/msnrpc.uts | 28 +- test/scapy/layers/ntlm.uts | 40 +- test/scapy/layers/smb.uts | 12 +- test/scapy/layers/spnego.uts | 162 +++++- 19 files changed, 1092 insertions(+), 627 deletions(-) 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 577230e8bf4..26fc3727eb0 100644 --- a/scapy/layers/http.py +++ b/scapy/layers/http.py @@ -944,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 b48a52898a0..a3125382ed5 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -136,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, @@ -2123,13 +2124,14 @@ 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, 62]: + 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 @@ -3065,12 +3067,13 @@ def __init__( fast: bool = False, armor_ticket: KRB_Ticket = None, armor_ticket_upn: Optional[str] = None, - armor_ticket_skey: Optional['Key'] = None, - key_list_req: List['EncryptionType'] = [], - etypes: Optional[List['EncryptionType']] = None, + armor_ticket_skey: Optional["Key"] = None, + key_list_req: List["EncryptionType"] = [], + etypes: Optional[List["EncryptionType"]] = None, pkinit_kex_method: PKINIT_KEX_METHOD = PKINIT_KEX_METHOD.DIFFIE_HELLMAN, port: int = 88, timeout: int = 5, + verbose: bool = True, **kwargs, ): import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 @@ -3192,6 +3195,7 @@ def __init__( self.result = None # Result self._timeout = timeout + self._verbose = verbose self._ip = ip self._port = port self.kdc_proxy = kdc_proxy @@ -3271,6 +3275,29 @@ def send(self, pkt): """ super(KerberosClient, self).send(KerberosTCPHeader() / pkt) + def _show_krb_error(self, error): + """ + Displays a Kerberos error + """ + if error.root.errorCode == 0x07: + # KDC_ERR_S_PRINCIPAL_UNKNOWN + if ( + isinstance(error.root.eData, KERB_ERROR_UNK) + and error.root.eData.dataType == -128 + ): + log_runtime.error( + "KerberosSSP: KDC requires U2U for SPN '%s' !" % error.root.getSPN() + ) + else: + log_runtime.error( + "KerberosSSP: KDC_ERR_S_PRINCIPAL_UNKNOWN for SPN '%s'" + % error.root.getSPN() + ) + else: + log_runtime.error(error.root.sprintf("KerberosSSP: Received %errorCode% !")) + if self._verbose: + error.show() + def _base_kdc_req(self, now_time): """ Return the KRB_KDC_REQ_BODY used in both AS-REQ and TGS-REQ @@ -3998,8 +4025,7 @@ def receive_krb_error_as_req(self, pkt): self.pre_auth = True raise self.BEGIN() else: - log_runtime.error("Received KRB_ERROR") - pkt.show() + self._show_krb_error(pkt) raise self.FINAL() @ATMT.receive_condition(SENT_AS_REQ, prio=2) @@ -4073,17 +4099,7 @@ def receive_krb_error_tgs_req(self, pkt): self.receive_krb_error_tgs_req(ferr) return - if ( - pkt.root.errorCode == 0x07 - and isinstance(pkt.root.eData, KERB_ERROR_UNK) - and pkt.root.eData.dataType == -128 - ): - log_runtime.warning( - "KDC requires U2U for SPN '%s' !" % pkt.root.getSPN() - ) - else: - log_runtime.warning("Received KRB_ERROR") - pkt.show() + self._show_krb_error(pkt) raise self.FINAL() @ATMT.receive_condition(SENT_TGS_REQ) @@ -4463,11 +4479,11 @@ def kpasswd( debug=debug, **kwargs, ) - Context, tok, negResult = ssp.GSS_Init_sec_context( + Context, tok, status = ssp.GSS_Init_sec_context( None, req_flags=0, # No GSS_C_MUTUAL_FLAG ) - if negResult != GSS_S_CONTINUE_NEEDED: + if status != GSS_S_CONTINUE_NEEDED: warning("SSP failed on initial GSS_Init_sec_context !") if tok: tok.show() @@ -4526,8 +4542,11 @@ def kpasswd( if KPASSWD_REP not in resp: resp.show() raise ValueError("Invalid response to KPASSWD_REQ !") - Context, tok, negResult = ssp.GSS_Init_sec_context(Context, resp.aprep) - if negResult != GSS_S_COMPLETE: + Context, tok, status = ssp.GSS_Init_sec_context( + Context, + input_token=resp.aprep, + ) + if status != GSS_S_COMPLETE: warning("SSP failed on subsequent GSS_Init_sec_context !") if tok: tok.show() @@ -4576,7 +4595,6 @@ class KerberosSSP(SSP): this IP using using the KEY when using U2U. """ - oid = "1.2.840.113554.1.2.2" auth_type = 0x10 class STATE(SSP.STATE): @@ -4671,6 +4689,15 @@ def __init__( self.SKEY_TYPE = SKEY_TYPE super(KerberosSSP, self).__init__(**kwargs) + def GSS_Inquire_names_for_mech(self): + mechs = [ + "1.2.840.48018.1.2.2", # MS KRB5 - Microsoft Kerberos 5 + "1.2.840.113554.1.2.2", # Kerberos 5 + ] + if self.U2U: + mechs.append("1.2.840.113554.1.2.2.3") # Kerberos 5 - User to User + return mechs + def GSS_GetMICEx(self, Context, msgs, qop_req=0): """ [MS-KILE] sect 3.4.5.6 @@ -5038,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, @@ -5076,16 +5103,16 @@ def GSS_Init_sec_context( 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 None: @@ -5096,7 +5123,12 @@ def GSS_Init_sec_context( key=self.KEY, password=self.PASSWORD, 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 @@ -5120,6 +5152,7 @@ def GSS_Init_sec_context( additional_tickets=additional_tickets, u2u=self.U2U, debug=self.debug, + verbose=bool(self.debug), ) if not res: # Failed to retrieve the ticket @@ -5273,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: @@ -5328,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, ): @@ -5354,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 @@ -5452,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: @@ -5525,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 @@ -5554,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: @@ -5567,7 +5602,9 @@ 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 @@ -5575,7 +5612,9 @@ def GSS_Passive( Context.state = self.STATE.FAILED 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: @@ -5620,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 78c811840b1..5933889c2d5 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -56,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 @@ -156,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) @@ -287,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 @@ -342,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) @@ -369,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: @@ -401,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: @@ -432,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) @@ -483,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, @@ -509,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, ): @@ -670,8 +679,9 @@ def establish_secure_channel( """ 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. diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index c092ffae24c..e3a1d46a435 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -585,7 +585,7 @@ def _bind( # 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, ) @@ -628,7 +628,7 @@ def _bind( 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: 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 c0ec9ffd2d0..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 @@ -453,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(" 48, + ), ] + def post_build(self, pkt, pay): + if self.Size is None: + pkt = struct.pack(": "the maximum lifetime is 36 hours" (lol, Kerberos has 5min) + NTLM_MaxLifetime = 36 * 3600 + def __init__( self, UPN=None, @@ -1367,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 @@ -1383,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 @@ -1442,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 @@ -1461,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 @@ -1473,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, @@ -1553,16 +1597,43 @@ 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, @@ -1571,37 +1642,34 @@ def GSS_Init_sec_context( 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] + ( [ @@ -1629,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"), ] ) @@ -1643,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: @@ -1666,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 @@ -1686,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 @@ -1701,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, ): @@ -1709,12 +1790,13 @@ 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, @@ -1800,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( @@ -1814,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 @@ -1823,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 @@ -1836,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: @@ -1848,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( @@ -1865,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: @@ -1946,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) @@ -1972,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, @@ -2007,22 +2115,36 @@ class NTLMSSP_DOMAIN(NTLMSSP): :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()" @@ -2030,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, @@ -2037,10 +2169,9 @@ def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): ) # Treat specific parameters - self.DC_HOST = kwargs.pop("DC_HOST", None) - self.DC_NB_NAME = kwargs.pop("DC_NB_NAME", None) - if self.DC_HOST 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 dc = dclocator( @@ -2048,10 +2179,7 @@ def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): timeout=timeout, debug=kwargs.get("debug", 0), ) - self.DC_HOST = dc.ip self.DC_FQDN = dc.samlogon.DnsHostName.decode().rstrip(".") - elif self.DC_NB_NAME is None: - raise ValueError("When providing DC_HOST, must provide DC_NB_NAME !") # If logging in via Kerberos self.ssp = ssp @@ -2067,31 +2195,31 @@ 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(self.DC_HOST) + 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, UPN=f"{self.COMPUTER_NB_NAME}@{self.DOMAIN_NB_NAME}", DC_FQDN=self.DC_FQDN, - HashNt=self.HASHNT, + HASHNT=self.HASHNT, ) else: # Login via KerberosSSP (Windows 2025) @@ -2099,6 +2227,8 @@ def _getSessionBaseKey(self, Context, ntlm): 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: 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 2239bc792c1..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: 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 84136997a59..a37091313b3 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -21,9 +21,9 @@ 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 ( @@ -32,11 +32,12 @@ 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.fields import ( @@ -56,6 +57,7 @@ XStrFixedLenField, XStrLenField, ) +from scapy.error import log_runtime from scapy.packet import Packet, bind_layers from scapy.utils import ( valid_ip, @@ -63,18 +65,19 @@ ) 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 @@ -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 @@ -805,63 +879,24 @@ def from_cli_arguments( # 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" + ), + ), + ) ) ), ) @@ -882,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) + # 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.mechToken = SPNEGO_Token(value=tok) - else: - # Subsequent client tokens - spnego_tok = SPNEGO_negToken( # GSSAPI_BLOB is stripped - token=SPNEGO_negTokenResp( - supportedMech=None, - negResult=None, - ) - ) - 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, ) @@ -1203,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/x509.py b/scapy/layers/x509.py index 7c50930ec81..b9280be9660 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -879,7 +879,8 @@ class X509_AlgorithmIdentifier(ASN1_Packet): 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." + pkt.algorithm.val[:21] == "2.16.840.1.101.3.4.2." or + pkt.algorithm.val[:11] == "1.3.14.3.2." ) ), ( diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py index 9d5a45b821f..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. @@ -861,11 +861,12 @@ def ssp(self, i): ticket, sessionkey, upn, spn = self.export_krb(i) if spn.startswith("krbtgt/"): # It's a TGT + kwargs.setdefault("SPN", None) # Use target_name only return KerberosSSP( TGT=ticket, KEY=sessionkey, UPN=upn, - SPN=None, # Use target_name only + **kwargs, ) else: # It's a ST @@ -874,6 +875,7 @@ def ssp(self, i): KEY=sessionkey, UPN=upn, SPN=spn, + **kwargs, ) elif isinstance(i, str): spn = i @@ -881,6 +883,7 @@ def ssp(self, i): return KerberosSSP( SPN=spn, KEY=key, + **kwargs, ) else: raise ValueError("Invalid 'i' value. Must be int or str") diff --git a/test/scapy/layers/kerberos.uts b/test/scapy/layers/kerberos.uts index b8a44f28169..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) @@ -1614,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' @@ -1660,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, [ @@ -1711,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 | @@ -1722,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) @@ -1747,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)) @@ -1765,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)) @@ -1781,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 index 46844b59d49..ff04b448091 100644 --- a/test/scapy/layers/spnego.uts +++ b/test/scapy/layers/spnego.uts @@ -2,13 +2,135 @@ + 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 -NTLM = '1.3.6.1.4.1.311.2.2.10' -KERBEROS = '1.2.840.113554.1.2.2' - # Detect password prompts def password_failure(*args, **kwargs): raise ValueError("Password was prompted unexpectedly !") @@ -35,8 +157,8 @@ ssp = test_pwinput( target="machine.domain.local", ) assert isinstance(ssp, SPNEGOSSP) -assert len(ssp.supported_ssps) == 1 -assert ssp.supported_ssps[NTLM].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' +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 @@ -57,8 +179,8 @@ ssp = test_pwfail( password="Password", ) assert isinstance(ssp, SPNEGOSSP) -assert len(ssp.supported_ssps) == 1 -assert ssp.supported_ssps[NTLM].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' +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 @@ -67,9 +189,11 @@ ssp = test_pwinput( target="machine.domain.local", ) assert isinstance(ssp, SPNEGOSSP) -assert len(ssp.supported_ssps) == 3 -assert ssp.supported_ssps[NTLM].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' -assert ssp.supported_ssps[KERBEROS].UPN == "Administrator@domain.local" +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 @@ -143,9 +267,9 @@ ssp = test_pwfail( target="machine.domain.local", use_krb5ccname=True, ) -assert len(ssp.supported_ssps) == 2 -assert ssp.supported_ssps[KERBEROS].TGT -assert not ssp.supported_ssps[KERBEROS].ST +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 @@ -154,9 +278,9 @@ ssp = test_pwfail( target="machine.domain.local", ccache=ccache_file ) -assert len(ssp.supported_ssps) == 2 -assert ssp.supported_ssps[KERBEROS].TGT -assert not ssp.supported_ssps[KERBEROS].ST +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 @@ -165,9 +289,9 @@ ssp = test_pwfail( target="dc1.domain.local", ccache=ccache_file ) -assert len(ssp.supported_ssps) == 2 -assert ssp.supported_ssps[KERBEROS].ST -assert not ssp.supported_ssps[KERBEROS].TGT +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].ST +assert not ssp.ssps[0].TGT = SPNEGOSSP.from_cli_arguments - UPN + CCache - Failure From ed7f1f1f1907f2e31550d192e804f892275180e1 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:37:10 +0100 Subject: [PATCH 22/22] Increase debugging logs --- test/scapy/layers/dcerpc.uts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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"))