mirror of https://github.com/secdev/scapy.git
LDAP: add modify/add/delete (#4580)
This commit is contained in:
parent
206f1beea0
commit
8e08cbf759
|
@ -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")
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue