diff --git a/.config/ci/install.sh b/.config/ci/install.sh index 716e6892309..61f66837648 100755 --- a/.config/ci/install.sh +++ b/.config/ci/install.sh @@ -37,12 +37,7 @@ then sudo apt-get -qy install can-utils || exit 1 sudo apt-get -qy install linux-modules-extra-$(uname -r) || exit 1 sudo apt-get -qy install samba smbclient - # For OpenLDAP, we need to pre-populate some setup questions - sudo debconf-set-selections <<< 'slapd slapd/password2 password Bonjour1' - sudo debconf-set-selections <<< 'slapd slapd/password1 password Bonjour1' - sudo debconf-set-selections <<< 'slapd slapd/domain string scapy.net' - sudo apt-get -qy install slapd - ldapadd -D "cn=admin,dc=scapy,dc=net" -w Bonjour1 -f $CUR/openldap-testdata.ldif -c + sudo bash $CUR/openldap/install.sh # Make sure libpcap is installed if [ ! -z $SCAPY_USE_LIBPCAP ] then diff --git a/.config/ci/openldap-testdata.ldif b/.config/ci/openldap-testdata.ldif deleted file mode 100644 index 56a429afef2..00000000000 --- a/.config/ci/openldap-testdata.ldif +++ /dev/null @@ -1,146 +0,0 @@ -# SPDX-License-Identifier: OLDAP-2.8 -# This file is https://git.openldap.org/openldap/openldap/-/blob/master/tests/data/ppolicy.ldif?ref_type=heads -# (renamed to dc=scapy, dc=net) - -dn: dc=scapy, dc=net -objectClass: top -objectClass: organization -objectClass: dcObject -o: Scapy -dc: scapy - -dn: ou=People, dc=scapy, dc=net -objectClass: top -objectClass: organizationalUnit -ou: People - -dn: ou=Groups, dc=scapy, dc=net -objectClass: organizationalUnit -ou: Groups - -dn: cn=Policy Group, ou=Groups, dc=scapy, dc=net -objectClass: groupOfNames -cn: Policy Group -member: uid=nd, ou=People, dc=scapy, dc=net -owner: uid=ndadmin, ou=People, dc=scapy, dc=net - -dn: cn=Test Group, ou=Groups, dc=scapy, dc=net -objectClass: groupOfNames -cn: Policy Group -member: uid=another, ou=People, dc=scapy, dc=net - -dn: ou=Policies, dc=scapy, dc=net -objectClass: top -objectClass: organizationalUnit -ou: Policies - -dn: cn=Standard Policy, ou=Policies, dc=scapy, dc=net -objectClass: top -objectClass: device -objectClass: pwdPolicy -cn: Standard Policy -pwdAttribute: 2.5.4.35 -pwdLockoutDuration: 15 -pwdInHistory: 6 -pwdCheckQuality: 2 -pwdExpireWarning: 10 -pwdMaxAge: 30 -pwdMinLength: 5 -pwdMaxLength: 13 -pwdGraceAuthnLimit: 3 -pwdAllowUserChange: TRUE -pwdMustChange: TRUE -pwdMaxFailure: 3 -pwdFailureCountInterval: 120 -pwdSafeModify: TRUE -pwdLockout: TRUE - -dn: cn=Idle Expiration Policy, ou=Policies, dc=scapy, dc=net -objectClass: top -objectClass: device -objectClass: pwdPolicy -cn: Idle Expiration Policy -pwdAttribute: 2.5.4.35 -pwdLockoutDuration: 15 -pwdInHistory: 6 -pwdCheckQuality: 2 -pwdExpireWarning: 10 -pwdMaxIdle: 15 -pwdMinLength: 5 -pwdMaxLength: 13 -pwdGraceAuthnLimit: 3 -pwdAllowUserChange: TRUE -pwdMustChange: TRUE -pwdMaxFailure: 3 -pwdFailureCountInterval: 120 -pwdSafeModify: TRUE -pwdLockout: TRUE - -dn: cn=Stricter Policy, ou=Policies, dc=scapy, dc=net -objectClass: top -objectClass: device -objectClass: pwdPolicy -cn: Stricter Policy -pwdAttribute: 2.5.4.35 -pwdLockoutDuration: 15 -pwdInHistory: 6 -pwdCheckQuality: 2 -pwdExpireWarning: 10 -pwdMaxAge: 15 -pwdMinLength: 5 -pwdMaxLength: 13 -pwdAllowUserChange: TRUE -pwdMustChange: TRUE -pwdMaxFailure: 3 -pwdFailureCountInterval: 120 -pwdSafeModify: TRUE -pwdLockout: TRUE - -dn: cn=Another Policy, ou=Policies, dc=scapy, dc=net -objectClass: top -objectClass: device -objectClass: pwdPolicy -cn: Test Policy -pwdAttribute: 2.5.4.35 - -dn: uid=nd, ou=People, dc=scapy, dc=net -objectClass: top -objectClass: person -objectClass: inetOrgPerson -cn: Neil Dunbar -uid: nd -sn: Dunbar -givenName: Neil -userPassword: testpassword - -dn: uid=ndadmin, ou=People, dc=scapy, dc=net -objectClass: top -objectClass: person -objectClass: inetOrgPerson -cn: Neil Dunbar (Admin) -uid: ndadmin -sn: Dunbar -givenName: Neil -userPassword: testpw - -dn: uid=test, ou=People, dc=scapy, dc=net -objectClass: top -objectClass: person -objectClass: inetOrgPerson -cn: test test -uid: test -sn: Test -givenName: Test -userPassword: kfhgkjhfdgkfd -pwdPolicySubEntry: cn=No Policy, ou=Policies, dc=scapy, dc=net - -dn: uid=another, ou=People, dc=scapy, dc=net -objectClass: top -objectClass: person -objectClass: inetOrgPerson -cn: Another Test -uid: another -sn: Test -givenName: Another -userPassword: testing - diff --git a/.config/ci/openldap/config.ldif b/.config/ci/openldap/config.ldif new file mode 100644 index 00000000000..48df480744c --- /dev/null +++ b/.config/ci/openldap/config.ldif @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy + +# Contains the configuration of our OpenLDAP test server + +# Configure LDAPS +dn: cn=config +changetype: modify +add: olcTLSCACertificateFile +olcTLSCACertificateFile: {{CAFILE}} + +dn: cn=config +changetype: modify +replace: olcTLSCertificateKeyFile +olcTLSCertificateKeyFile: {{KEYFILE}} + +dn: cn=config +changetype: modify +replace: olcTLSCertificateFile +olcTLSCertificateFile: {{CRTFILE}} + +dn: cn=config +changetype: modify +add: olcTLSVerifyClient +olcTLSVerifyClient: never + +# Set channel bindings to 'tls-endpoint', like it would be on Windows +dn: cn=config +changetype: modify +replace: olcSaslCbinding +olcSaslCbinding: tls-endpoint diff --git a/.config/ci/openldap/install.sh b/.config/ci/openldap/install.sh new file mode 100755 index 00000000000..cbc8870fc8a --- /dev/null +++ b/.config/ci/openldap/install.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# Install an OpenLDAP test server + +# Pre-populate some setup questions +sudo debconf-set-selections <<< 'slapd slapd/password2 password Bonjour1' +sudo debconf-set-selections <<< 'slapd slapd/password1 password Bonjour1' +sudo debconf-set-selections <<< 'slapd slapd/domain string scapy.net' + +# Run setup +sudo apt-get -qy install slapd + +# Enable LDAPs +echo "Enabling HTTPS on slapd..." +sudo sed -i '/^SLAPD_SERVICES/ c\SLAPD_SERVICES="ldap:/// ldapi:/// ldaps://"' /etc/default/slapd +sudo systemctl restart slapd + +# Calculate the paths we're going to need. +CUR=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PKIPATH=$(realpath "$CUR/../../../test/scapy/layers/tls/pki") +OLDAPPATH=$(mktemp -d -t scapy_openldap_XXXX) + +# Copy certificates to temp path +cp ${PKIPATH}/ca_cert.pem ${OLDAPPATH} +cp ${PKIPATH}/srv_cert.pem ${OLDAPPATH} +cp ${PKIPATH}/srv_key.pem ${OLDAPPATH} +chmod a+rx -R ${OLDAPPATH} + +# Copy config template and replace variables. +echo "Creating OpenLDAP config..." +openldap_conf=${OLDAPPATH}/openldap_config.ldif +cp $CUR/config.ldif $openldap_conf +sed -i "s@{{CAFILE}}@${OLDAPPATH}/ca_cert.pem@g" $openldap_conf +sed -i "s@{{CRTFILE}}@${OLDAPPATH}/srv_cert.pem@g" $openldap_conf +sed -i "s@{{KEYFILE}}@${OLDAPPATH}/srv_key.pem@g" $openldap_conf + +echo "Applying OpenLDAP config..." +sudo ldapmodify -Y EXTERNAL -H "ldapi:///" -w Bonjour1 -f $openldap_conf -c +echo "Adding initial dummy data..." +sudo ldapadd -D "cn=admin,dc=scapy,dc=net" -w Bonjour1 -H "ldapi:///" -f $CUR/testdata.ldif -c diff --git a/.config/ci/openldap/testdata.ldif b/.config/ci/openldap/testdata.ldif new file mode 100644 index 00000000000..63c150b4b64 --- /dev/null +++ b/.config/ci/openldap/testdata.ldif @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: OLDAP-2.8 +# This file is based on https://git.openldap.org/openldap/openldap/-/blob/master/tests/data/ppolicy.ldif?ref_type=heads +# (renamed to dc=scapy, dc=net) + +dn: dc=scapy, dc=net +objectClass: top +objectClass: organization +objectClass: dcObject +o: Scapy +dc: scapy + +dn: ou=People, dc=scapy, dc=net +objectClass: top +objectClass: organizationalUnit +ou: People + +dn: ou=Groups, dc=scapy, dc=net +objectClass: organizationalUnit +ou: Groups + +dn: cn=Policy Group, ou=Groups, dc=scapy, dc=net +objectClass: groupOfNames +cn: Policy Group +member: uid=nd, ou=People, dc=scapy, dc=net +owner: uid=ndadmin, ou=People, dc=scapy, dc=net + +dn: cn=Test Group, ou=Groups, dc=scapy, dc=net +objectClass: groupOfNames +cn: Policy Group +member: uid=another, ou=People, dc=scapy, dc=net + +dn: ou=Policies, dc=scapy, dc=net +objectClass: top +objectClass: organizationalUnit +ou: Policies + +dn: uid=nd, ou=People, dc=scapy, dc=net +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Neil Dunbar +uid: nd +sn: Dunbar +givenName: Neil +userPassword: testpassword + +dn: uid=ndadmin, ou=People, dc=scapy, dc=net +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Neil Dunbar (Admin) +uid: ndadmin +sn: Dunbar +givenName: Neil +userPassword: testpw + +dn: uid=another, ou=People, dc=scapy, dc=net +objectClass: top +objectClass: person +objectClass: inetOrgPerson +cn: Another Test +uid: another +sn: Test +givenName: Another +userPassword: testing + diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index c53af5d317c..a6427f395fe 100644 --- a/doc/scapy/layers/kerberos.rst +++ b/doc/scapy/layers/kerberos.rst @@ -1,21 +1,37 @@ Kerberos ======== -.. note:: Kerberos per `RFC4120 `_ + `RFC6113 `_ (FAST) +.. note:: Kerberos per `RFC4120 `_ + `RFC6113 `_ (FAST) + `[MS-KILE] `_ (Windows) High-Level __________ +Scapy provides several high-level utilities related to Kerberos: + +- ``Ticketer``: a module that allows manipulating Kerberos tickets: + - Request TGT/ST + - Generate a ``KerberosSSP`` from a ST + - Renew tickets + - Read, create, write **ccache** files + - Read, create, write **keytab** files + - Kerberos armoring (via FAST) is available + - S4U2Self / S4U2Proxy are implemented + - KPasswd is implemented +- ``KerberosSSP``: an implementation of a GSSAPI SSP for Kerberos, usable in any of Scapy's client that support GSSAPI. + - Encryption/MIC using GSSAPI is available + - Channel bindings are supported + - U2U (User-To-User) is fully supported + - [MS-KKDCP] (KDC proxy) is supported + Ticketer module ~~~~~~~~~~~~~~~ -Scapy implements a **Ticketer** module, in order to manipulate Kerberos tickets. -Ticketer++ is easy to use programmatically, and allows you to manipulate the tickets yourself. -Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], meaning you can edit ANY field in a ticket to your likings. +The **Ticketer** module can be used both from the CLI or programmatically. This section tries to give many usage examples of features +that are available. For more detail regarding the parameters of the functions, it is encouraged to have a look at their docstrings. -- **Request TGT/ST**: +- **Request TGT**: -.. code:: +.. code:: pycon >>> load_module("ticketer") >>> t = Ticketer() @@ -24,22 +40,15 @@ Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], m >>> t.show() Tickets: 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL - >>> t.request_st(0, "host/dc1.domain.local") - >>> t.show() - Tickets: - 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL Start time End time Renew until Auth time 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 -- **Renew TGT/ST**: Scapy's ticketer can be used to renew TGT or ST. +- **Then request a ST, using the TGT**: -.. code:: +.. code:: pycon - >>> load_module("ticketer") - >>> t = Ticketer() - >>> t.request_tgt("Administrator@DOMAIN.LOCAL") - Enter password: ************ + >>> # The TGT we just got has an ID of 0 >>> t.request_st(0, "host/dc1.domain.local") >>> t.show() Tickets: @@ -50,8 +59,7 @@ Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], m 1. Administrator@DOMAIN.LOCAL -> host/dc1.domain.local@DOMAIN.LOCAL Start time End time Renew until Auth time 31/08/23 11:39:07 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 - >>> t.renew(0) # renew TGT - >>> t.renew(1) # renew ST + - **Use ticket as SSP**: the ``.ssp()`` function. @@ -60,38 +68,14 @@ Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], m >>> # We use ticket 1 from the above store. >>> smbclient("dc1.domain.local", ssp=t.ssp(1)) -- **Perform S4U2Self** - -.. code:: pycon - - >>> load_module("ticketer") - >>> t = Ticketer() - >>> t.request_tgt("SERVER1$@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) - >>> t.request_st(0, "host/SERVER1", for_user="Administrator@domain.local") - >>> t.show() - CCache tickets: - 0. SERVER1$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL - canonicalize+pre-authent+initial+renewable+forwardable - Start time End time Renew until Auth time - 15/04/25 20:15:17 16/04/25 06:10:22 16/04/25 06:10:22 15/04/25 20:15:17 - - 1. Administrator@domain.local -> host/SERVER1@DOMAIN.LOCAL - canonicalize+pre-authent+renewable+forwardable - Start time End time Renew until Auth time - 15/04/25 20:15:20 16/04/25 06:10:22 16/04/25 06:10:22 15/04/25 20:15:17 +- **Renew a TGT or ST**: -- **Change password using kpasswd in 'set' mode:** - -.. code:: pycon +.. code:: - >>> t = Ticketer() - >>> t.request_tgt("Administrator@domain.local") - Enter password: ************ - >>> t.kpasswdset(0, "SERVER1$@domain.local") - INFO: Using 'Set Password' mode. This only works with admin privileges. - Enter NEW password: *********** + >>> t.renew(0) # renew TGT + >>> t.renew(1) # renew ST. Works only with 'host/' SPNs -- **Import tickets** +- **Import tickets from a ccache**: .. note:: We first added a realm ``DOMAIN.LOCAL`` with a kdc to ``/etc/krb5.conf`` @@ -109,7 +93,7 @@ Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], m Start time End time Renew until Auth time 31/08/23 12:08:15 31/08/23 22:08:15 01/09/23 12:08:12 31/08/23 12:08:15 -- **Export tickets** +- **Export tickets into a ccache**: .. code:: pycon @@ -126,6 +110,26 @@ Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], m 08/31/2023 12:08:15 08/31/2023 23:08:15 krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL renew until 09/01/2023 12:08:12 +- **Perform S4U2Self** + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("SERVER1$@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) + >>> t.request_st(0, "host/SERVER1", for_user="Administrator@domain.local") + >>> t.show() + CCache tickets: + 0. SERVER1$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + canonicalize+pre-authent+initial+renewable+forwardable + Start time End time Renew until Auth time + 15/04/25 20:15:17 16/04/25 06:10:22 16/04/25 06:10:22 15/04/25 20:15:17 + + 1. Administrator@domain.local -> host/SERVER1@DOMAIN.LOCAL + canonicalize+pre-authent+renewable+forwardable + Start time End time Renew until Auth time + 15/04/25 20:15:20 16/04/25 06:10:22 16/04/25 06:10:22 15/04/25 20:15:17 + - **Load and use keytab for client** .. code:: pycon @@ -174,6 +178,17 @@ Scapy's ticketer++ implements all fields from RFC4120, [MS-KILE] and [MS-PAC], m No tickets in CCache. +- **Change password using kpasswd in 'set' mode:** + +.. code:: pycon + + >>> t = Ticketer() + >>> t.request_tgt("Administrator@domain.local") + Enter password: ************ + >>> t.kpasswdset(0, "SERVER1$@domain.local") + INFO: Using 'Set Password' mode. This only works with admin privileges. + Enter NEW password: *********** + - **Craft tickets**: We can start by showing how to craft a **golden ticket**: .. code:: pycon diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py index 5843047189b..8ed45368b7e 100644 --- a/scapy/layers/gssapi.py +++ b/scapy/layers/gssapi.py @@ -10,6 +10,7 @@ - GSSAPI: RFC4121 / RFC2743 - GSSAPI C bindings: RFC2744 + - Channel Bindings for TLS: RFC5929 This is implemented in the following SSPs: @@ -26,7 +27,7 @@ import abc from dataclasses import dataclass -from enum import IntEnum, IntFlag +from enum import Enum, IntEnum, IntFlag from scapy.asn1.asn1 import ( ASN1_SEQUENCE, @@ -41,6 +42,7 @@ ASN1F_SEQUENCE, ) from scapy.asn1packet import ASN1_Packet +from scapy.error import log_runtime from scapy.fields import ( FieldLenField, LEIntEnumField, @@ -156,6 +158,7 @@ class _GSSAPI_Field(PacketField): PacketField that contains a GSSAPI_BLOB_SIGNATURE, but one that can have a payload when not encrypted. """ + __slots__ = ["pay_cls"] def __init__(self, name, pay_cls): @@ -174,6 +177,12 @@ def getfield(self, pkt, s): return remain, val +# RFC2744 Annex A, Null values + +GSS_C_QOP_DEFAULT = 0 +GSS_C_NO_CHANNEL_BINDINGS = b"\x00" + + # RFC2744 sect 3.9 - Status Values GSS_S_COMPLETE = 0 @@ -256,6 +265,18 @@ def getfield(self, pkt, s): # GSS Structures +class ChannelBindingType(Enum): + """ + Channel Binding Application Data types, per: + RFC 5929 / RFC 9266 + """ + + TLS_UNIQUE = "unique" + TLS_SERVER_END_POINT = "tls-server-end-point" + TLS_UNIQUE_FOR_TELNET = "tls-unique-for-telnet" + TLS_EXPORTER = "tls-exporter" # RFC9266 + + class GssBufferDesc(Packet): name = "gss_buffer_desc" fields_desc = [ @@ -277,6 +298,72 @@ class GssChannelBindings(Packet): PacketField("application_data", None, GssBufferDesc), ] + def digestMD5(self): + """ + Calculate a MD5 hash of the channel binding + """ + from scapy.layers.tls.crypto.hash import Hash_MD5 + + return Hash_MD5().digest(bytes(self)) + + @classmethod + def fromssl( + cls, + token_type: ChannelBindingType, + sslsock=None, + certfile=None, + ) -> "GssChannelBindings": + """ + Build a GssChannelBindings struct from a socket + + :param token_type: the type from ChannelBindingType, per RFC5929 + :param sslsock: take the certificate from the the socket.socket object + :param certfile: take the certificate from a file + """ + from scapy.layers.tls.cert import Cert + from cryptography.hazmat.primitives import hashes + + if token_type == ChannelBindingType.TLS_SERVER_END_POINT: + # RFC5929 sect 4 + try: + # Parse certificate + if certfile is not None: + cert = Cert(certfile) + else: + cert = Cert(sslsock.getpeercert(binary_form=True)) + except Exception: + # We failed to parse the certificate. + log_runtime.warning("Failed to parse the SSL Certificate. CBT not used") + return GSS_C_NO_CHANNEL_BINDINGS + try: + h = cert.getSignatureHash() + except Exception: + # We failed to get the signature algorithm. + log_runtime.warning( + "Failed to get the Certificate signature algorithm. CBT not used" + ) + return GSS_C_NO_CHANNEL_BINDINGS + # RFC5929 sect 4.1 + if h == hashes.MD5 or h == hashes.SHA1: + h = hashes.SHA256 + # Calc hash of certificate + digest = hashes.Hash(h) + digest.update(bytes(cert.x509Cert)) + cbdata = digest.finalize() + elif token_type == ChannelBindingType.TLS_UNIQUE: + # RFC5929 sect 3 + cbdata = sslsock.get_channel_binding(cb_type="tls-unique") + else: + raise NotImplementedError + # RFC5056 sect 2.1 + # "channel bindings MUST start with the channel binding unique prefix followed + # by a colon (ASCII 0x3A)." + return GssChannelBindings( + application_data=GssBufferDesc( + value=token_type.value.encode() + b":" + cbdata + ) + ) + # --- The base GSSAPI SSP base class @@ -298,6 +385,14 @@ class GSS_C_FLAGS(IntFlag): GSS_C_EXTENDED_ERROR_FLAG = 0x4000 +class GSS_S_FLAGS(IntFlag): + """ + Equivalent to Microsoft's ASC_REQ* Flags in AcceptSecurityContext + """ + + GSS_S_ALLOW_MISSING_BINDINGS = 0x10000000 + + class SSP: """ The general SSP class @@ -319,7 +414,7 @@ class CONTEXT: __slots__ = ["state", "_flags", "passive"] - def __init__(self, req_flags: Optional[GSS_C_FLAGS] = None): + def __init__(self, req_flags: Optional['GSS_C_FLAGS | GSS_S_FLAGS'] = None): if req_flags is None: # Default req_flags = ( @@ -353,7 +448,11 @@ class STATE(IntEnum): @abc.abstractmethod def GSS_Init_sec_context( - self, Context: CONTEXT, val=None, req_flags: Optional[GSS_C_FLAGS] = None + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): """ GSS_Init_sec_context: client-side call for the SSP @@ -361,7 +460,13 @@ def GSS_Init_sec_context( raise NotImplementedError @abc.abstractmethod - def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): """ GSS_Accept_sec_context: server-side call for the SSP """ @@ -370,7 +475,7 @@ def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): # Passive @abc.abstractmethod - def GSS_Passive(self, Context: CONTEXT, val=None): + def GSS_Passive(self, Context: CONTEXT, token=None): """ GSS_Passive: client/server call for the SSP in passive mode """ @@ -392,7 +497,10 @@ class WRAP_MSG: @abc.abstractmethod def GSS_WrapEx( - self, Context: CONTEXT, msgs: List[WRAP_MSG], qop_req: int = 0 + self, + Context: CONTEXT, + msgs: List[WRAP_MSG], + qop_req: int = GSS_C_QOP_DEFAULT, ) -> Tuple[List[WRAP_MSG], Any]: """ GSS_WrapEx @@ -426,7 +534,10 @@ class MIC_MSG: @abc.abstractmethod def GSS_GetMICEx( - self, Context: CONTEXT, msgs: List[MIC_MSG], qop_req: int = 0 + self, + Context: CONTEXT, + msgs: List[MIC_MSG], + qop_req: int = GSS_C_QOP_DEFAULT, ) -> Any: """ GSS_GetMICEx @@ -440,7 +551,12 @@ def GSS_GetMICEx( raise NotImplementedError @abc.abstractmethod - def GSS_VerifyMICEx(self, Context: CONTEXT, msgs: List[MIC_MSG], signature) -> None: + def GSS_VerifyMICEx( + self, + Context: CONTEXT, + msgs: List[MIC_MSG], + signature, + ) -> None: """ :param Context: the SSP context :param msgs: list of VERIF_MSG @@ -464,7 +580,12 @@ def MaximumSignatureLength(self, Context: CONTEXT): # sect 2.3.1 - def GSS_GetMIC(self, Context: CONTEXT, message: bytes, qop_req: int = 0): + def GSS_GetMIC( + self, + Context: CONTEXT, + message: bytes, + qop_req: int = GSS_C_QOP_DEFAULT, + ): return self.GSS_GetMICEx( Context, [ @@ -478,7 +599,12 @@ def GSS_GetMIC(self, Context: CONTEXT, message: bytes, qop_req: int = 0): # sect 2.3.2 - def GSS_VerifyMIC(self, Context: CONTEXT, message: bytes, signature): + def GSS_VerifyMIC( + self, + Context: CONTEXT, + message: bytes, + signature, + ): self.GSS_VerifyMICEx( Context, [ @@ -497,7 +623,7 @@ def GSS_Wrap( Context: CONTEXT, input_message: bytes, conf_req_flag: bool, - qop_req: int = 0, + qop_req: int = GSS_C_QOP_DEFAULT, ): _msgs, signature = self.GSS_WrapEx( Context, diff --git a/scapy/layers/http.py b/scapy/layers/http.py index f8a74e7b2b3..b43d096924f 100644 --- a/scapy/layers/http.py +++ b/scapy/layers/http.py @@ -70,10 +70,14 @@ from scapy.utils import get_temp_file, ContextManagerSubprocess from scapy.layers.gssapi import ( + ChannelBindingType, + GSSAPI_BLOB, + GSS_C_NO_CHANNEL_BINDINGS, GSS_S_COMPLETE, - GSS_S_FAILURE, GSS_S_CONTINUE_NEEDED, - GSSAPI_BLOB, + GSS_S_FAILURE, + GSS_S_FLAGS, + GssChannelBindings, ) from scapy.layers.inet import TCP @@ -758,6 +762,7 @@ class HTTP_Client(object): :param mech: one of HTTP_AUTH_MECHS :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 """ def __init__( @@ -776,6 +781,7 @@ def __init__( self.ssp = ssp self.sspcontext = None self.no_check_certificate = no_check_certificate + self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS def _connect_or_reuse(self, host, port=None, tls=False, timeout=5): # Get the port @@ -817,6 +823,12 @@ 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: + # Compute the channel binding token (CBT) + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + sslsock=sock, + ) self.sock = SSLStreamSocket(sock, HTTP) else: self.sock = StreamSocket(sock, HTTP) @@ -925,6 +937,7 @@ def request( self.sspcontext, ssp_blob, req_flags=0, + chan_bindings=self.chan_bindings, ) if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: raise Scapy_Exception("Authentication failure") @@ -948,7 +961,7 @@ def request( def close(self): if self.verb: - print("X Connection to %s closed\n" % repr(self.sock.ins.getpeername())) + print("X Connection to server closed\n") self.sock.close() @@ -1040,6 +1053,8 @@ def __init__( self.ssp = ssp self.authmethod = mech.value self.sspcontext = None + self.ssp_req_flags = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS + self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS self.basic = False self.BASIC_IDENTITIES = kwargs.pop("BASIC_IDENTITIES", {}) self.BASIC_REALM = kwargs.pop("BASIC_REALM", "default") @@ -1125,7 +1140,10 @@ def AUTH(self, pkt=None): raise self.AUTH_ERROR(proxy) # And call the SSP self.sspcontext, tok, status = self.ssp.GSS_Accept_sec_context( - self.sspcontext, ssp_blob + self.sspcontext, + ssp_blob, + req_flags=self.ssp_req_flags, + chan_bindings=self.chan_bindings, ) else: # This is actually Basic authentication @@ -1141,7 +1159,7 @@ def AUTH(self, pkt=None): tok, status = None, GSS_S_FAILURE # Send answer if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: - self.debug(3, "Authentication failed.") + self.debug(3, "Authentication failed: %s." % status) raise self.AUTH_ERROR(proxy) elif status == GSS_S_CONTINUE_NEEDED: data = self.authmethod.encode() @@ -1252,9 +1270,11 @@ class HTTPS_Server(HTTP_Server): This has the same arguments and attributes as HTTP_Server, with the addition of: :param sslcontext: an optional SSLContext object. - If used, cert and key are ignored. + If used, key is ignored but cert can still be used for + channel bindings. :param cert: path to the certificate :param key: path to the key + :param require_cbt: require Channel Bindings to be valid """ socketcls = None @@ -1267,6 +1287,7 @@ def __init__( key=None, sslcontext=None, ssp=None, + require_cbt=False, *args, **kwargs, ): @@ -1284,6 +1305,8 @@ def __init__( context.wrap_socket(kwargs["sock"], server_side=True), self.pkt_cls, ) + + # Call super super(HTTPS_Server, self).__init__( mech=mech, verb=verb, @@ -1291,3 +1314,13 @@ def __init__( *args, **kwargs, ) + + # Set channel binding + if cert: + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + certfile=cert, + ) + if require_cbt: + # We require CBT by removing GSS_S_ALLOW_MISSING_BINDINGS + self.ssp_req_flags &= ~GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index b4d5d59f767..b3be884d46c 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -124,11 +124,14 @@ from scapy.layers.gssapi import ( GSSAPI_BLOB, GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_BAD_BINDINGS, GSS_S_BAD_MECH, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, GSS_S_DEFECTIVE_TOKEN, GSS_S_FAILURE, + GSS_S_FLAGS, GssChannelBindings, SSP, _GSSAPI_OIDS, @@ -2108,22 +2111,7 @@ class KRB_GSS_EXT(Packet): class KRB_AuthenticatorChecksum(Packet): fields_desc = [ FieldLenField("Lgth", None, length_of="Bnd", fmt="=" in x - else LDAP_FilterApproxMatch - if "~=" in x - else LDAP_FilterEqual + else ( + LDAP_FilterGreaterOrEqual + if ">=" in x + else LDAP_FilterApproxMatch if "~=" in x else LDAP_FilterEqual + ) )( attributeType=ASN1_STRING(x[0].strip()), attributeValue=ASN1_STRING(x[2]), @@ -1718,6 +1723,20 @@ def __init__(self, *args, **kwargs): self.resultCode = kwargs.pop("resultCode", None) self.diagnosticMessage = kwargs.pop("diagnosticMessage", None) super(LDAP_Exception, self).__init__(*args, **kwargs) + # If there's a 'data' string argument, attempt to parse the error code. + try: + m = re.match(r"(\d+): LdapErr.*", self.diagnosticMessage) + if m: + errstr = m.group(1) + err = int(errstr, 16) + if err in STATUS_ERREF: + self.diagnosticMessage = self.diagnosticMessage.replace( + errstr, errstr + " (%s)" % STATUS_ERREF[err], 1 + ) + except ValueError: + pass + # Add note if this exception is raised + self.add_note(self.diagnosticMessage) class LDAP_Client(object): @@ -1789,18 +1808,30 @@ def __init__( self.sign = False # Session status self.sasl_wrap = False + self.chan_bindings = None self.bound = False self.messageID = 0 - def connect(self, ip, port=None, use_ssl=False, sslcontext=None, timeout=5): + def connect( + self, + ip, + port=None, + use_ssl=False, + sslcontext=None, + sni=None, + no_check_certificate=False, + timeout=5, + ): """ Initiate a connection - :param ip: the IP to connect to. + :param ip: the IP or hostname to connect to. :param port: the port to connect to. (Default: 389 or 636) :param use_ssl: whether to use LDAPS or not. (Default: False) :param sslcontext: an optional SSLContext to use. + :param sni: (optional) specify the SNI to use if LDAPS, otherwise use ip. + :param no_check_certificate: with SSL, do not check the certificate """ self.ssl = use_ssl self.sslcontext = sslcontext @@ -1829,17 +1860,26 @@ def connect(self, ip, port=None, use_ssl=False, sslcontext=None, timeout=5): "\u2514 Connected from %s" % repr(sock.getsockname()) ) ) + # For SSL, build and apply SSLContext if self.ssl: if self.sslcontext is None: - context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - # Hm, this is insecure. - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE + if no_check_certificate: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + else: + context = ssl.create_default_context() else: context = self.sslcontext - sock = context.wrap_socket(sock) + sock = context.wrap_socket(sock, server_hostname=sni or ip) + # Wrap the socket in a Scapy socket if self.ssl: self.sock = SSLStreamSocket(sock, LDAP) + # Compute the channel binding token (CBT) + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + sslsock=sock, + ) else: self.sock = StreamSocket(sock, LDAP) @@ -1979,9 +2019,10 @@ def bind( or not isinstance(resp.protocolOp, LDAP_BindResponse) or resp.protocolOp.resultCode != 0 ): - if self.verb: - resp.show() - raise RuntimeError("LDAP simple bind failed !") + raise LDAP_Exception( + "LDAP simple bind failed !", + resp=resp, + ) status = GSS_S_COMPLETE elif self.mech == LDAP_BIND_MECHS.SICILY: # [MS-ADTS] sect 5.1.1.1.3 @@ -1993,8 +2034,10 @@ def bind( ) ) if resp.protocolOp.resultCode != 0: - resp.show() - raise RuntimeError("Sicily package discovery failed !") + raise LDAP_Exception( + "Sicily package discovery failed !", + resp=resp, + ) # 2. First exchange: Negotiate self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, @@ -2016,11 +2059,15 @@ def bind( ) val = resp.protocolOp.serverCreds if not val: - resp.show() - raise RuntimeError("Sicily negotiate failed !") + raise LDAP_Exception( + "Sicily negotiate failed !", + resp=resp, + ) # 3. Second exchange: Response self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( - self.sspcontext, GSSAPI_BLOB(val) + self.sspcontext, + GSSAPI_BLOB(val), + chan_bindings=self.chan_bindings, ) resp = self.sr1( LDAP_BindRequest( @@ -2050,6 +2097,7 @@ def bind( | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.sign else 0) | (GSS_C_FLAGS.GSS_C_CONF_FLAG if self.encrypt else 0) ), + chan_bindings=self.chan_bindings, ) while token: resp = self.sr1( @@ -2071,13 +2119,17 @@ def bind( status = resp.protocolOp.resultCode break self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( - self.sspcontext, GSSAPI_BLOB(val) + self.sspcontext, + GSSAPI_BLOB(val), + chan_bindings=self.chan_bindings, ) else: status = GSS_S_COMPLETE if status != GSS_S_COMPLETE: - resp.show() - raise RuntimeError("%s bind failed !" % self.mech.name) + raise LDAP_Exception( + "%s bind failed !" % self.mech.name, + resp=resp, + ) elif self.mech == LDAP_BIND_MECHS.SASL_GSSAPI: # GSSAPI has 2 extra exchanges # https://datatracker.ietf.org/doc/html/rfc2222#section-7.2.1 @@ -2106,12 +2158,14 @@ def bind( raise RuntimeError("GSSAPI SASL failed to negotiate CONFIDENTIALITY !") # Announce client-supported layers saslOptions = LDAP_SASL_GSSAPI_SsfCap( - supported_security_layers="+".join( - (["INTEGRITY"] if self.sign else []) - + (["CONFIDENTIALITY"] if self.encrypt else []) - ) - if (self.sign or self.encrypt) - else "NONE", + supported_security_layers=( + "+".join( + (["INTEGRITY"] if self.sign else []) + + (["CONFIDENTIALITY"] if self.encrypt else []) + ) + if (self.sign or self.encrypt) + else "NONE" + ), # Same as server max_output_token_size=saslOptions.max_output_token_size, ) diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index 7c3144649f8..493c8918265 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -24,9 +24,11 @@ ) from scapy.layers.gssapi import ( GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, GSS_S_FAILURE, + GSS_S_FLAGS, ) from scapy.layers.ntlm import RC4, RC4K, RC4Init, SSP @@ -473,7 +475,7 @@ def GSS_VerifyMICEx(self, Context, msgs, signature): self._unsecure(Context, msgs, signature, False) def GSS_Init_sec_context( - self, Context, val=None, req_flags: Optional[GSS_C_FLAGS] = None + self, Context, token=None, req_flags: Optional[GSS_C_FLAGS] = None ): if Context is None: Context = self.CONTEXT(True, req_flags=req_flags, AES=self.AES) @@ -493,9 +495,15 @@ def GSS_Init_sec_context( else: return Context, None, GSS_S_COMPLETE - def GSS_Accept_sec_context(self, Context, val=None): + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, + ): if Context is None: - Context = self.CONTEXT(False, req_flags=0, AES=self.AES) + Context = self.CONTEXT(False, req_flags=req_flags, AES=self.AES) if Context.state == self.STATE.INIT: Context.state = self.STATE.SRV_SENT_NL diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index 5a62498036d..c6cb7f133cd 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -347,7 +347,8 @@ def _bind(self, interface, reqcls, respcls): else: # Call the underlying SSP self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( - self.sspcontext, val=resp.auth_verifier.auth_value + self.sspcontext, + token=resp.auth_verifier.auth_value, ) if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: # Authentication should continue @@ -388,7 +389,8 @@ def _bind(self, interface, reqcls, respcls): status = GSS_S_COMPLETE break self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( - self.sspcontext, val=resp.auth_verifier.auth_value + self.sspcontext, + token=resp.auth_verifier.auth_value, ) # Check context acceptance if ( diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 301026383e0..c27d0566b95 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -61,10 +61,14 @@ from scapy.layers.gssapi import ( GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_BAD_BINDINGS, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, GSS_S_DEFECTIVE_CREDENTIAL, GSS_S_DEFECTIVE_TOKEN, + GSS_S_FLAGS, + GssChannelBindings, SSP, _GSSAPI_OIDS, _GSSAPI_SIGNATURE_OIDS, @@ -1129,7 +1133,7 @@ def SEALKEY(NegFlg, ExportedSessionKey, Mode): raise ValueError("Unknown Mode") elif NegFlg.NEGOTIATE_LM_KEY: if NegFlg.NEGOTIATE_56: - return ExportedSessionKey[:6] + b"\xA0" + return ExportedSessionKey[:6] + b"\xa0" else: return ExportedSessionKey[:4] + b"\xe5\x38\xb0" else: @@ -1368,7 +1372,11 @@ def verifyMechListMIC(self, Context, otherMIC, input): Context.RecvSealHandle = OriginalHandle def GSS_Init_sec_context( - self, Context: CONTEXT, val=None, req_flags: Optional[GSS_C_FLAGS] = None + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): if Context is None: Context = self.CONTEXT(False, req_flags=req_flags) @@ -1432,8 +1440,8 @@ def GSS_Init_sec_context( Context.state = self.STATE.CLI_SENT_NEGO return Context, tok, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.CLI_SENT_NEGO: - # Client: auth (val=challenge) - chall_tok = val + # Client: auth (token=challenge) + chall_tok = token if self.UPN is None or self.HASHNT is None: raise ValueError( "Must provide a 'UPN' and a 'HASHNT' or 'PASSWORD' when " @@ -1494,7 +1502,20 @@ def GSS_Init_sec_context( AvId="MsvAvSingleHost", Value=Single_Host_Data(MachineID=os.urandom(32)), ), - AV_PAIR(AvId="MsvAvChannelBindings", Value=b"\x00" * 16), + ] + + ( + [ + AV_PAIR( + # [MS-NLMP] sect 2.2.2.1 refers to RFC 4121 sect 4.1.1.2 + # "The Bnd field contains the MD5 hash of channel bindings" + AvId="MsvAvChannelBindings", + Value=chan_bindings.digestMD5(), + ), + ] + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else [] + ) + + [ AV_PAIR(AvId="MsvAvTargetName", Value="host/" + tok.Workstation), AV_PAIR(AvId="MsvAvEOL"), ] @@ -1555,7 +1576,7 @@ def GSS_Init_sec_context( Context.state = self.STATE.CLI_SENT_AUTH return Context, tok, GSS_S_COMPLETE elif Context.state == self.STATE.CLI_SENT_AUTH: - if val: + if token: # what is that? status = GSS_S_DEFECTIVE_CREDENTIAL else: @@ -1564,13 +1585,19 @@ def GSS_Init_sec_context( else: raise ValueError("NTLMSSP: unexpected state %s" % repr(Context.state)) - def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): if Context is None: - Context = self.CONTEXT(IsAcceptor=True, req_flags=0) + Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) if Context.state == self.STATE.INIT: - # Server: challenge (val=negotiate) - nego_tok = val + # Server: challenge (token=negotiate) + nego_tok = token if not nego_tok or NTLM_NEGOTIATE not in nego_tok: log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Negotiate") return Context, None, GSS_S_DEFECTIVE_TOKEN @@ -1659,16 +1686,20 @@ def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): Context.state = self.STATE.SRV_SENT_CHAL return Context, tok, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.SRV_SENT_CHAL: - # server: OK or challenge again (val=auth) - auth_tok = val + # server: OK or challenge again (token=auth) + auth_tok = token + if not auth_tok or NTLM_AUTHENTICATE_V2 not in auth_tok: log_runtime.debug( "NTLMSSP: Unexpected token. Expected NTLM Authenticate v2" ) return Context, None, GSS_S_DEFECTIVE_TOKEN + if self.DO_NOT_CHECK_LOGIN: # Just trust me bro return Context, None, GSS_S_COMPLETE + + # Compute the session key SessionBaseKey = self._getSessionBaseKey(Context, auth_tok) if SessionBaseKey: # [MS-NLMP] sect 3.2.5.1.2 @@ -1686,6 +1717,19 @@ def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): Context.ExportedSessionKey = ExportedSessionKey # [MS-SMB] 3.2.5.3 Context.SessionKey = Context.ExportedSessionKey + + # Check the channel bindings + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + try: + Bnd = auth_tok.NtChallengeResponse.getAv(0x000A).Value + if Bnd != chan_bindings.digestMD5(): + # Bad channel bindings + return Context, None, GSS_S_BAD_BINDINGS + except IndexError: + if GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS not in req_flags: + # Uhoh, we required channel bindings + return Context, None, GSS_S_BAD_BINDINGS + # Check the NTProofStr if Context.SessionKey: # Compute NTLM keys @@ -1710,6 +1754,7 @@ def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): if auth_tok.NegotiateFlags.NEGOTIATE_SEAL: Context.flags |= GSS_C_FLAGS.GSS_C_CONF_FLAG return Context, None, GSS_S_COMPLETE + # Bad NTProofStr or unknown user Context.SessionKey = None Context.state = self.STATE.INIT @@ -1726,7 +1771,7 @@ def MaximumSignatureLength(self, Context: CONTEXT): """ return 16 # len(NTLMSSP_MESSAGE_SIGNATURE()) - def GSS_Passive(self, Context: CONTEXT, val=None): + def GSS_Passive(self, Context: CONTEXT, token=None): if Context is None: Context = self.CONTEXT(True) Context.passive = True @@ -1735,24 +1780,24 @@ def GSS_Passive(self, Context: CONTEXT, val=None): # and discard the output. if Context.state == self.STATE.INIT: - if not val or NTLM_NEGOTIATE not in val: + if not token or NTLM_NEGOTIATE not in token: log_runtime.warning("NTLMSSP: Expected NTLM Negotiate") return None, GSS_S_DEFECTIVE_TOKEN - Context.neg_tok = val + Context.neg_tok = token Context.state = self.STATE.CLI_SENT_NEGO return Context, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.CLI_SENT_NEGO: - if not val or NTLM_CHALLENGE not in val: + if not token or NTLM_CHALLENGE not in token: log_runtime.warning("NTLMSSP: Expected NTLM Challenge") return None, GSS_S_DEFECTIVE_TOKEN - Context.chall_tok = val + Context.chall_tok = token Context.state = self.STATE.SRV_SENT_CHAL return Context, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.SRV_SENT_CHAL: - if not val or NTLM_AUTHENTICATE_V2 not in val: + if not token or NTLM_AUTHENTICATE_V2 not in token: log_runtime.warning("NTLMSSP: Expected NTLM Authenticate") return None, GSS_S_DEFECTIVE_TOKEN - Context, _, status = self.GSS_Accept_sec_context(Context, val) + Context, _, status = self.GSS_Accept_sec_context(Context, token) if status != GSS_S_COMPLETE: log_runtime.info("NTLMSSP: auth failed.") Context.state = self.STATE.INIT diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py index d7c18f07a16..230b3be4d8b 100644 --- a/scapy/layers/smb2.py +++ b/scapy/layers/smb2.py @@ -101,9 +101,12 @@ 0x80000005: "STATUS_BUFFER_OVERFLOW", 0x80000006: "STATUS_NO_MORE_FILES", 0x8000002D: "STATUS_STOPPED_ON_SYMLINK", + 0x80090308: "SEC_E_INVALID_TOKEN", 0x8009030C: "SEC_E_LOGON_DENIED", 0x8009030F: "SEC_E_MESSAGE_ALTERED", 0x80090310: "SEC_E_OUT_OF_SEQUENCE", + 0x80090346: "SEC_E_BAD_BINDINGS", + 0x80090351: "SEC_E_SMARTCARD_CERT_REVOKED", 0xC0000003: "STATUS_INVALID_INFO_CLASS", 0xC0000004: "STATUS_INFO_LENGTH_MISMATCH", 0xC000000D: "STATUS_INVALID_PARAMETER", diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 5746d538186..b0532491abe 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -456,7 +456,7 @@ def NEGOTIATED(self, ssp_blob=None): # Begin session establishment ssp_tuple = self.session.ssp.GSS_Init_sec_context( self.session.sspcontext, - ssp_blob, + token=ssp_blob, req_flags=( GSS_C_FLAGS.GSS_C_MUTUAL_FLAG | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.session.SigningRequired else 0) @@ -615,7 +615,8 @@ def AUTH_FAILED(self): @ATMT.state() def AUTHENTICATED(self, ssp_blob=None): self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context( - self.session.sspcontext, ssp_blob + self.session.sspcontext, + token=ssp_blob, ) if status != GSS_S_COMPLETE: raise ValueError("Internal error: the SSP completed with an error.") diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index cfaeeaed75f..f962b2fb1c8 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -61,9 +61,12 @@ GSSAPI_BLOB, GSSAPI_BLOB_SIGNATURE, GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, GSS_S_BAD_MECH, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, + GSS_S_FLAGS, + GssChannelBindings, SSP, _GSSAPI_OIDS, _GSSAPI_SIGNATURE_OIDS, @@ -710,7 +713,17 @@ def GSS_VerifyMICEx(self, Context, *args, **kwargs): def LegsAmount(self, Context: CONTEXT): return 4 - def _common_spnego_handler(self, Context, IsClient, val=None, req_flags=None): + def _common_spnego_handler( + self, + Context, + IsClient, + token=None, + req_flags=None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + """ + Common code shared across both GSS_sec_Init_Context and GSS_sec_Accept_Context + """ if Context is None: # New Context Context = SPNEGOSSP.CONTEXT( @@ -723,8 +736,8 @@ def _common_spnego_handler(self, Context, IsClient, val=None, req_flags=None): # Extract values from GSSAPI token status, MIC, otherMIC, rawToken = 0, None, None, False - if val: - val, status, otherMIC, rawToken = self._extract_gssapi(Context, val) + if token: + token, status, otherMIC, rawToken = self._extract_gssapi(Context, token) # If we don't have a SSP already negotiated, check for requested and available # SSPs and find a common one. @@ -744,7 +757,7 @@ def _common_spnego_handler(self, Context, IsClient, val=None, req_flags=None): # Check whether the selected SSP was the one preferred by the client if ( Context.negotiated_mechtype != Context.requested_mechtypes[0] - and val + and token ): Context.first_choice = False # No SSPs were requested. Use the first available SSP we know. @@ -772,12 +785,16 @@ def _common_spnego_handler(self, Context, IsClient, val=None, req_flags=None): status, ) = Context.ssp.GSS_Init_sec_context( Context.sub_context, - val=val, + token=token, req_flags=Context.req_flags, + chan_bindings=chan_bindings, ) else: Context.sub_context, tok, status = Context.ssp.GSS_Accept_sec_context( - Context.sub_context, val=val + Context.sub_context, + token=token, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, ) # Check whether client or server says the specified mechanism is not valid if status == GSS_S_BAD_MECH: @@ -808,7 +825,13 @@ def _common_spnego_handler(self, Context, IsClient, val=None, req_flags=None): Context.sub_context = None # Reset the SSP context if IsClient: # Call ourselves again for the client to generate a token - return self._common_spnego_handler(Context, True, None) + return self._common_spnego_handler( + Context, + IsClient=True, + token=None, + req_flags=req_flags, + chan_bindings=chan_bindings, + ) else: # Return nothing but the supported SSP list tok, status = None, GSS_S_CONTINUE_NEEDED @@ -919,23 +942,45 @@ def _common_spnego_handler(self, Context, IsClient, val=None, req_flags=None): return Context, spnego_tok, status def GSS_Init_sec_context( - self, Context: CONTEXT, val=None, req_flags: Optional[GSS_C_FLAGS] = None + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): - return self._common_spnego_handler(Context, True, val=val, req_flags=req_flags) + return self._common_spnego_handler( + Context, + True, + token=token, + req_flags=req_flags, + chan_bindings=chan_bindings, + ) - def GSS_Accept_sec_context(self, Context: CONTEXT, val=None): - return self._common_spnego_handler(Context, False, val=val, req_flags=0) + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + return self._common_spnego_handler( + Context, + False, + token=token, + req_flags=req_flags, + chan_bindings=chan_bindings, + ) - def GSS_Passive(self, Context: CONTEXT, val=None): + def GSS_Passive(self, Context: CONTEXT, token=None): if Context is None: # New Context Context = SPNEGOSSP.CONTEXT(self.supported_ssps) Context.passive = True # Extraction - val, status, _, rawToken = self._extract_gssapi(Context, val) + token, status, _, rawToken = self._extract_gssapi(Context, token) - if val is None and status == GSS_S_COMPLETE: + if token is None and status == GSS_S_COMPLETE: return Context, None # Just get the negotiated SSP @@ -961,7 +1006,9 @@ def GSS_Passive(self, Context: CONTEXT, val=None): Context.ssp = ssp # Passthrough - Context.sub_context, status = Context.ssp.GSS_Passive(Context.sub_context, val) + Context.sub_context, status = Context.ssp.GSS_Passive( + Context.sub_context, token + ) return Context, status diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 0317dc521e6..aa3b03febc8 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -768,6 +768,15 @@ 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): + """ + Return the hash used by the 'signatureAlgorithm' + """ + tbsCert = self.tbsCertificate + sigAlg = tbsCert.signature + h = hash_by_oid[sigAlg.algorithm.val] + return _get_hash(h) + def remainingDays(self, now=None): """ Based on the value of notAfter field, returns the number of diff --git a/test/scapy/layers/http.uts b/test/scapy/layers/http.uts index 591c3275098..2d2093b976d 100644 --- a/test/scapy/layers/http.uts +++ b/test/scapy/layers/http.uts @@ -279,20 +279,32 @@ assert gzip.decompress(z) == c.load + Test HTTP client/server = Util function to launch HTTP_server -~ http-client +~ http-client https-client -from scapy.layers.http import HTTP_Server, HTTP_AUTH_MECHS +from scapy.layers.http import ( + HTTP_Server, + HTTPS_Server, + HTTP_AUTH_MECHS, +) class run_httpserver: - def __init__(self, mech=None, ssp=None, **kwargs): + def __init__(self, mech=None, ssp=None, ssl=False, **kwargs): self.server = None self.mech = mech self.ssp = ssp + self.ssl = ssl self.kwargs = kwargs def __enter__(self): - print("@ Starting http server") + if self.ssl: + cls = HTTPS_Server + self.kwargs["cert"] = scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem") + self.kwargs["key"] = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") + print("@ Starting https server") + else: + cls = HTTP_Server + print("@ Starting http server") # Start server - self.server = HTTP_Server.spawn( + self.server = cls.spawn( 8080, iface=conf.loopback_name, mech=self.mech, ssp=self.ssp, @@ -327,7 +339,7 @@ class run_httpserver: print("@ http server stopped !") -= HTTP_client fails to ask HTTP_server that required authentication += HTTP - HTTP_client fails to ask HTTP_server that required authentication ~ http-client from scapy.layers.http import HTTP_Client @@ -339,7 +351,7 @@ with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": M assert resp.Status_Code == b"401" -= HTTP_client asks HTTP_server with NTLMSSP += HTTP - HTTP_client asks HTTP_server with NTLMSSP ~ http-client from scapy.layers.http import HTTP_Client @@ -354,7 +366,7 @@ with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": M assert resp.load == b'

