Skip to content

Commit 69dffc1

Browse files
committed
PKINIT: add ASN.1 CMS structures
1 parent 5eb00ba commit 69dffc1

4 files changed

Lines changed: 309 additions & 48 deletions

File tree

scapy/asn1/mib.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ def load_mib(filenames):
267267
"1.3.101.113": "Ed448",
268268
}
269269

270+
# pkcs7 #
271+
272+
pkcs7_oids = {
273+
"1.2.840.113549.1.7.2": "id-signedData",
274+
}
275+
270276
# pkcs9 #
271277

272278
pkcs9_oids = {
@@ -677,6 +683,7 @@ def load_mib(filenames):
677683
pkcs1_oids,
678684
secsig_oids,
679685
thawte_oids,
686+
pkcs7_oids,
680687
pkcs9_oids,
681688
attributeType_oids,
682689
certificateExtension_oids,

scapy/layers/kerberos.py

Lines changed: 131 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
from scapy.layers.inet import TCP, UDP
142142
from scapy.layers.smb import _NV_VERSION
143143
from scapy.layers.smb2 import STATUS_ERREF
144+
from scapy.layers.tls.cert import Cert, PrivKey
144145
from scapy.layers.x509 import X509_AlgorithmIdentifier
145146

146147
# Redirect exports from RFC3961
@@ -2714,30 +2715,50 @@ def select(sockets, remain=None):
27142715

27152716
class KerberosClient(Automaton):
27162717
"""
2718+
Implementation of a Kerberos client.
2719+
2720+
Prefer to use the ``krb_as_req`` and ``krb_tgs_req`` functions which
2721+
wrap this client.
2722+
2723+
Common parameters:
2724+
27172725
:param mode: the mode to use for the client (default: AS_REQ).
27182726
:param ip: the IP of the DC (default: discovered by dclocator)
27192727
:param upn: the UPN of the client.
27202728
:param password: the password of the client.
27212729
:param key: the Key of the client (instead of the password)
27222730
:param realm: the realm of the domain. (default: from the UPN)
2723-
:param spn: the SPN to request in a TGS-REQ
2724-
:param ticket: the existing ticket to use in a TGS-REQ
27252731
:param host: the name of the host doing the request
2726-
:param renew: sets the Renew flag in a TGS-REQ
2727-
:param additional_tickets: in U2U or S4U2Proxy, the additional tickets
2728-
:param u2u: sets the U2U flag
2729-
:param for_user: the UPN of another user in TGS-REQ, to do a S4U2Self
2730-
:param s4u2proxy: sets the S4U2Proxy flag
2731-
:param dmsa: sets the 'unconditional delegation' mode for DMSA TGT retrieval
2732+
:param port: the Kerberos port (default 88)
2733+
:param timeout: timeout of each request (default 5)
2734+
2735+
Advanced common parameters:
2736+
27322737
:param kdc_proxy: specify a KDC proxy url
27332738
:param kdc_proxy_no_check_certificate: do not check the KDC proxy certificate
27342739
:param fast: use FAST armoring
27352740
:param armor_ticket: an external ticket to use for armoring
27362741
:param armor_ticket_upn: the UPN of the client of the armoring ticket
27372742
:param armor_ticket_skey: the session Key object of the armoring ticket
27382743
:param etypes: specify the list of encryption types to support
2739-
:param port: the Kerberos port (default 88)
2740-
:param timeout: timeout of each request (default 5)
2744+
2745+
AS-REQ only:
2746+
2747+
:param x509: a X509 certificate to use for PKINIT AS_REQ or S4U2Proxy
2748+
:param x509key: the private key of the X509 certificate (in an AS_REQ)
2749+
:param p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case,
2750+
'password' is the password of the p12.
2751+
2752+
TGS-REQ only:
2753+
2754+
:param spn: the SPN to request in a TGS-REQ
2755+
:param ticket: the existing ticket to use in a TGS-REQ
2756+
:param renew: sets the Renew flag in a TGS-REQ
2757+
:param additional_tickets: in U2U or S4U2Proxy, the additional tickets
2758+
:param u2u: sets the U2U flag
2759+
:param for_user: the UPN of another user in TGS-REQ, to do a S4U2Self
2760+
:param s4u2proxy: sets the S4U2Proxy flag
2761+
:param dmsa: sets the 'unconditional delegation' mode for DMSA TGT retrieval
27412762
"""
27422763

27432764
RES_AS_MODE = namedtuple("AS_Result", ["asrep", "sessionkey", "kdcrep"])
@@ -2756,6 +2777,9 @@ def __init__(
27562777
password=None,
27572778
key=None,
27582779
realm=None,
2780+
x509=None,
2781+
x509key=None,
2782+
p12=None,
27592783
spn=None,
27602784
ticket=None,
27612785
host=None,
@@ -2796,9 +2820,31 @@ def __init__(
27962820
else:
27972821
raise ValueError("Invalid realm")
27982822

2823+
# PKINIT checks
2824+
if p12 is not None:
2825+
from cryptography.hazmat.primitives.serialization import pkcs12
2826+
2827+
# password should be None or bytes
2828+
if isinstance(password, str):
2829+
password = password.encode()
2830+
2831+
# Read p12/pfx
2832+
with open(p12, "rb") as fd:
2833+
x509key, x509, _ = pkcs12.load_key_and_certificates(
2834+
fd.read(),
2835+
password=password,
2836+
)
2837+
x509 = Cert(cryptography_obj=x509)
2838+
x509key = PrivKey(cryptography_obj=x509key)
2839+
elif x509 and x509key:
2840+
x509 = Cert(x509)
2841+
x509key = PrivKey(x509key)
2842+
27992843
if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]:
28002844
if not host:
28012845
raise ValueError("Invalid host")
2846+
if (x509 is None) ^ (x509key is None):
2847+
raise ValueError("Must provide both 'x509' and 'x509key' !")
28022848
elif mode == self.MODE.TGS_REQ:
28032849
if not ticket:
28042850
raise ValueError("Invalid ticket")
@@ -2815,6 +2861,7 @@ def __init__(
28152861
debug=kwargs.get("debug", 0),
28162862
).ip
28172863

2864+
# Armoring checks
28182865
if fast:
28192866
if mode == self.MODE.AS_REQ:
28202867
# Requires an external ticket
@@ -2867,6 +2914,8 @@ def __init__(
28672914
self.spn = spn
28682915
self.upn = upn
28692916
self.realm = realm.upper()
2917+
self.x509 = x509
2918+
self.x509key = x509key
28702919
self.ticket = ticket
28712920
self.fast = fast
28722921
self.armor_ticket = armor_ticket
@@ -2902,6 +2951,10 @@ def __init__(
29022951
)
29032952

29042953
def _connect(self):
2954+
"""
2955+
Internal function to bind a socket to the DC.
2956+
This also takes care of an eventual KDC proxy.
2957+
"""
29052958
if self.kdc_proxy:
29062959
# If we are using a KDC Proxy, wrap the socket with the KdcProxySocket,
29072960
# that takes our messages and transport them over HTTP.
@@ -2918,9 +2971,15 @@ def _connect(self):
29182971
return sock
29192972

29202973
def send(self, pkt):
2974+
"""
2975+
Sends a wrapped Kerberos packet
2976+
"""
29212977
super(KerberosClient, self).send(KerberosTCPHeader() / pkt)
29222978

29232979
def _base_kdc_req(self, now_time):
2980+
"""
2981+
Return the KRB_KDC_REQ_BODY used in both AS-REQ and TGS-REQ
2982+
"""
29242983
kdcreq = KRB_KDC_REQ_BODY(
29252984
etype=[ASN1_INTEGER(x) for x in self.etypes],
29262985
additionalTickets=None,
@@ -3112,33 +3171,46 @@ def as_req(self):
31123171

31133172
# Pre-auth is requested
31143173
if self.pre_auth:
3115-
if self.fast:
3116-
# Special FAST factor
3117-
# RFC6113 sect 5.4.6
3118-
from scapy.libs.rfc3961 import KRB_FX_CF2
3119-
3120-
# Calculate the 'challenge key'
3121-
ts_key = KRB_FX_CF2(
3122-
self.fast_armorkey,
3123-
self.key,
3124-
b"clientchallengearmor",
3125-
b"challengelongterm",
3126-
)
3174+
if self.x509:
3175+
# Special PKINIT (RFC4556) factor
31273176
pafactor = PADATA(
3128-
padataType=138, # PA-ENCRYPTED-CHALLENGE
3129-
padataValue=EncryptedData(),
3177+
padataType=16, # PA-PK-AS-REQ
3178+
padataValue=PA_PK_AS_REQ(
3179+
3180+
)
31303181
)
31313182
else:
3132-
# Usual 'timestamp' factor
3133-
ts_key = self.key
3134-
pafactor = PADATA(
3135-
padataType=2, # PA-ENC-TIMESTAMP
3136-
padataValue=EncryptedData(),
3183+
# Key-based factor
3184+
3185+
if self.fast:
3186+
# Special FAST factor
3187+
# RFC6113 sect 5.4.6
3188+
from scapy.libs.rfc3961 import KRB_FX_CF2
3189+
3190+
# Calculate the 'challenge key'
3191+
ts_key = KRB_FX_CF2(
3192+
self.fast_armorkey,
3193+
self.key,
3194+
b"clientchallengearmor",
3195+
b"challengelongterm",
3196+
)
3197+
pafactor = PADATA(
3198+
padataType=138, # PA-ENCRYPTED-CHALLENGE
3199+
padataValue=EncryptedData(),
3200+
)
3201+
else:
3202+
# Usual 'timestamp' factor
3203+
ts_key = self.key
3204+
pafactor = PADATA(
3205+
padataType=2, # PA-ENC-TIMESTAMP
3206+
padataValue=EncryptedData(),
3207+
)
3208+
pafactor.padataValue.encrypt(
3209+
ts_key,
3210+
PA_ENC_TS_ENC(patimestamp=ASN1_GENERALIZED_TIME(now_time)),
31373211
)
3138-
pafactor.padataValue.encrypt(
3139-
ts_key,
3140-
PA_ENC_TS_ENC(patimestamp=ASN1_GENERALIZED_TIME(now_time)),
3141-
)
3212+
3213+
# Insert Pre-Authentication data
31423214
padata.insert(
31433215
0,
31443216
pafactor,
@@ -3664,7 +3736,17 @@ def _spn_are_equal(spn1, spn2):
36643736

36653737

36663738
def krb_as_req(
3667-
upn, spn=None, ip=None, key=None, password=None, realm=None, host="WIN10", **kwargs
3739+
upn,
3740+
spn=None,
3741+
ip=None,
3742+
key=None,
3743+
password=None,
3744+
realm=None,
3745+
host="WIN10",
3746+
p12=None,
3747+
x509=None,
3748+
x509key=None,
3749+
**kwargs,
36683750
):
36693751
r"""
36703752
Kerberos AS-Req
@@ -3677,26 +3759,34 @@ def krb_as_req(
36773759
_kerberos._tcp.dc._msdcs.domain.local).
36783760
:param key: (optional) pass the Key object.
36793761
:param password: (optional) otherwise, pass the user's password
3762+
:param x509: (optional) pass a x509 certificate for PKINIT.
3763+
:param x509key: (optional) pass the key of the x509 certificate for PKINIT.
3764+
:param p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case,
3765+
'password' is the password of the p12.
36803766
:param realm: (optional) the realm to use. Otherwise use the one from UPN.
36813767
:param host: (optional) the host performing the AS-Req. WIN10 by default.
36823768
36833769
:return: returns a named tuple (asrep=<...>, sessionkey=<...>)
36843770
36853771
Example::
36863772
3687-
>>> # The KDC is on 192.168.122.17, we ask a TGT for user1
3688-
>>> krb_as_req("user1@DOMAIN.LOCAL", "192.168.122.17", password="Password1")
3773+
>>> # The KDC is found via DC Locator, we ask a TGT for user1
3774+
>>> krb_as_req("user1@DOMAIN.LOCAL", password="Password1")
36893775
36903776
Equivalent::
36913777
36923778
>>> from scapy.libs.rfc3961 import Key, EncryptionType
36933779
>>> key = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=hex_bytes("6d0748c546
36943780
...: f4e99205e78f8da7681d4ec5520ae4815543720c2a647c1ae814c9"))
3695-
>>> krb_as_req("user1@DOMAIN.LOCAL", "192.168.122.17", key=key)
3781+
>>> krb_as_req("user1@DOMAIN.LOCAL", ip="192.168.122.17", key=key)
3782+
3783+
Example using PKINIT with a p12::
3784+
3785+
>>> krb_as_req("user1@DOMAIN.LOCAL", p12="./store.p12", password="password")
36963786
"""
36973787
if realm is None:
36983788
_, realm = _parse_upn(upn)
3699-
if key is None:
3789+
if key is None and p12 is None and x509 is None:
37003790
if password is None:
37013791
try:
37023792
from prompt_toolkit import prompt
@@ -3713,6 +3803,9 @@ def krb_as_req(
37133803
upn=upn,
37143804
password=password,
37153805
key=key,
3806+
p12=p12,
3807+
x509=x509,
3808+
x509key=x509key,
37163809
**kwargs,
37173810
)
37183811
cli.run()

scapy/layers/tls/cert.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ class _PrivKeyFactory(_PKIObjMaker):
451451
It casts the appropriate class on the fly, then fills in
452452
the appropriate attributes with import_from_asn1pkt() submethod.
453453
"""
454-
def __call__(cls, key_path=None):
454+
def __call__(cls, key_path=None, cryptography_obj=None):
455455
"""
456456
key_path may be the path to either:
457457
_an RSAPrivateKey_OpenSSL (as generated by openssl);
@@ -468,7 +468,19 @@ def __call__(cls, key_path=None):
468468
obj.fill_and_store()
469469
return obj
470470

471-
obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE)
471+
# This allows to import cryptography objects directly
472+
if cryptography_obj is not None:
473+
# We (stupidly) need to go through the whole import process because RSA
474+
# does more than just importing the cryptography objects...
475+
obj = _PKIObj("DER", cryptography_obj.private_bytes(
476+
encoding=serialization.Encoding.DER,
477+
format=serialization.PrivateFormat.PKCS8,
478+
encryption_algorithm=serialization.NoEncryption()
479+
))
480+
else:
481+
# Load from file
482+
obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE)
483+
472484
try:
473485
privkey = RSAPrivateKey_OpenSSL(obj._der)
474486
privkey = privkey.privateKey
@@ -725,9 +737,16 @@ class _CertMaker(_PKIObjMaker):
725737
Metaclass for Cert creation. It is not necessary as it was for the keys,
726738
but we reuse the model instead of creating redundant constructors.
727739
"""
728-
def __call__(cls, cert_path):
729-
obj = _PKIObjMaker.__call__(cls, cert_path,
730-
_MAX_CERT_SIZE, "CERTIFICATE")
740+
def __call__(cls, cert_path=None, cryptography_obj=None):
741+
# This allows to import cryptography objects directly
742+
if cryptography_obj is not None:
743+
obj = _PKIObj("DER", cryptography_obj.public_bytes(
744+
encoding=serialization.Encoding.DER,
745+
))
746+
else:
747+
# Load from file
748+
obj = _PKIObjMaker.__call__(cls, cert_path,
749+
_MAX_CERT_SIZE, "CERTIFICATE")
731750
obj.__class__ = Cert
732751
obj.marker = "CERTIFICATE"
733752
try:

0 commit comments

Comments
 (0)