Skip to content

Commit

Permalink
Windows improvements: LDAP, Kerberos, X509
Browse files Browse the repository at this point in the history
  • Loading branch information
gpotter2 committed Jun 21, 2024
1 parent 8d35918 commit 7dcb5fe
Show file tree
Hide file tree
Showing 18 changed files with 864 additions and 222 deletions.
202 changes: 202 additions & 0 deletions doc/scapy/layers/ldap.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
LDAP
====

Scapy fully implements the LDAPv2 / LDAPv3 messages, in addition to a very basic :class:`~scapy.layers.ldap.LDAP_Client` class.

.. warning::
*The String Representation of LDAP Search Filters* (RFC2254) is currently **unsupported**.
This means that you can't use the commonly known LDAP search syntax, and instead have to use the binary format.
PRs are welcome !

LDAP client usage
-----------------

The general idea when using the :class:`~scapy.layers.ldap.LDAP_Client` class comes down to:

- instantiating the class
- calling :func:`~scapy.layers.ldap.LDAP_Client.connect` with the IP (this is where to specify whether to use SSL or not)
- calling :func:`~scapy.layers.ldap.LDAP_Client.bind` (this is where to specify a SSP if authentication is desired)

The simplest, unauthenticated demo of the client would be something like:

.. code:: pycon
>>> client = LDAP_Client()
>>> client.connect("192.168.0.100")
>>> client.bind(LDAP_BIND_MECHS.NONE)
>>> client.sr1(LDAP_SearchRequest()).show()
┃ Connecting to 192.168.0.100 on port 389...
└ Connected from ('192.168.0.102', 40228)
NONE bind succeeded !
>> LDAP_SearchRequest
<< LDAP_SearchResponseEntry
###[ LDAP ]###
messageID = 0x1 <ASN1_INTEGER[1]>
\protocolOp\
|###[ LDAP_SearchResponseEntry ]###
| objectName= <ASN1_STRING[b'']>
| \attributes\
| |###[ LDAP_SearchResponseEntryAttribute ]###
| | type = <ASN1_STRING[b'domainFunctionality']>
| | \values \
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
| | | value = <ASN1_STRING[b'7']>
| |###[ LDAP_SearchResponseEntryAttribute ]###
| | type = <ASN1_STRING[b'forestFunctionality']>
| | \values \
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
| | | value = <ASN1_STRING[b'7']>
| |###[ LDAP_SearchResponseEntryAttribute ]###
| | type = <ASN1_STRING[b'domainControllerFunctionality']>
| | \values \
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
| | | value = <ASN1_STRING[b'7']>
[...]
Connecting
~~~~~~~~~~

Let's first instantiate the :class:`~scapy.layers.ldap.LDAP_Client`, and connect to a server over the default port (389):

.. code:: python
client = LDAP_Client()
client.connect("192.168.0.100")
It is also possible to use TLS when connecting to the server.

.. code:: python
client = LDAP_Client()
client.connect("192.168.0.100", use_ssl=True)
In that case, the default port is 636. This can be changed using the ``port`` attribute.

.. note::
By default, the server certificate is NOT checked when using this mode, because the server certificate will likely be self-signed.
To actually use TLS securely, you should pass a ``sslcontext`` as shown below:

.. code:: python
import ssl
client = LDAP_Client()
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
sslcontext.load_verify_locations('path/to/ca.crt')
client.connect("192.168.0.100", use_ssl=True, sspcontext=sslcontext)
.. note:: If the client is too verbose, you can pass ``verb=False`` when instantiating :class:`~scapy.layers.ldap.LDAP_Client`.

Binding
~~~~~~~

When binding, you must specify a *mechanism type*. This type comes from the :class:`~scapy.layers.ldap.LDAP_BIND_MECHS` enumeration, which contains:

- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.NONE`: an unauthenticated bind.
- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SIMPLE`: the simple bind mechanism. Credentials are sent **in plaintext**.
- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SICILY`: a `Windows specific authentication mechanism specified in [MS-ADTS] <https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/8b9dbfb2-5b6a-497a-a533-7e709cb9a982>`_ that only supports NTLM.
- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSSAPI`: the SASL authentication mechanism, as specified by `RFC 4422 <https://datatracker.ietf.org/doc/html/rfc4422>`_.
- :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSS_SPNEGO`: the SPNEGO authentication mechanism, another `Windows specific authentication mechanism specified in [MS-SPNG] <https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-spng/f377a379-c24f-4a0f-a3eb-0d835389e28a>`_.

Depending on the server that you are talking to, some of those mechanisms might not be available. This is most notably the case of :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SICILY` and :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSS_SPNEGO` which are mostly Windows-specific.

We'll now go over "how to bind" using each one of those mechanisms:

**NONE (Unauthenticated):**

.. code:: python
client.bind(LDAP_BIND_MECHS.NONE)
**SIMPLE:**

.. code:: python
client.bind(
LDAP_BIND_MECHS.SIMPLE,
simple_username="Administrator",
simple_password="Password1!",
)
**SICILY - NTLM:**

.. code:: python
ssp = NTLMSSP(UPN="Administrator", PASSWORD="Password1!")
client.bind(
LDAP_BIND_MECHS.SICILY,
ssp=ssp,
)
**SASL_GSSAPI - Kerberos:**

.. code:: python
ssp = KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!",
SPN="ldap/dc1.domain.local")
client.bind(
LDAP_BIND_MECHS.SASL_GSSAPI,
ssp=ssp,
)
**SASL_GSS_SPNEGO - NTLM / Kerberos:**

.. code:: python
ssp = SPNEGOSSP([
NTLMSSP(UPN="Administrator", PASSWORD="Password1!"),
KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!",
SPN="ldap/dc1.domain.local"),
])
client.bind(
LDAP_BIND_MECHS.SASL_GSS_SPNEGO,
ssp=ssp,
)
Signing / Encryption
~~~~~~~~~~~~~~~~~~~~

Additionally, it is possible to enable signing or encryption of the LDAP data, when LDAPS is NOT in use.
This is done by setting ``sign`` and ``encrypt`` parameters of the :func:`~scapy.layers.ldap.LDAP_Client.bind` function.

There are however a few caveats to note:

- It's not possible to use those flags in ``NONE`` (duh) or ``SIMPLE`` mode.
- When using the :class:`~scapy.layers.ntlm.NTLMSSP` (in :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SICILY` or :attr:`~scapy.layers.ldap.LDAP_BIND_MECHS.SASL_GSS_SPNEGO` mode), it isn't possible to use ``sign`` without ``encrypt``, because Windows doesn't implement it.

Querying
~~~~~~~~

Once the LDAP connection is bound, it becomes possible to perform requests. For instance, to query all the values of the root DSE:

.. code:: python
client.sr1(LDAP_SearchRequest()).show()
Querying more complicated requests is a bit tedious, as it *currently* requires you to build the Search request yourself.
For instance, this corresponds to querying the DN ``CN=Users,DC=domain,DC=local`` with the filter ``(objectCategory=person)`` and asking for the attributes ``objectClass,name,description,canonicalName``:

