Skip to content

Commit

Permalink
LDAP: add modify/add/delete (#4580)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpotter2 authored Nov 5, 2024
1 parent 206f1be commit 8e08cbf
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 30 deletions.
48 changes: 39 additions & 9 deletions doc/scapy/layers/ldap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ LDAP

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

.. warning::
Scapy's LDAP client is currently read-only. PRs are welcome !


LDAP client usage
-----------------
Expand All @@ -16,6 +13,7 @@ The general idea when using the :class:`~scapy.layers.ldap.LDAP_Client` class co
- 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)
- calling :func:`~scapy.layers.ldap.LDAP_Client.search` to search data.
- calling :func:`~scapy.layers.ldap.LDAP_Client.modify` to edit data attributes.

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

Expand All @@ -36,20 +34,20 @@ The simplest, unauthenticated demo of the client would be something like:
|###[ LDAP_SearchResponseEntry ]###
| objectName= <ASN1_STRING[b'']>
| \attributes\
| |###[ LDAP_SearchResponseEntryAttribute ]###
| |###[ LDAP_PartialAttribute ]###
| | type = <ASN1_STRING[b'domainFunctionality']>
| | \values \
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
| | |###[ LDAP_AttributeValue ]###
| | | value = <ASN1_STRING[b'7']>
| |###[ LDAP_SearchResponseEntryAttribute ]###
| |###[ LDAP_PartialAttribute ]###
| | type = <ASN1_STRING[b'forestFunctionality']>
| | \values \
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
| | |###[ LDAP_AttributeValue ]###
| | | value = <ASN1_STRING[b'7']>
| |###[ LDAP_SearchResponseEntryAttribute ]###
| |###[ LDAP_PartialAttribute ]###
| | type = <ASN1_STRING[b'domainControllerFunctionality']>
| | \values \
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
| | |###[ LDAP_AttributeValue ]###
| | | value = <ASN1_STRING[b'7']>
[...]
Expand Down Expand Up @@ -222,3 +220,35 @@ To understand exactly what's going on, note that the previous call is exactly id
.. 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``:

.. code:: python
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")
]
)
)
]
)
166 changes: 146 additions & 20 deletions scapy/layers/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class ASN1_Class_LDAP(ASN1_Class):


# Bind operation
# https://datatracker.ietf.org/doc/html/rfc1777#section-4.1
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.2


class ASN1_Class_LDAP_Authentication(ASN1_Class):
Expand Down Expand Up @@ -397,7 +397,7 @@ def serverSaslCredsData(self):


# Unbind operation
# https://datatracker.ietf.org/doc/html/rfc1777#section-4.2
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.3


class LDAP_UnbindRequest(ASN1_Packet):
Expand All @@ -409,7 +409,7 @@ class LDAP_UnbindRequest(ASN1_Packet):


# Search operation
# https://datatracker.ietf.org/doc/html/rfc1777#section-4.3
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.5


class LDAP_SubstringFilterInitial(ASN1_Packet):
Expand Down Expand Up @@ -759,16 +759,16 @@ class LDAP_SearchRequest(ASN1_Packet):
)


class LDAP_SearchResponseEntryAttributeValue(ASN1_Packet):
class LDAP_AttributeValue(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = AttributeValue("value", "")


class LDAP_SearchResponseEntryAttribute(ASN1_Packet):
class LDAP_PartialAttribute(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
AttributeType("type", ""),
ASN1F_SET_OF("values", [], LDAP_SearchResponseEntryAttributeValue),
ASN1F_SET_OF("values", [], LDAP_AttributeValue),
)


Expand All @@ -778,8 +778,8 @@ class LDAP_SearchResponseEntry(ASN1_Packet):
LDAPDN("objectName", ""),
ASN1F_SEQUENCE_OF(
"attributes",
LDAP_SearchResponseEntryAttribute(),
LDAP_SearchResponseEntryAttribute,
LDAP_PartialAttribute(),
LDAP_PartialAttribute,
),
implicit_tag=ASN1_Class_LDAP.SearchResultEntry,
)
Expand All @@ -793,14 +793,6 @@ class LDAP_SearchResponseResultDone(ASN1_Packet):
)


class LDAP_AbandonRequest(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
ASN1F_INTEGER("messageID", 0),
implicit_tag=ASN1_Class_LDAP.AbandonRequest,
)


