LDAP

Scapy fully implements the LDAPv2 / LDAPv3 messages, in addition to a very basic LDAP_Client class.

LDAP client usage

The general idea when using the LDAP_Client class comes down to:

  • instantiating the class

  • calling connect() with the IP (this is where to specify whether to use SSL or not)

  • calling bind() (this is where to specify a SSP if authentication is desired)

  • calling search() to search data.

  • calling modify() to edit data attributes.

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

>>> 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_PartialAttribute ]###
|   |  type      = <ASN1_STRING[b'domainFunctionality']>
|   |  \values    \
|   |   |###[ LDAP_AttributeValue ]###
|   |   |  value     = <ASN1_STRING[b'7']>
|   |###[ LDAP_PartialAttribute ]###
|   |  type      = <ASN1_STRING[b'forestFunctionality']>
|   |  \values    \
|   |   |###[ LDAP_AttributeValue ]###
|   |   |  value     = <ASN1_STRING[b'7']>
|   |###[ LDAP_PartialAttribute ]###
|   |  type      = <ASN1_STRING[b'domainControllerFunctionality']>
|   |  \values    \
|   |   |###[ LDAP_AttributeValue ]###
|   |   |  value     = <ASN1_STRING[b'7']>
[...]

Connecting

Let’s first instantiate the LDAP_Client, and connect to a server over the default port (389):

client = LDAP_Client()
client.connect("192.168.0.100")

It is also possible to use TLS when connecting to the server.

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:

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 LDAP_Client.

Binding

When binding, you must specify a mechanism type. This type comes from the LDAP_BIND_MECHS enumeration, which contains:

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

We’ll now go over “how to bind” using each one of those mechanisms:

NONE (Unauthenticated):

client.bind(LDAP_BIND_MECHS.NONE)

SIMPLE:

client.bind(
    LDAP_BIND_MECHS.SIMPLE,
    simple_username="Administrator",
    simple_password="Password1!",
)

SICILY - NTLM:

ssp = NTLMSSP(UPN="Administrator", PASSWORD="Password1!")
client.bind(
    LDAP_BIND_MECHS.SICILY,
    ssp=ssp,
)

SASL_GSSAPI - Kerberos:

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:

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 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 NTLMSSP (in SICILY or 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:

client.sr1(LDAP_SearchRequest()).show()

We can also use the search() passing a base DN, a filter (as specified by RFC2254) and a scope.\

The scope can be one of the following:

  • 0=baseObject: only the base DN’s attributes are queried

  • 1=singleLevel: the base DN’s children are queried

  • 2=wholeSubtree: the entire subtree under the base DN is included

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:

resp = client.search(
    "CN=Users,DC=domain,DC=local",
    "(objectCategory=person)",
    ["objectClass", "name", "description", "canonicalName"],
    scope=1,  # children
)
resp.show()

To understand exactly what’s going on, note that the previous call is exactly identical to the following:

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)
    )
)

Warning

Our RFC2254 parser currently does not support ‘Extensible Match’.

Modifying attributes

It’s also possible to change some attributes on an object. The following issues a Modify Request that replaces the displayName attribute and adds a servicePrincipalName:

client.modify(
    "CN=User1,CN=Users,DC=domain,DC=local",
    changes=[
        LDAP_ModifyRequestChange(
            operation="replace",
            modification=LDAP_PartialAttribute(
                type="displayName",
                values=[
                    LDAP_AttributeValue(value="Lord User the 1st")
                ]
            )
        ),
        LDAP_ModifyRequestChange(
            operation="add",
            modification=LDAP_PartialAttribute(
                type="servicePrincipalName",
                values=[
                    LDAP_AttributeValue(value="http/lorduser")
                ]
            )
        )
    ]
)