diff --git a/doc/scapy/layers/ldap.rst b/doc/scapy/layers/ldap.rst index 73de23432..89c4adc62 100644 --- a/doc/scapy/layers/ldap.rst +++ b/doc/scapy/layers/ldap.rst @@ -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= | \attributes\ - | |###[ LDAP_SearchResponseEntryAttribute ]### + | |###[ LDAP_PartialAttribute ]### | | type = | | \values \ - | | |###[ LDAP_SearchResponseEntryAttributeValue ]### + | | |###[ LDAP_AttributeValue ]### | | | value = - | |###[ LDAP_SearchResponseEntryAttribute ]### + | |###[ LDAP_PartialAttribute ]### | | type = | | \values \ - | | |###[ LDAP_SearchResponseEntryAttributeValue ]### + | | |###[ LDAP_AttributeValue ]### | | | value = - | |###[ LDAP_SearchResponseEntryAttribute ]### + | |###[ LDAP_PartialAttribute ]### | | type = | | \values \ - | | |###[ LDAP_SearchResponseEntryAttributeValue ]### + | | |###[ LDAP_AttributeValue ]### | | | value = [...] @@ -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") + ] + ) + ) + ] + ) \ No newline at end of file diff --git a/scapy/layers/ldap.py b/scapy/layers/ldap.py index 803dc8165..3ad956e70 100644 --- a/scapy/layers/ldap.py +++ b/scapy/layers/ldap.py @@ -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") diff --git a/test/scapy/layers/ldap.uts b/test/scapy/layers/ldap.uts index 86e452e94..a4d1892e9 100644 --- a/test/scapy/layers/ldap.uts +++ b/test/scapy/layers/ldap.uts @@ -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))