OK

' -= HTTP_Server with native python client with Basic auth += HTTP - HTTP_Server with native python client with Basic auth ~ http-client import urllib.request @@ -373,7 +385,7 @@ with run_httpserver(mech=HTTP_AUTH_MECHS.BASIC, BASIC_IDENTITIES={"user": "passw assert html == "

OK

" -= HTTP_Server with native python client without auth += HTTP - HTTP_Server with native python client without auth ~ http-client import urllib.request @@ -383,3 +395,21 @@ with run_httpserver(mech=HTTP_AUTH_MECHS.NONE): html = f.read().decode('utf-8') assert html == "

OK

" + ++ Test HTTPS client/server + += HTTPS - HTTPS_client asks HTTPS_server with NTLMSSP and CBT +~ https-client + +from scapy.layers.http import HTTP_Client + +with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")}), ssl=True): + client = HTTP_Client( + HTTP_AUTH_MECHS.NTLM, + ssp=NTLMSSP(UPN="user", PASSWORD="password"), + no_check_certificate=True, + ) + resp = client.request("https://127.0.0.1:8080") + client.close() + +assert resp.load == b'

OK

' diff --git a/test/scapy/layers/ldapopenldap.uts b/test/scapy/layers/ldapopenldap.uts index aabc2363841..a25b692d944 100644 --- a/test/scapy/layers/ldapopenldap.uts +++ b/test/scapy/layers/ldapopenldap.uts @@ -30,3 +30,13 @@ assert res == { 'userPassword': ['testing'] } } + += (OpenLDAP) connect to server using SSL +~ disabled + +# We need a version of OpenLDAP that is more recent. Let's wait. + +cli = LDAP_Client() +cli.connect("127.0.0.1", use_ssl=True, no_check_certificate=True) +cli.bind(LDAP_BIND_MECHS.SIMPLE, simple_username="cn=admin,dc=scapy,dc=net", simple_password="Bonjour1") +cli.close()