Skip to content

Commit 8e08cbf

Browse files
authored
LDAP: add modify/add/delete (#4580)
1 parent 206f1be commit 8e08cbf

File tree

3 files changed

+186
-30
lines changed

3 files changed

+186
-30
lines changed

doc/scapy/layers/ldap.rst

+39-9
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ LDAP
33

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

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

107
LDAP client usage
118
-----------------
@@ -16,6 +13,7 @@ The general idea when using the :class:`~scapy.layers.ldap.LDAP_Client` class co
1613
- calling :func:`~scapy.layers.ldap.LDAP_Client.connect` with the IP (this is where to specify whether to use SSL or not)
1714
- calling :func:`~scapy.layers.ldap.LDAP_Client.bind` (this is where to specify a SSP if authentication is desired)
1815
- calling :func:`~scapy.layers.ldap.LDAP_Client.search` to search data.
16+
- calling :func:`~scapy.layers.ldap.LDAP_Client.modify` to edit data attributes.
1917

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

@@ -36,20 +34,20 @@ The simplest, unauthenticated demo of the client would be something like:
3634
|###[ LDAP_SearchResponseEntry ]###
3735
| objectName= <ASN1_STRING[b'']>
3836
| \attributes\
39-
| |###[ LDAP_SearchResponseEntryAttribute ]###
37+
| |###[ LDAP_PartialAttribute ]###
4038
| | type = <ASN1_STRING[b'domainFunctionality']>
4139
| | \values \
42-
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
40+
| | |###[ LDAP_AttributeValue ]###
4341
| | | value = <ASN1_STRING[b'7']>
44-
| |###[ LDAP_SearchResponseEntryAttribute ]###
42+
| |###[ LDAP_PartialAttribute ]###
4543
| | type = <ASN1_STRING[b'forestFunctionality']>
4644
| | \values \
47-
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
45+
| | |###[ LDAP_AttributeValue ]###
4846
| | | value = <ASN1_STRING[b'7']>
49-
| |###[ LDAP_SearchResponseEntryAttribute ]###
47+
| |###[ LDAP_PartialAttribute ]###
5048
| | type = <ASN1_STRING[b'domainControllerFunctionality']>
5149
| | \values \
52-
| | |###[ LDAP_SearchResponseEntryAttributeValue ]###
50+
| | |###[ LDAP_AttributeValue ]###
5351
| | | value = <ASN1_STRING[b'7']>
5452
[...]
5553
@@ -222,3 +220,35 @@ To understand exactly what's going on, note that the previous call is exactly id
222220
223221
.. warning::
224222
Our RFC2254 parser currently does not support 'Extensible Match'.
223+
224+
Modifying attributes
225+
~~~~~~~~~~~~~~~~~~~~
226+
227+
It's also possible to change some attributes on an object.
228+
The following issues a ``Modify Request`` that replaces the ``displayName`` attribute and adds a ``servicePrincipalName``:
229+
230+
.. code:: python
231+
232+
client.modify(
233+
"CN=User1,CN=Users,DC=domain,DC=local",
234+
changes=[
235+
LDAP_ModifyRequestChange(
236+
operation="replace",
237+
modification=LDAP_PartialAttribute(
238+
type="displayName",
239+
values=[
240+
LDAP_AttributeValue(value="Lord User the 1st")
241+
]
242+
)
243+
),
244+
LDAP_ModifyRequestChange(
245+
operation="add",
246+
modification=LDAP_PartialAttribute(
247+
type="servicePrincipalName",
248+
values=[
249+
LDAP_AttributeValue(value="http/lorduser")
250+
]
251+
)
252+
)
253+
]
254+
)

scapy/layers/ldap.py

+146-20
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ class ASN1_Class_LDAP(ASN1_Class):
208208

209209

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

213213

214214
class ASN1_Class_LDAP_Authentication(ASN1_Class):
@@ -397,7 +397,7 @@ def serverSaslCredsData(self):
397397

398398

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

402402

403403
class LDAP_UnbindRequest(ASN1_Packet):
@@ -409,7 +409,7 @@ class LDAP_UnbindRequest(ASN1_Packet):
409409

410410

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

414414

415415
class LDAP_SubstringFilterInitial(ASN1_Packet):
@@ -759,16 +759,16 @@ class LDAP_SearchRequest(ASN1_Packet):
759759
)
760760

761761

762-
class LDAP_SearchResponseEntryAttributeValue(ASN1_Packet):
762+
class LDAP_AttributeValue(ASN1_Packet):
763763
ASN1_codec = ASN1_Codecs.BER
764764
ASN1_root = AttributeValue("value", "")
765765

