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:: 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/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/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, diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index c8a3320d6e7..efc0c4dc211 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, @@ -145,7 +146,20 @@ from scapy.layers.inet import TCP, UDP from scapy.layers.smb import _NV_VERSION from scapy.layers.smb2 import STATUS_ERREF -from scapy.layers.tls.cert import Cert, PrivKey +from scapy.layers.tls.cert import ( + Cert, + CertList, + CertTree, + CMS_Engine, + PrivKey, +) +from scapy.layers.tls.crypto.hash import ( + Hash_SHA, + Hash_SHA256, + Hash_SHA384, + Hash_SHA512, +) +from scapy.layers.tls.crypto.groups import _ffdh_groups from scapy.layers.x509 import ( _CMS_ENCAPSULATED, CMS_ContentInfo, @@ -154,17 +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, ) @@ -356,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 ): @@ -450,8 +487,6 @@ class EncryptionKey(ASN1_Packet): ) def toKey(self): - from scapy.libs.rfc3961 import Key - return Key( etype=self.keytype.val, key=self.keyvalue.val, @@ -519,7 +554,7 @@ def get_usage(self): def verify(self, key, text, key_usage_number=None): """ - Decrypt and return the data contained in cipher. + Verify a signature of text using a key. :param key: the key to use to check the checksum :param text: the bytes to verify @@ -532,7 +567,7 @@ def verify(self, key, text, key_usage_number=None): def make(self, key, text, key_usage_number=None, cksumtype=None): """ - Encrypt text and set it into cipher. + Make a signature. :param key: the key to use to make the checksum :param text: the bytes to make a checksum of @@ -950,9 +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", @@ -1248,7 +1284,7 @@ class PA_PK_AS_REQ(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "trustedCertifiers", - [ExternalPrincipalIdentifier()], + None, ExternalPrincipalIdentifier, explicit_tag=0xA1, ), @@ -1277,11 +1313,59 @@ class PAChecksum2(ASN1_Packet): ), ) + def verify(self, text): + """ + Verify a checksum of text. + + :param text: the bytes to verify + """ + # [MS-PKCA] 2.2.3 - PAChecksum2 + + # Only some OIDs are supported. Dumb but readable code. + oid = self.algorithmIdentifier.algorithm.val + if oid == "1.3.14.3.2.26": + hashcls = Hash_SHA + elif oid == "2.16.840.1.101.3.4.2.1": + hashcls = Hash_SHA256 + elif oid == "2.16.840.1.101.3.4.2.2": + hashcls = Hash_SHA384 + elif oid == "2.16.840.1.101.3.4.2.3": + hashcls = Hash_SHA512 + else: + raise ValueError("Bad PAChecksum2 checksum !") + + if hashcls().digest(text) != self.checksum.val: + raise ValueError("Bad PAChecksum2 checksum !") + + def make(self, text, h="sha256"): + """ + Make a checksum. + + :param text: the bytes to make a checksum of + """ + # Only some OIDs are supported. Dumb but readable code. + if h == "sha1": + hashcls = Hash_SHA + self.algorithmIdentifier.algorithm = ASN1_OID("1.3.14.3.2.26") + elif h == "sha256": + hashcls = Hash_SHA256 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.1") + elif h == "sha384": + hashcls = Hash_SHA384 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.2") + elif h == "sha512": + hashcls = Hash_SHA512 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.3") + else: + raise ValueError("Bad PAChecksum2 checksum !") + + self.checksum = ASN1_STRING(hashcls().digest(text)) + # still RFC 4556 sect 3.2.1 -class PKAuthenticator(ASN1_Packet): +class KRB_PKAuthenticator(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( Microseconds("cusec", 0, explicit_tag=0xA0), @@ -1292,14 +1376,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 @@ -1314,13 +1418,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( @@ -1340,7 +1444,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( @@ -1349,7 +1453,7 @@ class AuthPack(ASN1_Packet): ) -_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = AuthPack +_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = KRB_AuthPack # sect 3.2.3 @@ -1709,6 +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 @@ -2013,7 +2123,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 +2132,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 +2223,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 +2239,11 @@ class KRB_ERROR(ASN1_Packet): 74: "KDC_ERR_REVOCATION_STATUS_UNAVAILABLE", 75: "KDC_ERR_CLIENT_NAME_MISMATCH", 76: "KDC_ERR_KDC_NAME_MISMATCH", + 77: "KDC_ERR_INCONSISTENT_KEY_PURPOSE", + 78: "KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED", + 79: "KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED", + 80: "KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED", + 81: "KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED", # draft-ietf-kitten-iakerb 85: "KRB_AP_ERR_IAKERB_KDC_NOT_FOUND", 86: "KRB_AP_ERR_IAKERB_KDC_NO_RESPONSE", @@ -2313,11 +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="I", Context.SendSeqNum) tok = KRB_InnerToken( @@ -4683,13 +4981,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 @@ -4756,8 +5047,6 @@ def GSS_Init_sec_context( # New context Context = self.CONTEXT(IsAcceptor=False, req_flags=req_flags) - from scapy.libs.rfc3961 import Key - if Context.state == self.STATE.INIT and self.U2U: # U2U - Get TGT Context.state = self.STATE.CLI_SENT_TGTREQ @@ -4776,12 +5065,14 @@ def GSS_Init_sec_context( if Context.state in [self.STATE.INIT, self.STATE.CLI_SENT_TGTREQ]: if not self.UPN: raise ValueError("Missing UPN attribute") + # Do we have a ST? if self.ST is None: # Client sends an AP-req if not self.SPN and not target_name: raise ValueError("Missing SPN/target_name attribute") additional_tickets = [] + if self.U2U: try: # GSSAPI / Kerberos @@ -4796,39 +5087,56 @@ def GSS_Init_sec_context( tgt_rep.show() raise ValueError("KerberosSSP: Unexpected token !") additional_tickets = [tgt_rep.ticket] - if self.TGT is not None: - if not self.KEY: - raise ValueError("Cannot use TGT without the KEY") - # Use TGT - res = krb_tgs_req( - upn=self.UPN, - spn=self.SPN or target_name, - ip=self.DC_IP, - sessionkey=self.KEY, - ticket=self.TGT, - additional_tickets=additional_tickets, - u2u=self.U2U, - debug=self.debug, - ) - else: - # Ask for TGT then ST - res = krb_as_and_tgs( + + if self.TGT is None: + # Get TGT. We were passed a kerberos key + res = krb_as_req( upn=self.UPN, - spn=self.SPN or target_name, ip=self.DC_IP, key=self.KEY, password=self.PASSWORD, - additional_tickets=additional_tickets, - u2u=self.U2U, debug=self.debug, ) + # Update UPN (could have been canonicalized) + self.UPN = res.upn + + # Store TGT, + self.TGT = res.asrep.ticket + self.TGTSessionKey = res.sessionkey + else: + # We have a TGT and were passed its key + self.TGTSessionKey = self.KEY + + # Get ST + if not self.TGTSessionKey: + raise ValueError("Cannot use TGT without the KEY") + + res = krb_tgs_req( + upn=self.UPN, + spn=self.SPN or target_name, + ip=self.DC_IP, + sessionkey=self.TGTSessionKey, + ticket=self.TGT, + additional_tickets=additional_tickets, + u2u=self.U2U, + debug=self.debug, + ) if not res: # Failed to retrieve the ticket return Context, None, GSS_S_FAILURE - self.ST, self.KEY = res.tgsrep.ticket, res.sessionkey + + # Store the service ticket and associated key + self.ST, Context.STSessionKey = res.tgsrep.ticket, res.sessionkey elif not self.KEY: raise ValueError("Must provide KEY with ST") - Context.STSessionKey = self.KEY + else: + # We were passed a ST and its key + Context.STSessionKey = self.KEY + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + raise ValueError( + "Cannot use GSS_C_DELEG_FLAG when passed a service ticket !" + ) # Save ServerHostname if len(self.ST.sname.nameString) == 2: @@ -4860,25 +5168,47 @@ def GSS_Init_sec_context( # Get the realm of the client _, crealm = _parse_upn(self.UPN) + # Build the RFC4121 authenticator checksum + authenticator_checksum = KRB_AuthenticatorChecksum( + # RFC 4121 sect 4.1.1.2 + # "The Bnd field contains the MD5 hash of channel bindings" + Bnd=( + chan_bindings.digestMD5() + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else (b"\x00" * 16) + ), + Flags=int(Context.flags), + ) + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + # Delegate TGT + raise NotImplementedError("GSS_C_DELEG_FLAG is not implemented !") + # authenticator_checksum.Deleg = KRB_CRED( + # tickets=[self.TGT], + # encPart=EncryptedData() + # ) + # authenticator_checksum.encPart.encrypt( + # Context.STSessionKey, + # EncKrbCredPart( + # ticketInfo=KrbCredInfo( + # key=EncryptionKey.fromKey(self.TGTSessionKey), + # prealm=ASN1_GENERAL_STRING(crealm), + # pname=PrincipalName.fromUPN(self.UPN), + # # TODO: rework API to pass starttime... here. + # sreralm=self.TGT.realm, + # sname=self.TGT.sname, + # ) + # ) + # ) + # Build and encrypt the full KRB_Authenticator ap_req.authenticator.encrypt( Context.STSessionKey, KRB_Authenticator( crealm=crealm, cname=PrincipalName.fromUPN(self.UPN), - # RFC 4121 checksum cksum=Checksum( - cksumtype="KRB-AUTHENTICATOR", - checksum=KRB_AuthenticatorChecksum( - # RFC 4121 sect 4.1.1.2 - # "The Bnd field contains the MD5 hash of channel bindings" - Bnd=( - chan_bindings.digestMD5() - if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS - else (b"\x00" * 16) - ), - Flags=int(Context.flags), - ), + cksumtype="KRB-AUTHENTICATOR", checksum=authenticator_checksum ), ctime=ASN1_GENERALIZED_TIME(now_time), cusec=ASN1_INTEGER(0), @@ -4895,7 +5225,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) + ), ) ), ), @@ -5004,7 +5336,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: @@ -5242,19 +5573,21 @@ def GSS_Passive( Context.state = self.STATE.CLI_SENT_APREQ else: Context.state = self.STATE.FAILED - return Context, status elif Context.state == self.STATE.CLI_SENT_APREQ: Context, _, status = self.GSS_Init_sec_context( Context, token, req_flags=req_flags ) if status == GSS_S_COMPLETE: + if req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + status = GSS_S_CONTINUE_NEEDED Context.state = self.STATE.SRV_SENT_APREP else: Context.state == self.STATE.FAILED - return Context, status + else: + # Unknown state. Don't crash though. + status = GSS_S_FAILURE - # Unknown state. Don't crash though. - return Context, GSS_S_FAILURE + return Context, status def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): if Context.IsAcceptor is not IsAcceptor: diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index fef1007e562..edfb5352360 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,38 @@ 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) + # We need to normalize here, since the functions require both the accountname + # and the normal (no dollar) computer name. + if computername.endswith("$"): + computername = computername[:-1] + + if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: + if ssp or KEY: + raise ValueError("Cannot use 'ssp' on 'KEY' in Authenticate3 mode !") + if not HASHNT: + if PASSWORD: + HASHNT = MD4le(PASSWORD) + else: + raise ValueError("Missing either 'PASSWORD' or 'HASHNT' !") + if "." in domainname: + raise ValueError( + "The UPN in Authenticate3 must have a NETBIOS domain name !" ) - ) - netr_server_req_chall_response.show() - raise ValueError + else: + if HASHNT: + raise ValueError("Cannot use 'HASHNT' in Kerberos mode !") # Calc NegotiateFlags NegotiateFlags = FlagValue( @@ -712,23 +719,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 +785,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 +800,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 +828,48 @@ def establish_secure_channel( domainname=domainname, computername=computername, ) + + # Finally alter context (to use the SSP) + if not self.alter_context(self.interface): + raise ValueError("Bind failed !") + elif mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos: + # We use the brand new NetrServerAuthenticateKerberos function NegotiateFlags += "Kerberos" - # TODO - raise NotImplementedError - # Finally alter context (to use the SSP) - self.alter_context() + # Set KerberosSSP and alter context + if ssp: + self.ssp = self.sock.session.ssp = ssp + else: + self.ssp = self.sock.session.ssp = KerberosSSP( + UPN=UPN, + SPN="netlogon/" + DC_FQDN, + PASSWORD=PASSWORD, + KEY=KEY, + ) + if not self.bind_or_alter(self.interface): + raise ValueError("Bind failed !") + + # Send AuthenticateKerberos request + netr_server_authkerb_response = self.sr1_req( + NetrServerAuthenticateKerberos_Request( + PrimaryName="\\\\" + DC_FQDN, + AccountName=computername + "$", + AccountType=secureChannelType, + ComputerName=computername, + NegotiateFlags=int(NegotiateFlags), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerAuthenticateKerberos_Response + not in netr_server_authkerb_response + or netr_server_authkerb_response.status != 0 + ): + # An error 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/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 diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 556faee0ea5..c0ec9ffd2d0 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,9 +1346,17 @@ def __init__( if HASHNT is None and PASSWORD is not None: HASHNT = MD4le(PASSWORD) self.HASHNT = HASHNT - self.USE_MIC = USE_MIC + self.VARIANT = VARIANT + if self.VARIANT != NTLM_VARIANT.RECENT: + log_runtime.warning( + "VARIANT != NTLM_VARIANT.RECENT. You shouldn't touch this !" + ) + self.USE_MIC = False + else: + self.USE_MIC = USE_MIC self.NTLM_VALUES = NTLM_VALUES if UPN is not None: + # Populate values used only in server mode. from scapy.layers.kerberos import _parse_upn try: @@ -1399,6 +1492,7 @@ def GSS_Init_sec_context( # Client: negotiate # Create a default token tok = NTLM_NEGOTIATE( + VARIANT=self.VARIANT, NegotiateFlags="+".join( [ "NEGOTIATE_UNICODE", @@ -1408,10 +1502,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 +1564,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 +1717,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 +1728,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 @@ -1898,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:: @@ -1932,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 @@ -1971,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: @@ -1979,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( 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", 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..9cbd85acec9 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -16,6 +16,7 @@ `GSSAPI `_ """ +import os import struct from uuid import UUID @@ -82,6 +83,7 @@ from scapy.layers.kerberos import ( Kerberos, KerberosSSP, + _parse_spn, _parse_upn, ) from scapy.layers.ntlm import ( @@ -640,8 +642,11 @@ def from_cli_arguments( HashAes128Sha96: bytes = None, kerberos_required: bool = False, ST=None, + TGT=None, KEY=None, + ccache: str = None, debug: int = 0, + use_krb5ccname: bool = False, ): """ Initialize a SPNEGOSSP from a list of many arguments. @@ -655,8 +660,12 @@ def from_cli_arguments( :param HashAes256Sha96: (bytes) if provided, used for auth (Kerberos) :param HashAes128Sha96: (bytes) if provided, used for auth (Kerberos) :param ST: if provided, the service ticket to use (Kerberos) + :param TGT: if provided, the TGT to use (Kerberos) :param KEY: if ST provided, the session key associated to the ticket (Kerberos). - Else, the user secret key. + This can be either for the ST or TGT. Else, the user secret key. + :param ccache: (str) if provided, a path to a CCACHE (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. """ kerberos = True hostname = None @@ -664,11 +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: @@ -680,6 +687,10 @@ def from_cli_arguments( # not a UPN: NTLM only kerberos = False + # If we're asked, check the environment for KRB5CCNAME + if use_krb5ccname and ccache is None and "KRB5CCNAME" in os.environ: + ccache = os.environ["KRB5CCNAME"] + # Do we need to ask the password? if all( x is None @@ -689,6 +700,7 @@ def from_cli_arguments( HashNt, HashAes256Sha96, HashAes128Sha96, + ccache, ] ): # yes. @@ -700,7 +712,44 @@ def from_cli_arguments( # Kerberos if kerberos and hostname: # Get ticket if we don't already have one. - if ST is None: + if ST is None and TGT is None and ccache is not None: + # In this case, load the KerberosSSP from ccache + from scapy.modules.ticketer import Ticketer + + # Import into a Ticketer object + t = Ticketer() + t.open_ccache(ccache) + + # Look for the ticket that we'll use. We chose: + # - either a ST if the SPN matches our target + # - else a TGT if we got nothing better + tgts = [] + for i, (tkt, key, upn, spn) in enumerate(t.iter_tickets()): + spn, _ = _parse_spn(spn) + spn_host = spn.split("/")[-1] + # Check that it's for the correct user + if upn.lower() == UPN.lower(): + # Check that it's either a TGT or a ST to the correct service + if spn.lower().startswith("krbtgt/"): + # TGT. Keep it, and see if we don't have a better ST. + tgts.append(t.ssp(i)) + elif hostname.lower() == spn_host.lower(): + # ST. We're done ! + ssps.append(t.ssp(i)) + break + else: + # No ST found + if tgts: + # Using a TGT ! + ssps.append(tgts[0]) + else: + # Nothing found + t.show() + raise ValueError( + f"Could not find a ticket for {upn}, either a " + f"TGT or towards {hostname}" + ) + elif ST is None and TGT is None: # In this case, KEY is supposed to be the user's key. from scapy.libs.rfc3961 import Key, EncryptionType @@ -734,6 +783,7 @@ def from_cli_arguments( KerberosSSP( UPN=UPN, ST=ST, + TGT=TGT, KEY=KEY, debug=debug, ) @@ -748,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/layers/tls/cert.py b/scapy/layers/tls/cert.py index b38f52ca073..39a471bb68a 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -4,36 +4,58 @@ # Copyright (C) 2008 Arnaud Ebalard # # 2015, 2016, 2017 Maxence Tury +# 2022-2025 Gabriel Potter """ -High-level methods for PKI objects (X.509 certificates, CRLs, asymmetric keys). -Supports both RSA and ECDSA objects. +High-level methods for PKI objects (X.509 certificates, CRLs, asymmetric keys, CMS). +Supports both RSA, ECDSA and EDDSA objects. The classes below are wrappers for the ASN.1 objects defined in x509.py. + +Example 1: Certificate & Private key +____________________________________ + For instance, here is what you could do in order to modify the subject public key info of a 'cert' and then resign it with whatever 'key':: - from scapy.layers.tls.cert import * - cert = Cert("cert.der") - k = PrivKeyRSA() # generate a private key - cert.setSubjectPublicKeyFromPrivateKey(k) - cert.resignWith(k) - cert.export("newcert.pem") - k.export("mykey.pem") + >>> from scapy.layers.tls.cert import * + >>> cert = Cert("cert.der") + >>> k = PrivKeyRSA() # generate a private key + >>> cert.setSubjectPublicKeyFromPrivateKey(k) + >>> cert.resignWith(k) + >>> cert.export("newcert.pem") + >>> k.export("mykey.pem") One could also edit arguments like the serial number, as such:: - from scapy.layers.tls.cert import * - c = Cert("mycert.pem") - c.tbsCertificate.serialNumber = 0x4B1D - k = PrivKey("mykey.pem") # import an existing private key - c.resignWith(k) - c.export("newcert.pem") + >>> from scapy.layers.tls.cert import * + >>> c = Cert("mycert.pem") + >>> c.tbsCertificate.serialNumber = 0x4B1D + >>> k = PrivKey("mykey.pem") # import an existing private key + >>> c.resignWith(k) + >>> c.export("newcert.pem") To export the public key of a private key:: - k = PrivKey("mykey.pem") - k.pubkey.export("mypubkey.pem") + >>> k = PrivKey("mykey.pem") + >>> k.pubkey.export("mypubkey.pem") + +Example 2: CertList and CertTree +________________________________ + +Load a .pem file that contains multiple certificates:: + + >>> l = CertList("ca_chain.pem") + >>> l.show() + 0000 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test CA...] + 0001 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test Client...] + +Use 'CertTree' to organize the certificates in a tree:: + + >>> tree = CertTree("ca_chain.pem") # or tree = CertTree(l) + >>> tree.show() + /C=Ulaanbaatar/OU=Scapy Test PKI/CN=Scapy Test CA [Self Signed] + /C=FR/OU=Scapy Test PKI/CN=Scapy Test Client [Not Self Signed] No need for obnoxious openssl tweaking anymore. :) """ @@ -43,30 +65,59 @@ import time from scapy.config import conf, crypto_validator +from scapy.compat import Self from scapy.error import warning from scapy.utils import binrepr -from scapy.asn1.asn1 import ASN1_BIT_STRING +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_NULL, + ASN1_OID, + ASN1_STRING, +) from scapy.asn1.mib import hash_by_oid +from scapy.packet import Packet from scapy.layers.x509 import ( + CMS_Attribute, + CMS_CertificateChoices, + CMS_ContentInfo, + CMS_EncapsulatedContentInfo, + CMS_IssuerAndSerialNumber, + CMS_RevocationInfoChoice, + CMS_SignedAttrsForSignature, + CMS_SignedData, + CMS_SignerInfo, ECDSAPrivateKey_OpenSSL, ECDSAPrivateKey, ECDSAPublicKey, - EdDSAPublicKey, EdDSAPrivateKey, + EdDSAPublicKey, RSAPrivateKey_OpenSSL, RSAPrivateKey, RSAPublicKey, + X509_AlgorithmIdentifier, X509_Cert, X509_CRL, X509_SubjectPublicKeyInfo, ) -from scapy.layers.tls.crypto.pkcs1 import pkcs_os2ip, _get_hash, \ - _EncryptAndVerifyRSA, _DecryptAndSignRSA -from scapy.compat import raw, bytes_encode +from scapy.layers.tls.crypto.pkcs1 import ( + _DecryptAndSignRSA, + _EncryptAndVerifyRSA, + _get_hash, + pkcs_os2ip, +) +from scapy.compat import bytes_encode + +# Typing imports +from typing import ( + List, + Optional, + Union, +) if conf.crypto_valid: from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa, ec, x25519 @@ -89,21 +140,24 @@ # loading huge file when importing a cert _MAX_KEY_SIZE = 50 * 1024 _MAX_CERT_SIZE = 50 * 1024 -_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big +_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big ##################################################################### # Some helpers ##################################################################### + @conf.commands.register def der2pem(der_string, obj="UNKNOWN"): """Convert DER octet string to PEM format (with optional header)""" # Encode a byte string in PEM format. Header advertises type. pem_string = "-----BEGIN %s-----\n" % obj base64_string = base64.b64encode(der_string).decode() - chunks = [base64_string[i:i + 64] for i in range(0, len(base64_string), 64)] # noqa: E501 - pem_string += '\n'.join(chunks) + chunks = [ + base64_string[i : i + 64] for i in range(0, len(base64_string), 64) + ] # noqa: E501 + pem_string += "\n".join(chunks) pem_string += "\n-----END %s-----\n" % obj return pem_string @@ -164,7 +218,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): raise Exception(error_msg) obj_path = bytes_encode(obj_path) - if (b'\x00' not in obj_path) and os.path.isfile(obj_path): + if (b"\x00" not in obj_path) and os.path.isfile(obj_path): _size = os.path.getsize(obj_path) if _size > obj_max_size: raise Exception(error_msg) @@ -181,7 +235,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): frmt = "PEM" pem = _raw der_list = split_pem(pem) - der = b''.join(map(pem2der, der_list)) + der = b"".join(map(pem2der, der_list)) else: frmt = "DER" der = _raw @@ -200,12 +254,14 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): # Public Keys # ############### + class _PubKeyFactory(_PKIObjMaker): """ Metaclass for PubKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ + def __call__(cls, key_path=None, cryptography_obj=None): # This allows to import cryptography objects directly if cryptography_obj is not None: @@ -275,12 +331,11 @@ class PubKey(metaclass=_PubKeyFactory): """ def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ + """Verifies either a Cert or an X509_Cert.""" + h = cert.getSignatureHashName() tbsCert = cert.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + sigVal = bytes(cert.signatureValue) + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") @property def pem(self): @@ -315,12 +370,20 @@ def export(self, filename, fmt=None): elif fmt == "PEM": return f.write(self.pem.encode()) + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + class PubKeyRSA(PubKey, _EncryptAndVerifyRSA): """ Wrapper for RSA keys based on _EncryptAndVerifyRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None): pubExp = pubExp or 65537 @@ -374,8 +437,7 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): return _EncryptAndVerifyRSA.encrypt(self, msg, t=t, h=h, mgf=mgf, L=L) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - return _EncryptAndVerifyRSA.verify( - self, msg, sig, t=t, h=h, mgf=mgf, L=L) + return _EncryptAndVerifyRSA.verify(self, msg, sig, t=t, h=h, mgf=mgf, L=L) class PubKeyECDSA(PubKey): @@ -383,6 +445,7 @@ class PubKeyECDSA(PubKey): Wrapper for ECDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -415,6 +478,7 @@ class PubKeyEdDSA(PubKey): Wrapper for EdDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or x25519.X25519PrivateKey @@ -445,12 +509,14 @@ def verify(self, msg, sig, **kwargs): # Private Keys # ################ + class _PrivKeyFactory(_PKIObjMaker): """ Metaclass for PrivKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ + def __call__(cls, key_path=None, cryptography_obj=None): """ key_path may be the path to either: @@ -472,11 +538,14 @@ def __call__(cls, key_path=None, cryptography_obj=None): if cryptography_obj is not None: # We (stupidly) need to go through the whole import process because RSA # does more than just importing the cryptography objects... - obj = _PKIObj("DER", cryptography_obj.private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() - )) + obj = _PKIObj( + "DER", + cryptography_obj.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ), + ) else: # Load from file obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) @@ -518,8 +587,10 @@ def __call__(cls, key_path=None, cryptography_obj=None): class _Raw_ASN1_BIT_STRING(ASN1_BIT_STRING): """A ASN1_BIT_STRING that ignores BER encoding""" + def __bytes__(self): return self.val_readable + __str__ = __bytes__ @@ -546,7 +617,7 @@ def signTBSCert(self, tbsCert, h="sha256"): """ sigAlg = tbsCert.signature h = h or hash_by_oid[sigAlg.algorithm.val] - sigVal = self.sign(raw(tbsCert), h=h, t='pkcs') + sigVal = self.sign(bytes(tbsCert), h=h, t="pkcs") c = X509_Cert() c.tbsCertificate = tbsCert c.signatureAlgorithm = sigAlg @@ -554,16 +625,16 @@ def signTBSCert(self, tbsCert, h="sha256"): return c def resignCert(self, cert): - """ Rewrite the signature of either a Cert or an X509_Cert. """ + """Rewrite the signature of either a Cert or an X509_Cert.""" return self.signTBSCert(cert.tbsCertificate, h=None) def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ + """Verifies either a Cert or an X509_Cert.""" tbsCert = cert.tbsCertificate sigAlg = tbsCert.signature h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + sigVal = bytes(cert.signatureValue) + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") @property def pem(self): @@ -574,7 +645,7 @@ def der(self): return self.key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) def export(self, filename, fmt=None): @@ -592,19 +663,50 @@ def export(self, filename, fmt=None): elif fmt == "PEM": return f.write(self.pem.encode()) + @crypto_validator + def sign(self, data, h="sha256", **kwargs): + """ + Sign data. + """ + raise NotImplementedError + + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + class PrivKeyRSA(PrivKey, _DecryptAndSignRSA): """ Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator - def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, - prime1=None, prime2=None, coefficient=None, - exponent1=None, exponent2=None, privExp=None): + def fill_and_store( + self, + modulus=None, + modulusLen=None, + pubExp=None, + prime1=None, + prime2=None, + coefficient=None, + exponent1=None, + exponent2=None, + privExp=None, + ): pubExp = pubExp or 65537 - if None in [modulus, prime1, prime2, coefficient, privExp, - exponent1, exponent2]: + if None in [ + modulus, + prime1, + prime2, + coefficient, + privExp, + exponent1, + exponent2, + ]: # note that the library requires every parameter # in order to call RSAPrivateNumbers(...) # if one of these is missing, we generate a whole new key @@ -627,10 +729,15 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, if modulusLen and real_modulusLen != modulusLen: warning("modulus and modulusLen do not match!") pubNum = rsa.RSAPublicNumbers(n=modulus, e=pubExp) - privNum = rsa.RSAPrivateNumbers(p=prime1, q=prime2, - dmp1=exponent1, dmq1=exponent2, - iqmp=coefficient, d=privExp, - public_numbers=pubNum) + privNum = rsa.RSAPrivateNumbers( + p=prime1, + q=prime2, + dmp1=exponent1, + dmq1=exponent2, + iqmp=coefficient, + d=privExp, + public_numbers=pubNum, + ) self.key = privNum.private_key(default_backend()) pubkey = self.key.public_key() @@ -653,10 +760,16 @@ def import_from_asn1pkt(self, privkey): exponent1 = privkey.exponent1.val exponent2 = privkey.exponent2.val coefficient = privkey.coefficient.val - self.fill_and_store(modulus=modulus, pubExp=pubExp, - privExp=privExp, prime1=prime1, prime2=prime2, - exponent1=exponent1, exponent2=exponent2, - coefficient=coefficient) + self.fill_and_store( + modulus=modulus, + pubExp=pubExp, + privExp=privExp, + prime1=prime1, + prime2=prime2, + exponent1=exponent1, + exponent2=exponent2, + coefficient=coefficient, + ) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): return self.pubkey.verify( @@ -677,6 +790,7 @@ class PrivKeyECDSA(PrivKey): Wrapper for ECDSA keys based on SigningKey from ecdsa library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -686,8 +800,9 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) # noqa: E501 self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) self.marker = "EC PRIVATE KEY" @@ -705,6 +820,7 @@ class PrivKeyEdDSA(PrivKey): Wrapper for EdDSA keys Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or x25519.X25519PrivateKey @@ -714,8 +830,9 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) # noqa: E501 self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) self.marker = "PRIVATE KEY" @@ -732,21 +849,25 @@ def sign(self, data, **kwargs): # Certificates # ################ + class _CertMaker(_PKIObjMaker): """ Metaclass for Cert creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path=None, cryptography_obj=None): # This allows to import cryptography objects directly if cryptography_obj is not None: - obj = _PKIObj("DER", cryptography_obj.public_bytes( - encoding=serialization.Encoding.DER, - )) + obj = _PKIObj( + "DER", + cryptography_obj.public_bytes( + encoding=serialization.Encoding.DER, + ), + ) else: # Load from file - obj = _PKIObjMaker.__call__(cls, cert_path, - _MAX_CERT_SIZE, "CERTIFICATE") + obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CERT_SIZE, "CERTIFICATE") obj.__class__ = Cert obj.marker = "CERTIFICATE" try: @@ -771,7 +892,6 @@ def import_from_asn1pkt(self, cert): self.x509Cert = cert tbsCert = cert.tbsCertificate - self.tbsCertificate = tbsCert if tbsCert.version: self.version = tbsCert.version.val + 1 @@ -801,7 +921,7 @@ def import_from_asn1pkt(self, cert): raise Exception(error_msg) self.notAfter_str_simple = time.strftime("%x", self.notAfter) - self.pubKey = PubKey(raw(tbsCert.subjectPublicKeyInfo)) + self.pubKey = PubKey(bytes(tbsCert.subjectPublicKeyInfo)) if tbsCert.extensions: for extn in tbsCert.extensions: @@ -816,7 +936,7 @@ def import_from_asn1pkt(self, cert): elif extn.extnID.oidname == "authorityKeyIdentifier": self.authorityKeyID = extn.extnValue.keyIdentifier.val - self.signatureValue = raw(cert.signatureValue) + self.signatureValue = bytes(cert.signatureValue) self.signatureLen = len(self.signatureValue) def isIssuerCert(self, other): @@ -846,14 +966,19 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): return self.pubKey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) - def getSignatureHash(self): + def getSignatureHashName(self): """ - Return the hash used by the 'signatureAlgorithm' + Return the hash name used by the 'signatureAlgorithm'. """ tbsCert = self.tbsCertificate sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - return _get_hash(h) + return hash_by_oid[sigAlg.algorithm.val] + + def getSignatureHash(self): + """ + Return the hash cryptography object used by the 'signatureAlgorithm' + """ + return _get_hash(self.getSignatureHashName()) def setSubjectPublicKeyFromPrivateKey(self, key): """ @@ -901,17 +1026,19 @@ def remainingDays(self, now=None): now = time.localtime() elif isinstance(now, str): try: - if '/' in now: - now = time.strptime(now, '%m/%d/%y') + if "/" in now: + now = time.strptime(now, "%m/%d/%y") else: - now = time.strptime(now, '%b %d %H:%M:%S %Y %Z') + now = time.strptime(now, "%b %d %H:%M:%S %Y %Z") except Exception: - warning("Bad time string provided, will use localtime() instead.") # noqa: E501 + warning( + "Bad time string provided, will use localtime() instead." + ) # noqa: E501 now = time.localtime() now = time.mktime(now) nft = time.mktime(self.notAfter) - diff = (nft - now) / (24. * 3600) + diff = (nft - now) / (24.0 * 3600) return diff def isRevoked(self, crl_list): @@ -931,14 +1058,20 @@ def isRevoked(self, crl_list): Cert. Otherwise, the issuers are simply compared. """ for c in crl_list: - if (self.authorityKeyID is not None and - c.authorityKeyID is not None and - self.authorityKeyID == c.authorityKeyID): + if ( + self.authorityKeyID is not None + and c.authorityKeyID is not None + and self.authorityKeyID == c.authorityKeyID + ): return self.serial in (x[0] for x in c.revoked_cert_serials) elif self.issuer == c.issuer: return self.serial in (x[0] for x in c.revoked_cert_serials) return False + @property + def tbsCertificate(self): + return self.x509Cert.tbsCertificate + @property def pem(self): return der2pem(self.der, self.marker) @@ -947,6 +1080,12 @@ def pem(self): def der(self): return bytes(self.x509Cert) + def __eq__(self, other): + return self.der == other.der + + def __hash__(self): + return hash(self.der) + def export(self, filename, fmt=None): """ Export certificate in 'fmt' format (DER or PEM) to file 'filename' @@ -969,18 +1108,23 @@ def show(self): print("Validity: %s to %s" % (self.notBefore_str, self.notAfter_str)) def __repr__(self): - return "[X.509 Cert. Subject:%s, Issuer:%s]" % (self.subject_str, self.issuer_str) # noqa: E501 + return "[X.509 Cert. Subject:%s, Issuer:%s]" % ( + self.subject_str, + self.issuer_str, + ) # noqa: E501 ################################ # Certificate Revocation Lists # ################################ + class _CRLMaker(_PKIObjMaker): """ Metaclass for CRL creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path): obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CRL_SIZE, "X509 CRL") obj.__class__ = CRL @@ -1004,7 +1148,7 @@ def import_from_asn1pkt(self, crl): self.x509CRL = crl tbsCertList = crl.tbsCertList - self.tbsCertList = raw(tbsCertList) + self.tbsCertList = bytes(tbsCertList) if tbsCertList.version: self.version = tbsCertList.version.val + 1 @@ -1057,7 +1201,7 @@ def import_from_asn1pkt(self, crl): revoked.append((serial, date)) self.revoked_cert_serials = revoked - self.signatureValue = raw(crl.signatureValue) + self.signatureValue = bytes(crl.signatureValue) self.signatureLen = len(self.signatureValue) def isIssuerCert(self, other): @@ -1078,140 +1222,466 @@ def show(self): print("nextUpdate: %s" % self.nextUpdate_str) +#################### +# Certificate list # +#################### + + +class CertList(list): + """ + An object that can store a list of Cert objects, load them and export them + into DER/PEM format. + """ + + def __init__( + self, + certList: Union[Self, List[Cert], Cert, str], + ): + """ + Construct a list of certificates/CRLs to be used as list of ROOT certificates. + """ + # Parse the certificate list / CA + if isinstance(certList, str): + # It's a path. First get the _PKIObj + obj = _PKIObjMaker.__call__( + CertList, certList, _MAX_CERT_SIZE, "CERTIFICATE" + ) + + # Then parse the der until there's nothing left + certList = [] + payload = obj._der + while payload: + cert = X509_Cert(payload) + if conf.raw_layer in cert.payload: + payload = cert.payload.load + else: + payload = None + cert.remove_payload() + certList.append(Cert(cert)) + + self.frmt = obj.frmt + elif isinstance(certList, Cert): + certList = [certList] + self.frmt = "PEM" + else: + self.frmt = "PEM" + + super(CertList, self).__init__(certList) + + def findCertByIssuer(self, issuer): + """ + Find a certificate in the list by issuer. + """ + for cert in self: + if cert.issuer == issuer: + return cert + raise KeyError("Certificate not found !") + + def export(self, filename, fmt=None): + """ + Export a list of certificates 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + @property + def der(self): + return b"".join(x.der for x in self) + + @property + def pem(self): + return "".join(x.pem for x in self) + + def __repr__(self): + return "" % (len(self),) + + def show(self): + for i, c in enumerate(self): + print(conf.color_theme.id(i, fmt="%04i"), end=" ") + print(repr(c)) + + ###################### # Certificate chains # ###################### -class Chain(list): + +class CertTree(CertList): """ - Basically, an enhanced array of Cert. + An extension to CertList that additionally has a list of ROOT CAs + that are trusted. + + Example:: + + >>> tree = CertTree("ca_chain.pem") + >>> tree.show() + /CN=DOMAIN-DC1-CA/dc=DOMAIN [Self Signed] + /CN=Administrator/dc=DOMAIN [Not Self Signed] """ - def __init__(self, certList, cert0=None): + __slots__ = ["frmt", "rootCAs"] + + def __init__( + self, + certList: Union[List[Cert], CertList, str], + rootCAs: Union[List[Cert], CertList, Cert, str, None] = None, + ): """ - Construct a chain of certificates starting with a self-signed - certificate (or any certificate submitted by the user) - and following issuer/subject matching and signature validity. - If there is exactly one chain to be constructed, it will be, - but if there are multiple potential chains, there is no guarantee - that the retained one will be the longest one. - As Cert and CRL classes both share an isIssuerCert() method, - the trailing element of a Chain may alternatively be a CRL. + Construct a chain of certificates that follows issuer/subject matching and + respects signature validity. Note that we do not check AKID/{SKID/issuer/serial} matching, nor the presence of keyCertSign in keyUsage extension (if present). + + :param certList: a list of Cert/CRL objects (or path to PEM/DER file containing + multiple certs/CRL) to try to chain. + :param rootCAs: (optional) a list of certificates to trust. If not provided, + trusts any self-signed certificates from the certList. """ - list.__init__(self, ()) - if cert0: - self.append(cert0) + # Parse the certificate list + certList = CertList(certList) + + # Find the ROOT CAs if store isn't specified + if not rootCAs: + # Build cert store. + self.rootCAs = CertList([x for x in certList if x.isSelfSigned()]) + # And remove those certs from the list + for cert in self.rootCAs: + certList.remove(cert) else: - for root_candidate in certList: - if root_candidate.isSelfSigned(): - self.append(root_candidate) - certList.remove(root_candidate) - break - - if len(self) > 0: - while certList: - tmp_len = len(self) - for c in certList: - if c.isIssuerCert(self[-1]): - self.append(c) - certList.remove(c) - break - if len(self) == tmp_len: - # no new certificate appended to self - break - - def verifyChain(self, anchors, untrusted=None): + # Store cert store. + self.rootCAs = CertList(rootCAs) + # And remove those certs from the list if present (remove dups) + for cert in self.rootCAs: + if cert in certList: + certList.remove(cert) + + # Append our root CAs to the certList + certList.extend(self.rootCAs) + + # Super instantiate + super(CertTree, self).__init__(certList) + + @property + def tree(self): """ - Perform verification of certificate chains for that certificate. - A list of anchors is required. The certificates in the optional - untrusted list may be used as additional elements to the final chain. - On par with chain instantiation, only one chain constructed with the - untrusted candidates will be retained. Eventually, dates are checked. + Get a tree-like object of the certificate list """ - untrusted = untrusted or [] - for a in anchors: - chain = Chain(self + untrusted, a) - if len(chain) == 1: # anchor only - continue - # check that the chain does not exclusively rely on untrusted - if any(c in chain[1:] for c in self): - for c in chain: - if c.remainingDays() < 0: - break - if c is chain[-1]: # we got to the end of the chain - return chain - return None - - def verifyChainFromCAFile(self, cafile, untrusted_file=None): + # We store the tree object as a dictionary that contains children. + tree = [(x, []) for x in self.rootCAs] + + # We'll empty this list eventually + certList = list(self) + + # We make a list of certificates we have to search children for, and iterate + # through it until it's 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): """ - Does the same job as .verifyChain() but using the list of anchors - from the cafile. As for .verifyChain(), a list of untrusted - certificates can be passed (as a file, this time). + Return a chain of certificate that points from a ROOT CA to a certificate. """ - try: - with open(cafile, "rb") as f: - ca_certs = f.read() - except Exception: - raise Exception("Could not read from cafile") - anchors = [Cert(c) for c in split_pem(ca_certs)] + def _rec_getchain(chain, curtree): + # See if an element of the current tree signs the cert, if so add it to + # the chain, else recurse. + for c, subtree in curtree: + curchain = chain + [c] + # If 'cert' is issued by c + if cert.isIssuerCert(c): + # Final node of the chain ! + # (add the final cert if not self signed) + if c != cert: + curchain += [cert] + return curchain + else: + # Not the final node of the chain ! Recurse. + curchain = _rec_getchain(curchain, subtree) + if curchain: + return curchain + return None + + chain = _rec_getchain([], self.tree) + if chain is not None: + return CertTree(chain) + else: + return None - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + def verify(self, cert): + """ + Verify that a certificate is properly signed. + """ + # Check that we can find a chain to this certificate + if not self.getchain(cert): + raise ValueError("Certificate verification failed !") + + def show(self, ret: bool = False): + """ + Return the CertTree as a string certificate tree + """ + + def _rec_show(c, children, lvl=0): + s = "" + # Process the current CA + if c: + if not c.isSelfSigned(): + s += "%s [Not Self Signed]\n" % c.subject_str + else: + s += "%s [Self Signed]\n" % c.subject_str + s = lvl * " " + s + lvl += 1 + # Process all sub-CAs at a lower level + for child, subchildren in children: + s += _rec_show(child, subchildren, lvl=lvl) + return s + + showed = _rec_show(None, self.tree) + if ret: + return showed + else: + print(showed) + + def __repr__(self): + return "" % ( + len(self), + len(self.rootCAs), + ) + + +####### +# CMS # +####### + +# RFC3852 - return self.verifyChain(anchors, untrusted) - def verifyChainFromCAPath(self, capath, untrusted_file=None): +class CMS_Engine: + """ + A utility class to perform CMS/PKCS7 operations, as specified by RFC3852. + + :param store: a ROOT CA certificate list to trust. + :param crls: a list of CRLs to include. This is currently not checked. + """ + + def __init__( + self, + store: CertList, + crls: List[X509_CRL] = [], + ): + self.store = store + self.crls = crls + + def sign( + self, + message: Union[bytes, Packet], + eContentType: ASN1_OID, + cert: Cert, + key: PrivKey, + h: Optional[str] = None, + ): """ - Does the same job as .verifyChainFromCAFile() but using the list - of anchors in capath directory. The directory should (only) contain - certificates files in PEM format. As for .verifyChainFromCAFile(), - a list of untrusted certificates can be passed as a file - (concatenation of the certificates in PEM format). + Sign a message using CMS. + + :param message: the inner content to sign. + :param eContentType: the OID of the inner content. + :param cert: the certificate whose key to use use for signing. + :param key: the private key to use for signing. + :param h: the hash to use (default: same as the certificate's signature) + + We currently only support X.509 certificates ! """ - try: - anchors = [] - for cafile in os.listdir(capath): - with open(os.path.join(capath, cafile), "rb") as fd: - anchors.append(Cert(fd.read())) - except Exception: - raise Exception("capath provided is not a valid cert path") + # RFC3852 - 5.4. Message Digest Calculation Process + h = h or cert.getSignatureHashName() + hash = hashes.Hash(_get_hash(h)) + hash.update(bytes(message)) + hashed_message = hash.finalize() + + # 5.5. Signature Generation Process + signerInfo = CMS_SignerInfo( + version=1, + sid=CMS_IssuerAndSerialNumber( + issuer=cert.tbsCertificate.issuer, + serialNumber=cert.tbsCertificate.serialNumber, + ), + digestAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(h), + parameters=ASN1_NULL(0), + ), + signedAttrs=[ + CMS_Attribute( + attrType=ASN1_OID("contentType"), + attrValues=[ + eContentType, + ], + ), + CMS_Attribute( + attrType=ASN1_OID("messageDigest"), + # "A message-digest attribute MUST have a single attribute value" + attrValues=[ + ASN1_STRING(hashed_message), + ], + ), + ], + signatureAlgorithm=cert.tbsCertificate.signature, + ) + signerInfo.signature = ASN1_STRING( + key.sign( + bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + h=h, + ) + ) - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + # Build a chain of X509_Cert to ship (but skip the ROOT certificate) + certTree = CertTree(cert, self.store) + certificates = [x.x509Cert for x in certTree if not x.isSelfSigned()] + + # Build final structure + return CMS_ContentInfo( + contentType=ASN1_OID("id-signedData"), + content=CMS_SignedData( + version=3 if certificates else 1, + digestAlgorithms=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(h), + parameters=ASN1_NULL(0), + ), + encapContentInfo=CMS_EncapsulatedContentInfo( + eContentType=eContentType, + eContent=message, + ), + certificates=( + [CMS_CertificateChoices(certificate=cert) for cert in certificates] + if certificates + else None + ), + crls=( + [CMS_RevocationInfoChoice(crl=crl) for crl in self.crls] + if self.crls + else None + ), + signerInfos=[ + signerInfo, + ], + ), + ) + + def verify( + self, + contentInfo: CMS_ContentInfo, + eContentType: Optional[ASN1_OID] = None, + ): + """ + Verify a CMS message against the list of trusted certificates, + and return the unpacked message if the verification succeeds. - return self.verifyChain(anchors, untrusted) + :param contentInfo: the ContentInfo whose signature to verify + :param eContentType: if provided, verifies that the content type is valid + """ + if contentInfo.contentType.oidname != "id-signedData": + raise ValueError("ContentInfo isn't signed !") - def __repr__(self): - llen = len(self) - 1 - if llen < 0: - return "" - c = self[0] - s = "__ " - if not c.isSelfSigned(): - s += "%s [Not Self Signed]\n" % c.subject_str - else: - s += "%s [Self Signed]\n" % c.subject_str - idx = 1 - while idx <= llen: - c = self[idx] - s += "%s_ %s" % (" " * idx * 2, c.subject_str) - if idx != llen: - s += "\n" - idx += 1 - return s + signeddata = contentInfo.content + + # Build the certificate chain + certificates = [Cert(x.certificate) for x in signeddata.certificates] + certTree = CertTree(certificates, self.store) + + # Check there's at least one signature + if not signeddata.signerInfos: + raise ValueError("ContentInfo contained no signature !") + + # Check all signatures + for signerInfo in signeddata.signerInfos: + # Find certificate in the chain that did this + cert: Cert = certTree.findCertByIssuer(signerInfo.sid.get_issuer()) + + # Verify certificate signature + certTree.verify(cert) + + # Verify the message hash + if signerInfo.signedAttrs: + # Verify the contentType + try: + contentType = next( + x.attrValues[0] + for x in signerInfo.signedAttrs + if x.attrType.oidname == "contentType" + ) + + if contentType != signeddata.encapContentInfo.eContentType: + raise ValueError( + "Inconsistent 'contentType' was detected in packet !" + ) + + if eContentType is not None and eContentType != contentType: + raise ValueError( + "Expected '%s' but got '%s' contentType !" + % ( + eContentType, + contentType, + ) + ) + except StopIteration: + raise ValueError("Missing contentType in signedAttrs !") + + # Verify the messageDigest value + try: + # "A message-digest attribute MUST have a single attribute value" + messageDigest = next( + x.attrValues[0].val + for x in signerInfo.signedAttrs + if x.attrType.oidname == "messageDigest" + ) + + # Re-calculate hash + h = signerInfo.digestAlgorithm.algorithm.oidname + hash = hashes.Hash(_get_hash(h)) + hash.update(bytes(signeddata.encapContentInfo.eContent)) + hashed_message = hash.finalize() + + if hashed_message != messageDigest: + raise ValueError("Invalid messageDigest value !") + except StopIteration: + raise ValueError("Missing messageDigest in signedAttrs !") + + # Verify the signature + cert.verify( + msg=bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + sig=signerInfo.signature.val, + ) + else: + cert.verify( + msg=bytes(signeddata.encapContentInfo), + sig=signerInfo.signature.val, + ) + + # Return the content + return signeddata.encapContentInfo.eContent diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 1f1afe4f9c9..892af6941e7 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -7,14 +7,13 @@ # Cool history about this file: http://natisbad.org/scapy/index.html """ -X.509 certificates and other crypto-related ASN.1 structures +X.509 certificates, OCSP, CRL, CMS and other crypto-related ASN.1 structures """ from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1.asn1 import ( ASN1_Codecs, ASN1_IA5_STRING, - ASN1_NULL, ASN1_OID, ASN1_PRINTABLE_STRING, ASN1_UTC_TIME, @@ -218,6 +217,24 @@ class ECDSASignature(ASN1_Packet): ASN1F_INTEGER("s", 0)) +#################################### +# Diffie Hellman Exchange Packets # +#################################### +# based on PKCS#3 + +# PKCS#3 sect 9 + +class DHParameter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("p", 0), + ASN1F_INTEGER("g", 0), + ASN1F_optional( + ASN1F_INTEGER("l", 0) # aka. 'privateValueLength' + ), + ) + + #################################### # x25519/x448 packets # #################################### @@ -847,13 +864,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)), ) ) @@ -969,6 +1010,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 +1059,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 +1089,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 +1148,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 +1168,6 @@ class X509_TBSCertList(ASN1_Packet): X509_Extension, explicit_tag=0xa0))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - class ASN1F_X509_CRL(ASN1F_SEQUENCE): def __init__(self, **kargs): @@ -1213,7 +1231,7 @@ class CMS_EncapsulatedContentInfo(ASN1_Packet): ASN1F_optional( _EncapsulatedContent_Field("eContent", None, explicit_tag=0xA0), - ) + ), ) @@ -1241,10 +1259,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 +1307,17 @@ class CMS_SignerInfo(ASN1_Packet): ) +# RFC3852 sect 5.4 + +class CMS_SignedAttrsForSignature(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF( + "signedAttrs", + None, + CMS_Attribute, + ) + + # RFC3852 sect 5.1 class CMS_SignedData(ASN1_Packet): diff --git a/scapy/libs/rfc3961.py b/scapy/libs/rfc3961.py index ed6581ceaff..e07d00c1df9 100644 --- a/scapy/libs/rfc3961.py +++ b/scapy/libs/rfc3961.py @@ -1445,3 +1445,35 @@ def prfplus(key, pepper): ) ), ) + + +############ +# RFC 4556 # +############ + +def octetstring2key(etype: EncryptionType, x: bytes) -> Key: + """ + RFC4556 octetstring2key:: + + octetstring2key(x) == random-to-key(K-truncate( + SHA1(0x00 | x) | + SHA1(0x01 | x) | + SHA1(0x02 | x) | + ... + )) + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + + out = b"" + count = 0 + while len(out) < ep.keysize: + out += Hash_SHA().digest(struct.pack("!B", count) + x) + count += 1 + + return Key.random_to_key( + etype=etype, + seed=out[:ep.keysize], + ) diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py index 87c591753bc..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) @@ -2424,6 +2434,9 @@ def request_tgt( fast=False, armor_with=None, spn=None, + x509=None, + x509key=None, + p12=None, **kwargs, ): """ @@ -2458,6 +2471,9 @@ def request_tgt( armor_ticket_upn=armor_ticket_upn, armor_ticket_skey=armor_ticket_skey, spn=spn, + x509=x509, + x509key=x509key, + p12=p12, **kwargs, ) if not res: @@ -2570,3 +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) 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 0080964f995..b8a44f28169 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, CertList, CMS_Engine + +# Get root CA +ca = Cert(bytes.fromhex('3082036930820251a00302010202106b671318bb858b8e437e4229b0d32f12300d06092a864886f70d01010b0500304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341301e170d3235303932303232313034365a170d3330303932303232323034365a304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d434130820122300d06092a864886f70d01010105000382010f003082010a0282010100d502f47f909c951c87f2e8e6ac1c6f86d555b3311e5ef6086b588fb5eeb66277f63d18f04e65ba07570999bcc7cca3e0fa70914fcfa8acd81d4fbf4bb570a089b1b897cf3e07abc9fa75417bcb7171aaa95e20df12add93fada7df5447210820c1de12e356b248b7fe169019b7cf254c5be50571da26ff4219b8680fa249c14673bf743ef37b46c740353cb88097a099fbc7ca41a79c2cd9bc3a663003edfd12678c88b3970fdc211e38b985d6795d57041de0f3182873670bfee903069f59d3f0ff1634bf57f122ef7d1511775c47fdc574f632c9a1e8af305c81077af542f5499977870d8b0bce0d1fd8088636814d7847e0863ceb0ebe8bb0bd4e47eed01d0203010001a351304f300b0603551d0f040403020186300f0603551d130101ff040530030101ff301d0603551d0e04160414ab14d5ae948281f079726970b3b8f97003aa760c301006092b06010401823715010403020100300d06092a864886f70d01010b05000382010100763c9c93d6f0dd98d6ee5269f1d5f8b83fa14e62a9513806f6f978769208ff65f263f1809743f42b6b70cc77f93f5278e62e4d1da2ae5285e8da155951aa5207cea519d373a202d889e37a9fdde6c79e7a574d2dacd3ea695fde5980d16f91b14cd8f3944cc6a5d3d4c5d95e12f863857fe733285ac04d43fdb0ee52dc8ae5c8d1dd6e32405df2f835bd1681dbf5af9fc523cfe31c31fcde16a07f90733f48cff0392a0a18a1787b91d6b67441d78f507043acfb99c64eebc77717a21cf85ec160411a8f8244f8ef493ad22e5bbdb73d647fc6d911b040d373740b11fa65df5f2a8087ae63f69da5fc14e2e320f6d3e013d319a15762ec6ee2eb3cdf9763a523')) + +# Build CMS engine to verify the authpack +cms = CMS_Engine(CertList([ca])) + +# Verify signature +authpack = cms.verify(signedauthpack, ASN1_OID('id-pkinit-authData')) +assert isinstance(authpack, KRB_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, KRB_AuthPack) + = PKINIT - Parse AS-REP with CMS structures (MIT Kerberos) from scapy.layers.tls.cert import Cert @@ -1357,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/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 diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts index f0a258e4db4..237c4f9aeaa 100644 --- a/test/scapy/layers/tls/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -614,130 +614,53 @@ pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- """) -c0.isIssuerCert(c1) and c1.isIssuerCert(c2) and not c0.isIssuerCert(c2) +assert c0.isIssuerCert(c1) and c1.isIssuerCert(c2) and not c0.isIssuerCert(c2) = Cert class : Checking isSelfSigned() -c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() +assert c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() = PubKey class : Checking verifyCert() -c2.pubKey.verifyCert(c2) and c1.pubKey.verifyCert(c0) +assert c2.pubKey.verifyCert(c2) and c1.pubKey.verifyCert(c0) -= Chain class : Checking chain construction -assert len(Chain([c0, c1, c2])) == 3 -assert len(Chain([c0], c1)) == 2 -len(Chain([c0], c2)) == 1 += CertTree class : Checking verification of chain +chain0 = CertTree([c0, c1, c2]).getchain(c0) +assert len(chain0) == 3 +assert chain0[0] == c1 +assert chain0[1] == c0 +assert chain0[2] == c2 +chain1 = CertTree([c2, c1, c0]).getchain(c1) +assert len(chain1) == 2 +assert chain1[0] == c1 +assert chain1[1] == c2 +chain2 = CertTree([c0, c2, c1]).getchain(c2) +assert len(chain2) == 1 +assert chain2[0] == c2 -= Chain class : repr += CertTree class : show() -expected_repr = """__ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed] - _ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 - _ /OU=Domain Control Validated/CN=*.tools.ietf.org""" -assert str(Chain([c0, c1, c2])) == expected_repr +expected_repr = '/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed]\n /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 [Not Self Signed]\n /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' +assert CertTree([c0, c1, c2]).show(ret=True) == expected_repr -= Chain class : Checking chain verification -assert Chain([], c0).verifyChain([c2], [c1]) -not Chain([c1]).verifyChain([c0]) +repr_str = CertTree([], c0).show(ret=True) +assert repr_str == '/OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' -= Chain class: Checking chain verification with file += CertTree class : verify -import tempfile - -tf_folder = tempfile.mkdtemp() +CertTree([c1, c2]).verify(c0) +CertTree([c2]).verify(c1) try: - os.makedirs(tf_folder) -except: + CertTree([c1]).verify(c0) + assert False +except ValueError: pass -tf = os.path.join(tf_folder, "trusted") -utf = os.path.join(tf_folder, "untrusted") - -tf -utf - -# Create files -trusted = open(tf, "w") -trusted.write(""" ------BEGIN CERTIFICATE----- -MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw -MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk -dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg -Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF -pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE -3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV -Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+ -MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX -v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+ -zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB -BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo -LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo -LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF -BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv -MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN -QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0 -rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO -eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ -sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ -7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7 ------END CERTIFICATE----- -""") -trusted.close() - -untrusted = open(utf, "w") -untrusted.write(""" ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- -""") -untrusted.close() - -assert Chain([], c0).verifyChainFromCAFile(tf, untrusted_file=utf) -assert Chain([], c0).verifyChainFromCAPath(tf_folder, untrusted_file=utf) - -= Clear files - try: - os.remove("./certs_test_ca/trusted") - os.remove("./certs_test_ca/untrusted") -except: + CertTree([c2]).verify(c0) + assert False +except ValueError: pass -try: - os.rmdir("././certs_test_ca") -except: - pass - -= Test __repr__ - -repr_str = Chain([], c0).__repr__() -assert repr_str == '__ /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' = Test GeneralizedTime