.. code:: python
resp = client.sr1(
LDAP_SearchRequest(
filter=LDAP_Filter(
filter=LDAP_FilterEqual(
attributeType=ASN1_STRING(b'objectCategory'),
attributeValue=ASN1_STRING(b'person')
)
),
attributes=[
LDAP_SearchRequestAttribute(type=ASN1_STRING(b'objectClass')),
LDAP_SearchRequestAttribute(type=ASN1_STRING(b'name')),
LDAP_SearchRequestAttribute(type=ASN1_STRING(b'description')),
LDAP_SearchRequestAttribute(type=ASN1_STRING(b'canonicalName'))
],
baseObject=ASN1_STRING(b'CN=Users,DC=domain,DC=local'),
scope=ASN1_ENUMERATED(1),
derefAliases=ASN1_ENUMERATED(0),
sizeLimit=ASN1_INTEGER(1000),
timeLimit=ASN1_INTEGER(60),
attrsOnly=ASN1_BOOLEAN(0)
)
)
resp.show()
24 changes: 19 additions & 5 deletions scapy/asn1/asn1.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,9 @@ def _fix(self, n=0):
if issubclass(o, ASN1_INTEGER):
return o(int(random.gauss(0, 1000)))
elif issubclass(o, ASN1_IPADDRESS):
z = RandIP()._fix()
return o(z)
elif issubclass(o, ASN1_GENERALIZED_TIME) or issubclass(o, ASN1_UTC_TIME): # noqa: E501
z = GeneralizedTime()._fix()
return o(z)
return o(RandIP()._fix())
elif issubclass(o, ASN1_GENERALIZED_TIME) or issubclass(o, ASN1_UTC_TIME):
return o(GeneralizedTime()._fix())
elif issubclass(o, ASN1_STRING):
z1 = int(random.expovariate(0.05) + 1)
return o("".join(random.choice(self.chars) for _ in range(z1)))
Expand Down Expand Up @@ -712,6 +710,22 @@ class ASN1_UNIVERSAL_STRING(ASN1_STRING):
class ASN1_BMP_STRING(ASN1_STRING):
tag = ASN1_Class_UNIVERSAL.BMP_STRING

def __setattr__(self, name, value):
# type: (str, Any) -> None
if name == "val":
if isinstance(value, str):
value = value.encode("utf-16be")
object.__setattr__(self, name, value)
else:
object.__setattr__(self, name, value)

def __repr__(self):
# type: () -> str
return "<%s[%r]>" % (
self.__dict__.get("name", self.__class__.__name__),
self.val.decode("utf-16be"), # type: ignore
)


class ASN1_SEQUENCE(ASN1_Object[List[Any]]):
tag = ASN1_Class_UNIVERSAL.SEQUENCE
Expand Down
43 changes: 40 additions & 3 deletions scapy/asn1/mib.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ def load_mib(filenames):
# pkcs1 #