766766

767-
class LDAP_SearchResponseEntryAttribute(ASN1_Packet):
767+
class LDAP_PartialAttribute(ASN1_Packet):
768768
ASN1_codec = ASN1_Codecs.BER
769769
ASN1_root = ASN1F_SEQUENCE(
770770
AttributeType("type", ""),
771-
ASN1F_SET_OF("values", [], LDAP_SearchResponseEntryAttributeValue),
771+
ASN1F_SET_OF("values", [], LDAP_AttributeValue),
772772
)
773773

774774

@@ -778,8 +778,8 @@ class LDAP_SearchResponseEntry(ASN1_Packet):
778778
LDAPDN("objectName", ""),
779779
ASN1F_SEQUENCE_OF(
780780
"attributes",
781-
LDAP_SearchResponseEntryAttribute(),
782-
LDAP_SearchResponseEntryAttribute,
781+
LDAP_PartialAttribute(),
782+
LDAP_PartialAttribute,
783783
),
784784
implicit_tag=ASN1_Class_LDAP.SearchResultEntry,
785785
)
@@ -793,14 +793,6 @@ class LDAP_SearchResponseResultDone(ASN1_Packet):
793793
)
794794

795795

796-
class LDAP_AbandonRequest(ASN1_Packet):
797-
ASN1_codec = ASN1_Codecs.BER
798-
ASN1_root = ASN1F_SEQUENCE(
799-
ASN1F_INTEGER("messageID", 0),
800-
implicit_tag=ASN1_Class_LDAP.AbandonRequest,
801-
)
802-
803-
804796
class LDAP_SearchResponseReference(ASN1_Packet):
805797
ASN1_codec = ASN1_Codecs.BER
806798
ASN1_root = ASN1F_SEQUENCE_OF(
@@ -811,6 +803,106 @@ class LDAP_SearchResponseReference(ASN1_Packet):
811803
)
812804

813805

806+
# Modify Operation
807+
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.6
808+
809+
810+
class LDAP_ModifyRequestChange(ASN1_Packet):
811+
ASN1_codec = ASN1_Codecs.BER
812+
ASN1_root = ASN1F_SEQUENCE(
813+
ASN1F_ENUMERATED(
814+
"operation",
815+
0,
816+
{
817+
0: "add",
818+
1: "delete",
819+
2: "replace",
820+
},
821+
),
822+
ASN1F_PACKET("modification", LDAP_PartialAttribute(), LDAP_PartialAttribute),
823+
)
824+
825+
826+
class LDAP_ModifyRequest(ASN1_Packet):
827+
ASN1_codec = ASN1_Codecs.BER
828+
ASN1_root = ASN1F_SEQUENCE(
829+
LDAPDN("object", ""),
830+
ASN1F_SEQUENCE_OF("changes", [], LDAP_ModifyRequestChange),
831+
implicit_tag=ASN1_Class_LDAP.ModifyRequest,
832+
)
833+
834+
835+
class LDAP_ModifyResponse(ASN1_Packet):
836+
ASN1_codec = ASN1_Codecs.BER
837+
ASN1_root = ASN1F_SEQUENCE(
838+
*LDAPResult,
839+
implicit_tag=ASN1_Class_LDAP.ModifyResponse,
840+
)
841+
842+
843+
# Add Operation
844+
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.7
845+
846+
847+
class LDAP_Attribute(ASN1_Packet):
848+
ASN1_codec = ASN1_Codecs.BER
849+
ASN1_root = LDAP_PartialAttribute.ASN1_root
850+
851+
852+
class LDAP_AddRequest(ASN1_Packet):
853+
ASN1_codec = ASN1_Codecs.BER
854+
ASN1_root = ASN1F_SEQUENCE(
855+
LDAPDN("entry", ""),
856+
ASN1F_SEQUENCE_OF(
857+
"attributes",
858+
LDAP_Attribute(),
859+
LDAP_Attribute,
860+
),
861+
implicit_tag=ASN1_Class_LDAP.AddRequest,
862+
)
863+
864+
865+
class LDAP_AddResponse(ASN1_Packet):
866+
ASN1_codec = ASN1_Codecs.BER
867+
ASN1_root = ASN1F_SEQUENCE(
868+
*LDAPResult,
869+
implicit_tag=ASN1_Class_LDAP.AddResponse,
870+
)
871+
872+
873+
# Delete Operation
874+
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.8
875+
876+
877+
class LDAP_DelRequest(ASN1_Packet):
878+
ASN1_codec = ASN1_Codecs.BER
879+
ASN1_root = LDAPDN(
880+
"entry",
881+
"",
882+
implicit_tag=ASN1_Class_LDAP.DelRequest,
883+
)
884+
885+
886+
class LDAP_DelResponse(ASN1_Packet):
887+
ASN1_codec = ASN1_Codecs.BER
888+
ASN1_root = ASN1F_SEQUENCE(
889+
*LDAPResult,
890+
implicit_tag=ASN1_Class_LDAP.DelResponse,
891+
)
892+
893+
894+
# Abandon Operation
895+
# https://datatracker.ietf.org/doc/html/rfc4511#section-4.11
896+
897+
898+
class LDAP_AbandonRequest(ASN1_Packet):
899+
ASN1_codec = ASN1_Codecs.BER
900+
ASN1_root = ASN1F_SEQUENCE(
901+
ASN1F_INTEGER("messageID", 0),
902+
implicit_tag=ASN1_Class_LDAP.AbandonRequest,
903+
)
904+
905+
814906
# LDAP v3
815907

816908
# RFC 4511 sect 4.12 - Extended Operation
@@ -926,6 +1018,12 @@ class LDAP(ASN1_Packet):
9261018
LDAP_SearchResponseResultDone,
9271019
LDAP_AbandonRequest,
9281020
LDAP_SearchResponseReference,
1021+
LDAP_ModifyRequest,
1022+
LDAP_ModifyResponse,
1023+
LDAP_AddRequest,
1024+
LDAP_AddResponse,
1025+
LDAP_DelRequest,
1026+
LDAP_DelResponse,
9291027
LDAP_UnbindRequest,
9301028
LDAP_ExtendedResponse,
9311029
),
@@ -966,8 +1064,8 @@ def tcp_reassemble(cls, data, *args, **kwargs):
9661064
pkt = cls(data)
9671065
# Packet can be a whole response yet still miss some content.
9681066
if (
969-
LDAP_SearchResponseEntry in pkt and
970-
LDAP_SearchResponseResultDone not in pkt
1067+
LDAP_SearchResponseEntry in pkt
1068+
and LDAP_SearchResponseResultDone not in pkt
9711069
):
9721070
return None
9731071
return pkt
@@ -1242,9 +1340,9 @@ def make_reply(self, req):
12421340
/ CLDAP(
12431341
protocolOp=LDAP_SearchResponseEntry(
12441342
attributes=[
1245-
LDAP_SearchResponseEntryAttribute(
1343+
LDAP_PartialAttribute(
12461344
values=[
1247-
LDAP_SearchResponseEntryAttributeValue(
1345+
LDAP_AttributeValue(
12481346
value=ASN1_STRING(
12491347
val=bytes(
12501348
NETLOGON_SAM_LOGON_RESPONSE_EX(
@@ -2146,6 +2244,34 @@ def _ssafe(x):
21462244
break
21472245
return entries
21482246

2247+
def modify(
2248+
self,
2249+
object: str,
2250+
changes: List[LDAP_ModifyRequestChange],
2251+
controls: List[LDAP_Control] = [],
2252+
) -> None:
2253+
"""
2254+
Perform a LDAP modify request.
2255+
2256+
:returns:
2257+
"""
2258+
resp = self.sr1(
2259+
LDAP_ModifyRequest(
2260+
object=object,
2261+
changes=changes,
2262+
),
2263+
controls=controls,
2264+
timeout=3,
2265+
)
2266+
if (
2267+
LDAP_ModifyResponse not in resp.protocolOp
2268+
or resp.protocolOp.resultCode != 0
2269+
):
2270+
raise LDAP_Exception(
2271+
"LDAP modify failed !",
2272+
resp=resp,
2273+
)
2274+
21492275
def close(self):
21502276
if self.verb:
21512277
print("X Connection closed\n")

test/scapy/layers/ldap.uts

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ assert raw(pkt[CLDAP]) == b'0k\x02\x01\x01cf\x04\x00\n\x01\x00\n\x01\x00\x02\x01
113113

114114
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')
115115
assert pkt.getlayer(CLDAP, 2)
116-
assert isinstance(pkt.protocolOp[0].attributes[0].values[0], LDAP_SearchResponseEntryAttributeValue)
116+
assert isinstance(pkt.protocolOp[0].attributes[0].values[0], LDAP_AttributeValue)
117117
assert pkt.getlayer(CLDAP, 2).protocolOp.resultCode == 0x0
118118

119119
pkt2 = Ether(raw(pkt))

0 commit comments

Comments
 (0)