LDAP: add modify/add/delete (#4580)

This commit is contained in:
gpotter2 2024-11-05 09:33:14 +01:00 committed by GitHub
parent 206f1beea0
commit 8e08cbf759
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 186 additions and 30 deletions

View File

@ -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
-----------------
@ -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:
@ -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']>
[...]
@ -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")
]
)
)
]
)

View File

@ -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):
@ -397,7 +397,7 @@ class LDAP_BindResponse(ASN1_Packet):
# 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):
@ -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):
@ -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),
)
@ -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,
)
@ -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(
@ -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
@ -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,
),
@ -966,8 +1064,8 @@ class LDAP(ASN1_Packet):
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
@ -1242,9 +1340,9 @@ class LdapPing_am(AnsweringMachine):
/ CLDAP(
protocolOp=LDAP_SearchResponseEntry(
attributes=[
LDAP_SearchResponseEntryAttribute(
LDAP_PartialAttribute(
values=[
LDAP_SearchResponseEntryAttributeValue(
LDAP_AttributeValue(
value=ASN1_STRING(
val=bytes(
NETLOGON_SAM_LOGON_RESPONSE_EX(
@ -2146,6 +2244,34 @@ class LDAP_Client(object):
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")

View File

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