pkcs1_oids = {
"1.2.840.113549.1.1": "pkcs1",
"1.2.840.113549.1.1.1": "rsaEncryption",
"1.2.840.113549.1.1.2": "md2WithRSAEncryption",
"1.2.840.113549.1.1.3": "md4WithRSAEncryption",
Expand All @@ -229,12 +230,40 @@ def load_mib(filenames):
# secsig oiw #

secsig_oids = {
"1.3.14.3.2.26": "sha1"
"1.3.14.3.2": "OIWSEC",
"1.3.14.3.2.2": "md4RSA",
"1.3.14.3.2.3": "md5RSA",
"1.3.14.3.2.4": "md4RSA2",
"1.3.14.3.2.6": "desECB",
"1.3.14.3.2.7": "desCBC",
"1.3.14.3.2.8": "desOFB",
"1.3.14.3.2.9": "desCFB",
"1.3.14.3.2.10": "desMAC",
"1.3.14.3.2.11": "rsaSign",
"1.3.14.3.2.12": "dsa",
"1.3.14.3.2.13": "shaDSA",
"1.3.14.3.2.14": "mdc2RSA",
"1.3.14.3.2.15": "shaRSA",
"1.3.14.3.2.16": "dhCommMod",
"1.3.14.3.2.17": "desEDE",
"1.3.14.3.2.18": "sha",
"1.3.14.3.2.19": "mdc2",
"1.3.14.3.2.20": "dsaComm",
"1.3.14.3.2.21": "dsaCommSHA",
"1.3.14.3.2.22": "rsaXchg",
"1.3.14.3.2.23": "keyHashSeal",
"1.3.14.3.2.24": "md2RSASign",
"1.3.14.3.2.25": "md5RSASign",
"1.3.14.3.2.26": "sha1",
"1.3.14.3.2.27": "dsaSHA1",
"1.3.14.3.2.28": "dsaCommSHA1",
"1.3.14.3.2.29": "sha1RSASign",
}

# pkcs9 #

pkcs9_oids = {
"1.2.840.113549.1.9": "pkcs9",
"1.2.840.113549.1.9.0": "modules",
"1.2.840.113549.1.9.1": "emailAddress",
"1.2.840.113549.1.9.2": "unstructuredName",
Expand Down Expand Up @@ -361,7 +390,9 @@ def load_mib(filenames):
"2.5.4.94": "epcInUrn",
"2.5.4.95": "ldapUrl",
"2.5.4.96": "ldapUrl",
"2.5.4.97": "organizationIdentifier"
"2.5.4.97": "organizationIdentifier",
# RFC 4519
"0.9.2342.19200300.100.1.25": "dc",
}

certificateExtension_oids = {
Expand Down Expand Up @@ -430,7 +461,13 @@ def load_mib(filenames):
"2.5.29.66": "id-ce-groupAC",
"2.5.29.67": "id-ce-allowedAttAss",
"2.5.29.68": "id-ce-attributeMappings",
"2.5.29.69": "id-ce-holderNameConstraints"
"2.5.29.69": "id-ce-holderNameConstraints",
# [MS-WCCE]
"1.3.6.1.4.1.311.2.1.14": "CERT_EXTENSIONS",
"1.3.6.1.4.1.311.20.2": "ENROLL_CERTTYPE",
"1.3.6.1.4.1.311.25.1": "NTDS_REPLICATION",
"1.3.6.1.4.1.311.25.2": "NTDS_CA_SECURITY_EXT",
"1.3.6.1.4.1.311.25.2.1": "NTDS_OBJECTSID",
}

certExt_oids = {
Expand Down
19 changes: 19 additions & 0 deletions scapy/asn1fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -968,3 +968,22 @@ def i2repr(self, pkt, x):
pretty_s = ", ".join(self.get_flags(pkt))
return pretty_s + " " + repr(x)
return repr(x)


class ASN1F_STRING_PacketField(ASN1F_STRING):
"""
ASN1F_STRING that holds packets.
"""
holds_packets = 1

def i2m(self, pkt, val):
# type: (ASN1_Packet, Any) -> bytes
if hasattr(val, "ASN1_root"):
val = ASN1_STRING(bytes(val)) # type: ignore
return super(ASN1F_STRING_PacketField, self).i2m(pkt, val)

def any2i(self, pkt, x):
# type: (ASN1_Packet, Any) -> Any
if hasattr(x, "add_underlayer"):
x.add_underlayer(pkt)
return super(ASN1F_STRING_PacketField, self).any2i(pkt, x)
6 changes: 6 additions & 0 deletions scapy/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2728,6 +2728,12 @@ def __init__(self, name, default, enum):
super(LEShortEnumField, self).__init__(name, default, enum, "<H")


class LongEnumField(EnumField[int]):
def __init__(self, name, default, enum):
# type: (str, int, Union[Dict[int, str], List[str]]) -> None
super(LongEnumField, self).__init__(name, default, enum, "Q")


class LELongEnumField(EnumField[int]):
def __init__(self, name, default, enum):
# type: (str, int, Union[Dict[int, str], List[str]]) -> None
Expand Down
9 changes: 5 additions & 4 deletions scapy/layers/dcerpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2697,10 +2697,11 @@ def in_pkt(self, pkt):
and body
):
# This is a request/response
self.ssp.GSS_Passive_set_Direction(
self.sspcontext,
IsAcceptor=DceRpc5Response in pkt,
)
if self.sspcontext.passive:
self.ssp.GSS_Passive_set_Direction(
self.sspcontext,
IsAcceptor=DceRpc5Response in pkt,
)
if pkt.auth_verifier and pkt.auth_verifier.is_protected() and body:
if self.sspcontext is None:
return pkt
Expand Down
Loading

0 comments on commit 7dcb5fe

Please sign in to comment.