class LDAP_SearchResponseReference(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE_OF(
Expand All @@ -811,6 +803,106 @@ class LDAP_SearchResponseReference(ASN1_Packet):
)


# Modify Operation
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.6


class LDAP_ModifyRequestChange(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
ASN1F_ENUMERATED(
"operation",
0,
{
0: "add",
1: "delete",
2: "replace",
},
),
ASN1F_PACKET("modification", LDAP_PartialAttribute(), LDAP_PartialAttribute),
)


class LDAP_ModifyRequest(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
LDAPDN("object", ""),
ASN1F_SEQUENCE_OF("changes", [], LDAP_ModifyRequestChange),
implicit_tag=ASN1_Class_LDAP.ModifyRequest,
)


class LDAP_ModifyResponse(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
*LDAPResult,
implicit_tag=ASN1_Class_LDAP.ModifyResponse,
)


# Add Operation
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.7


class LDAP_Attribute(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = LDAP_PartialAttribute.ASN1_root


class LDAP_AddRequest(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
LDAPDN("entry", ""),
ASN1F_SEQUENCE_OF(
"attributes",
LDAP_Attribute(),
LDAP_Attribute,
),
implicit_tag=ASN1_Class_LDAP.AddRequest,
)


class LDAP_AddResponse(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
*LDAPResult,
implicit_tag=ASN1_Class_LDAP.AddResponse,
)


# Delete Operation
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.8


class LDAP_DelRequest(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = LDAPDN(
"entry",
"",
implicit_tag=ASN1_Class_LDAP.DelRequest,
)


class LDAP_DelResponse(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
*LDAPResult,
implicit_tag=ASN1_Class_LDAP.DelResponse,
)


# Abandon Operation
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.11


class LDAP_AbandonRequest(ASN1_Packet):
ASN1_codec = ASN1_Codecs.BER
ASN1_root = ASN1F_SEQUENCE(
ASN1F_INTEGER("messageID", 0),
implicit_tag=ASN1_Class_LDAP.AbandonRequest,
)


# LDAP v3

# RFC 4511 sect 4.12 - Extended Operation
Expand Down Expand Up @@ -926,6 +1018,12 @@ class LDAP(ASN1_Packet):
LDAP_SearchResponseResultDone,
LDAP_AbandonRequest,
LDAP_SearchResponseReference,
LDAP_ModifyRequest,
LDAP_ModifyResponse,
LDAP_AddRequest,
LDAP_AddResponse,
LDAP_DelRequest,
LDAP_DelResponse,
LDAP_UnbindRequest,
LDAP_ExtendedResponse,
),
Expand Down Expand Up @@ -966,8 +1064,8 @@ def tcp_reassemble(cls, data, *args, **kwargs):
pkt = cls(data)
# Packet can be a whole response yet still miss some content.
if (
LDAP_SearchResponseEntry in pkt and
LDAP_SearchResponseResultDone not in pkt
LDAP_SearchResponseEntry in pkt
and LDAP_SearchResponseResultDone not in pkt
):
return None
return pkt
Expand Down Expand Up @@ -1242,9 +1340,9 @@ def make_reply(self, req):
/ CLDAP(
protocolOp=LDAP_SearchResponseEntry(
attributes=[
LDAP_SearchResponseEntryAttribute(
LDAP_PartialAttribute(
values=[
LDAP_SearchResponseEntryAttributeValue(
LDAP_AttributeValue(
value=ASN1_STRING(
val=bytes(
NETLOGON_SAM_LOGON_RESPONSE_EX(
Expand Down Expand Up @@ -2146,6 +2244,34 @@ def _ssafe(x):
break
return entries

def modify(
self,
object: str,
changes: List[LDAP_ModifyRequestChange],
controls: List[LDAP_Control] = [],
) -> None:
"""
Perform a LDAP modify request.
:returns:
"""
resp = self.sr1(
LDAP_ModifyRequest(
object=object,
changes=changes,
),
controls=controls,
timeout=3,
)
if (
LDAP_ModifyResponse not in resp.protocolOp
or resp.protocolOp.resultCode != 0
):
raise LDAP_Exception(
"LDAP modify failed !",
resp=resp,
)

def close(self):
if self.verb:
print("X Connection closed\n")
Expand Down
2 changes: 1 addition & 1 deletion test/scapy/layers/ldap.uts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ assert raw(pkt[CLDAP]) == b'0k\x02\x01\x01cf\x04\x00\n\x01\x00\n\x01\x00\x02\x01

pkt = Ether(b'RT\x00y\xb1FRT\x00\xbc\xe0=\x08\x00E\x00\x00\xb3\x00\x00@\x00@\x11\xc4T\xc0\xa8z\x03\xc0\xa8z\x91\x01\x85\xf1!\x00\x9fv\x960\x81\x86\x02\x01\x01d\x81\x80\x04\x000|0z\x04\x08netlogon1n\x04l\x17\x00\x00\x00\xbd\x11\x00\x00t\x97x\x1f\x05;\xd7B\x8b\xb2\x8c\xf3\xd9z\x7fj\x02s4\x05howto\x08abartlet\x03net\x00\xc0\x18\x04obed\xc0\x18\x08S4-HOWTO\x00\x04OBED\x00\x00\x17Default-First-Site-Name\x00\xc0I\x05\x00\x00\x00\xff\xff\xff\xff0\x0c\x02\x01\x01e\x07\n\x01\x00\x04\x00\x04\x00')
assert pkt.getlayer(CLDAP, 2)
assert isinstance(pkt.protocolOp[0].attributes[0].values[0], LDAP_SearchResponseEntryAttributeValue)
assert isinstance(pkt.protocolOp[0].attributes[0].values[0], LDAP_AttributeValue)
assert pkt.getlayer(CLDAP, 2).protocolOp.resultCode == 0x0

pkt2 = Ether(raw(pkt))
Expand Down

0 comments on commit 8e08cbf

Please sign in to comment.