Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/scapy/layers/ldap.py: 43%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Gabriel Potter <gabriel[]potter[]fr>
6"""
7LDAP
9RFC 1777 - LDAP v2
10RFC 4511 - LDAP v3
12Note: to mimic Microsoft Windows LDAP packets, you must set::
14 conf.ASN1_default_long_size = 4
16.. note::
17 You will find more complete documentation for this layer over at
18 `LDAP <https://scapy.readthedocs.io/en/latest/layers/ldap.html>`_
19"""
21import collections
22import re
23import socket
24import ssl
25import string
26import struct
27import uuid
29from scapy.arch import get_if_addr
30from scapy.ansmachine import AnsweringMachine
31from scapy.asn1.asn1 import (
32 ASN1_BOOLEAN,
33 ASN1_Class,
34 ASN1_Codecs,
35 ASN1_ENUMERATED,
36 ASN1_INTEGER,
37 ASN1_STRING,
38)
39from scapy.asn1.ber import (
40 BER_Decoding_Error,
41 BER_id_dec,
42 BER_len_dec,
43 BERcodec_STRING,
44)
45from scapy.asn1fields import (
46 ASN1F_badsequence,
47 ASN1F_BOOLEAN,
48 ASN1F_CHOICE,
49 ASN1F_ENUMERATED,
50 ASN1F_FLAGS,
51 ASN1F_INTEGER,
52 ASN1F_NULL,
53 ASN1F_optional,
54 ASN1F_PACKET,
55 ASN1F_SEQUENCE_OF,
56 ASN1F_SEQUENCE,
57 ASN1F_SET_OF,
58 ASN1F_STRING_PacketField,
59 ASN1F_STRING,
60)
61from scapy.asn1packet import ASN1_Packet
62from scapy.config import conf
63from scapy.compat import StrEnum
64from scapy.consts import WINDOWS
65from scapy.error import log_runtime
66from scapy.fields import (
67 FieldLenField,
68 FlagsField,
69 ThreeBytesField,
70)
71from scapy.packet import (
72 Packet,
73 bind_bottom_up,
74 bind_layers,
75)
76from scapy.sendrecv import send
77from scapy.supersocket import (
78 SimpleSocket,
79 StreamSocket,
80 SSLStreamSocket,
81)
83from scapy.layers.dns import dns_resolve
84from scapy.layers.inet import IP, TCP, UDP
85from scapy.layers.inet6 import IPv6
86from scapy.layers.gssapi import (
87 _GSSAPI_Field,
88 ChannelBindingType,
89 GSS_C_FLAGS,
90 GSS_C_NO_CHANNEL_BINDINGS,
91 GSS_QOP_REQ_FLAGS,
92 GSS_S_COMPLETE,
93 GSS_S_CONTINUE_NEEDED,
94 GSSAPI_BLOB_SIGNATURE,
95 GSSAPI_BLOB,
96 GssChannelBindings,
97 SSP,
98)
99from scapy.layers.netbios import NBTDatagram
100from scapy.layers.smb import (
101 NETLOGON,
102 NETLOGON_SAM_LOGON_RESPONSE_EX,
103)
104from scapy.layers.windows.erref import STATUS_ERREF
106# Typing imports
107from typing import (
108 Any,
109 Dict,
110 List,
111 Optional,
112 Union,
113)
115# Elements of protocol
116# https://datatracker.ietf.org/doc/html/rfc1777#section-4
118LDAPString = ASN1F_STRING
119LDAPOID = ASN1F_STRING
120LDAPDN = LDAPString
121RelativeLDAPDN = LDAPString
122AttributeType = LDAPString
123AttributeValue = ASN1F_STRING
124URI = LDAPString
127class AttributeValueAssertion(ASN1_Packet):
128 ASN1_codec = ASN1_Codecs.BER
129 ASN1_root = ASN1F_SEQUENCE(
130 AttributeType("attributeType", "organizationName"),
131 AttributeValue("attributeValue", ""),
132 )
135class LDAPReferral(ASN1_Packet):
136 ASN1_codec = ASN1_Codecs.BER
137 ASN1_root = LDAPString("uri", "")
140LDAPResult = (
141 ASN1F_ENUMERATED(
142 "resultCode",
143 0,
144 {
145 0: "success",
146 1: "operationsError",
147 2: "protocolError",
148 3: "timeLimitExceeded",
149 4: "sizeLimitExceeded",
150 5: "compareFalse",
151 6: "compareTrue",
152 7: "authMethodNotSupported",
153 8: "strongAuthRequired",
154 10: "referral",
155 11: "adminLimitExceeded",
156 14: "saslBindInProgress",
157 16: "noSuchAttribute",
158 17: "undefinedAttributeType",
159 18: "inappropriateMatching",
160 19: "constraintViolation",
161 20: "attributeOrValueExists",
162 21: "invalidAttributeSyntax",
163 32: "noSuchObject",
164 33: "aliasProblem",
165 34: "invalidDNSyntax",
166 35: "isLeaf",
167 36: "aliasDereferencingProblem",
168 48: "inappropriateAuthentication",
169 49: "invalidCredentials",
170 50: "insufficientAccessRights",
171 51: "busy",
172 52: "unavailable",
173 53: "unwillingToPerform",
174 54: "loopDetect",
175 64: "namingViolation",
176 65: "objectClassViolation",
177 66: "notAllowedOnNonLeaf",
178 67: "notAllowedOnRDN",
179 68: "entryAlreadyExists",
180 69: "objectClassModsProhibited",
181 70: "resultsTooLarge", # CLDAP
182 80: "other",
183 },
184 ),
185 LDAPDN("matchedDN", ""),
186 LDAPString("diagnosticMessage", ""),
187 # LDAP v3 only
188 ASN1F_optional(ASN1F_SEQUENCE_OF("referral", [], LDAPReferral, implicit_tag=0xA3)),
189)
192# ldap APPLICATION
195class ASN1_Class_LDAP(ASN1_Class):
196 name = "LDAP"
197 # APPLICATION + CONSTRUCTED = 0x40 | 0x20
198 BindRequest = 0x60
199 BindResponse = 0x61
200 UnbindRequest = 0x42 # not constructed
201 SearchRequest = 0x63
202 SearchResultEntry = 0x64
203 SearchResultDone = 0x65
204 ModifyRequest = 0x66
205 ModifyResponse = 0x67
206 AddRequest = 0x68
207 AddResponse = 0x69
208 DelRequest = 0x4A # not constructed
209 DelResponse = 0x6B
210 ModifyDNRequest = 0x6C
211 ModifyDNResponse = 0x6D
212 CompareRequest = 0x6E
213 CompareResponse = 0x7F
214 AbandonRequest = 0x50 # application + primitive
215 SearchResultReference = 0x73
216 ExtendedRequest = 0x77
217 ExtendedResponse = 0x78
220# Bind operation
221# https://datatracker.ietf.org/doc/html/rfc4511#section-4.2
224class ASN1_Class_LDAP_Authentication(ASN1_Class):
225 name = "LDAP Authentication"
226 # CONTEXT-SPECIFIC = 0x80
227 simple = 0x80
228 krbv42LDAP = 0x81
229 krbv42DSA = 0x82
230 sasl = 0xA3 # CONTEXT-SPECIFIC | CONSTRUCTED
231 # [MS-ADTS] sect 5.1.1.1
232 sicilyPackageDiscovery = 0x89
233 sicilyNegotiate = 0x8A
234 sicilyResponse = 0x8B
237# simple
238class LDAP_Authentication_simple(ASN1_STRING):
239 tag = ASN1_Class_LDAP_Authentication.simple
242class BERcodec_LDAP_Authentication_simple(BERcodec_STRING):
243 tag = ASN1_Class_LDAP_Authentication.simple
246class ASN1F_LDAP_Authentication_simple(ASN1F_STRING):
247 ASN1_tag = ASN1_Class_LDAP_Authentication.simple
250# krbv42LDAP
251class LDAP_Authentication_krbv42LDAP(ASN1_STRING):
252 tag = ASN1_Class_LDAP_Authentication.krbv42LDAP
255class BERcodec_LDAP_Authentication_krbv42LDAP(BERcodec_STRING):
256 tag = ASN1_Class_LDAP_Authentication.krbv42LDAP
259class ASN1F_LDAP_Authentication_krbv42LDAP(ASN1F_STRING):
260 ASN1_tag = ASN1_Class_LDAP_Authentication.krbv42LDAP
263# krbv42DSA
264class LDAP_Authentication_krbv42DSA(ASN1_STRING):
265 tag = ASN1_Class_LDAP_Authentication.krbv42DSA
268class BERcodec_LDAP_Authentication_krbv42DSA(BERcodec_STRING):
269 tag = ASN1_Class_LDAP_Authentication.krbv42DSA
272class ASN1F_LDAP_Authentication_krbv42DSA(ASN1F_STRING):
273 ASN1_tag = ASN1_Class_LDAP_Authentication.krbv42DSA
276# sicilyPackageDiscovery
277class LDAP_Authentication_sicilyPackageDiscovery(ASN1_STRING):
278 tag = ASN1_Class_LDAP_Authentication.sicilyPackageDiscovery
281class BERcodec_LDAP_Authentication_sicilyPackageDiscovery(BERcodec_STRING):
282 tag = ASN1_Class_LDAP_Authentication.sicilyPackageDiscovery
285class ASN1F_LDAP_Authentication_sicilyPackageDiscovery(ASN1F_STRING):
286 ASN1_tag = ASN1_Class_LDAP_Authentication.sicilyPackageDiscovery
289# sicilyNegotiate
290class LDAP_Authentication_sicilyNegotiate(ASN1_STRING):
291 tag = ASN1_Class_LDAP_Authentication.sicilyNegotiate
294class BERcodec_LDAP_Authentication_sicilyNegotiate(BERcodec_STRING):
295 tag = ASN1_Class_LDAP_Authentication.sicilyNegotiate
298class ASN1F_LDAP_Authentication_sicilyNegotiate(ASN1F_STRING):
299 ASN1_tag = ASN1_Class_LDAP_Authentication.sicilyNegotiate
302# sicilyResponse
303class LDAP_Authentication_sicilyResponse(ASN1_STRING):
304 tag = ASN1_Class_LDAP_Authentication.sicilyResponse
307class BERcodec_LDAP_Authentication_sicilyResponse(BERcodec_STRING):
308 tag = ASN1_Class_LDAP_Authentication.sicilyResponse
311class ASN1F_LDAP_Authentication_sicilyResponse(ASN1F_STRING):
312 ASN1_tag = ASN1_Class_LDAP_Authentication.sicilyResponse
315_SASL_MECHANISMS = {b"GSS-SPNEGO": GSSAPI_BLOB, b"GSSAPI": GSSAPI_BLOB}
318class _SaslCredentialsField(ASN1F_STRING_PacketField):
319 def m2i(self, pkt, s):
320 val = super(_SaslCredentialsField, self).m2i(pkt, s)
321 if not val[0].val:
322 return val
323 if pkt.mechanism.val in _SASL_MECHANISMS:
324 return (
325 _SASL_MECHANISMS[pkt.mechanism.val](val[0].val, _underlayer=pkt),
326 val[1],
327 )
328 return val
331class LDAP_Authentication_SaslCredentials(ASN1_Packet):
332 ASN1_codec = ASN1_Codecs.BER
333 ASN1_root = ASN1F_SEQUENCE(
334 LDAPString("mechanism", ""),
335 ASN1F_optional(
336 _SaslCredentialsField("credentials", ""),
337 ),
338 implicit_tag=ASN1_Class_LDAP_Authentication.sasl,
339 )
342class LDAP_BindRequest(ASN1_Packet):
343 ASN1_codec = ASN1_Codecs.BER
344 ASN1_root = ASN1F_SEQUENCE(
345 ASN1F_INTEGER("version", 3),
346 LDAPDN("bind_name", ""),
347 ASN1F_CHOICE(
348 "authentication",
349 None,
350 ASN1F_LDAP_Authentication_simple,
351 ASN1F_LDAP_Authentication_krbv42LDAP,
352 ASN1F_LDAP_Authentication_krbv42DSA,
353 LDAP_Authentication_SaslCredentials,
354 ),
355 implicit_tag=ASN1_Class_LDAP.BindRequest,
356 )
359class LDAP_BindResponse(ASN1_Packet):
360 ASN1_codec = ASN1_Codecs.BER
361 ASN1_root = ASN1F_SEQUENCE(
362 *(
363 LDAPResult
364 + (
365 ASN1F_optional(
366 # For GSSAPI, the response is wrapped in
367 # LDAP_Authentication_SaslCredentials
368 ASN1F_STRING("serverSaslCredsWrap", "", implicit_tag=0xA7),
369 ),
370 ASN1F_optional(
371 ASN1F_STRING("serverSaslCreds", "", implicit_tag=0x87),
372 ),
373 )
374 ),
375 implicit_tag=ASN1_Class_LDAP.BindResponse,
376 )
378 @property
379 def serverCreds(self):
380 """
381 serverCreds field in SicilyBindResponse
382 """
383 return self.matchedDN.val
385 @serverCreds.setter
386 def serverCreds(self, val):
387 """
388 serverCreds field in SicilyBindResponse
389 """
390 self.matchedDN = ASN1_STRING(val)
392 @property
393 def serverSaslCredsData(self):
394 """
395 Get serverSaslCreds or serverSaslCredsWrap depending on what's available
396 """
397 if self.serverSaslCredsWrap and self.serverSaslCredsWrap.val:
398 wrap = LDAP_Authentication_SaslCredentials(self.serverSaslCredsWrap.val)
399 val = wrap.credentials
400 if isinstance(val, ASN1_STRING):
401 return val.val
402 return bytes(val)
403 elif self.serverSaslCreds and self.serverSaslCreds.val:
404 return self.serverSaslCreds.val
405 else:
406 return None
409# Unbind operation
410# https://datatracker.ietf.org/doc/html/rfc4511#section-4.3
413class LDAP_UnbindRequest(ASN1_Packet):
414 ASN1_codec = ASN1_Codecs.BER
415 ASN1_root = ASN1F_SEQUENCE(
416 ASN1F_NULL("info", 0),
417 implicit_tag=ASN1_Class_LDAP.UnbindRequest,
418 )
421# Search operation
422# https://datatracker.ietf.org/doc/html/rfc4511#section-4.5
425class LDAP_SubstringFilterInitial(ASN1_Packet):
426 ASN1_codec = ASN1_Codecs.BER
427 ASN1_root = LDAPString("val", "")
430class LDAP_SubstringFilterAny(ASN1_Packet):
431 ASN1_codec = ASN1_Codecs.BER
432 ASN1_root = LDAPString("val", "")
435class LDAP_SubstringFilterFinal(ASN1_Packet):
436 ASN1_codec = ASN1_Codecs.BER
437 ASN1_root = LDAPString("val", "")
440class LDAP_SubstringFilterStr(ASN1_Packet):
441 ASN1_codec = ASN1_Codecs.BER
442 ASN1_root = ASN1F_CHOICE(
443 "str",
444 ASN1_STRING(""),
445 ASN1F_PACKET(
446 "initial",
447 LDAP_SubstringFilterInitial(),
448 LDAP_SubstringFilterInitial,
449 implicit_tag=0x80,
450 ),
451 ASN1F_PACKET(
452 "any", LDAP_SubstringFilterAny(), LDAP_SubstringFilterAny, implicit_tag=0x81
453 ),
454 ASN1F_PACKET(
455 "final",
456 LDAP_SubstringFilterFinal(),
457 LDAP_SubstringFilterFinal,
458 implicit_tag=0x82,
459 ),
460 )
463class LDAP_SubstringFilter(ASN1_Packet):
464 ASN1_codec = ASN1_Codecs.BER
465 ASN1_root = ASN1F_SEQUENCE(
466 AttributeType("type", ""),
467 ASN1F_SEQUENCE_OF("filters", [], LDAP_SubstringFilterStr),
468 )
471_LDAP_Filter = lambda *args, **kwargs: LDAP_Filter(*args, **kwargs)
474class LDAP_FilterAnd(ASN1_Packet):
475 ASN1_codec = ASN1_Codecs.BER
476 ASN1_root = ASN1F_SET_OF("vals", [], _LDAP_Filter)
479class LDAP_FilterOr(ASN1_Packet):
480 ASN1_codec = ASN1_Codecs.BER
481 ASN1_root = ASN1F_SET_OF("vals", [], _LDAP_Filter)
484class LDAP_FilterNot(ASN1_Packet):
485 ASN1_codec = ASN1_Codecs.BER
486 ASN1_root = ASN1F_SEQUENCE(
487 ASN1F_PACKET("val", None, None, next_cls_cb=lambda *args, **kwargs: LDAP_Filter)
488 )
491class LDAP_FilterPresent(ASN1_Packet):
492 ASN1_codec = ASN1_Codecs.BER
493 ASN1_root = AttributeType("present", "objectClass")
496class LDAP_FilterEqual(ASN1_Packet):
497 ASN1_codec = ASN1_Codecs.BER
498 ASN1_root = AttributeValueAssertion.ASN1_root
501class LDAP_FilterGreaterOrEqual(ASN1_Packet):
502 ASN1_codec = ASN1_Codecs.BER
503 ASN1_root = AttributeValueAssertion.ASN1_root
506class LDAP_FilterLessOrEqual(ASN1_Packet):
507 ASN1_codec = ASN1_Codecs.BER
508 ASN1_root = AttributeValueAssertion.ASN1_root
511class LDAP_FilterApproxMatch(ASN1_Packet):
512 ASN1_codec = ASN1_Codecs.BER
513 ASN1_root = AttributeValueAssertion.ASN1_root
516class LDAP_FilterExtensibleMatch(ASN1_Packet):
517 ASN1_codec = ASN1_Codecs.BER
518 ASN1_root = ASN1F_SEQUENCE(
519 ASN1F_optional(
520 LDAPString("matchingRule", "", implicit_tag=0x81),
521 ),
522 ASN1F_optional(
523 LDAPString("type", "", implicit_tag=0x81),
524 ),
525 AttributeValue("matchValue", "", implicit_tag=0x82),
526 ASN1F_BOOLEAN("dnAttributes", False, implicit_tag=0x84),
527 )
530class ASN1_Class_LDAP_Filter(ASN1_Class):
531 name = "LDAP Filter"
532 # CONTEXT-SPECIFIC + CONSTRUCTED = 0x80 | 0x20
533 And = 0xA0
534 Or = 0xA1
535 Not = 0xA2
536 EqualityMatch = 0xA3
537 Substrings = 0xA4
538 GreaterOrEqual = 0xA5
539 LessOrEqual = 0xA6
540 Present = 0x87 # not constructed
541 ApproxMatch = 0xA8
542 ExtensibleMatch = 0xA9
545class LDAP_Filter(ASN1_Packet):
546 ASN1_codec = ASN1_Codecs.BER
547 ASN1_root = ASN1F_CHOICE(
548 "filter",
549 LDAP_FilterPresent(),
550 ASN1F_PACKET(
551 "and_", None, LDAP_FilterAnd, implicit_tag=ASN1_Class_LDAP_Filter.And
552 ),
553 ASN1F_PACKET(
554 "or_", None, LDAP_FilterOr, implicit_tag=ASN1_Class_LDAP_Filter.Or
555 ),
556 ASN1F_PACKET(
557 "not_", None, LDAP_FilterNot, implicit_tag=ASN1_Class_LDAP_Filter.Not
558 ),
559 ASN1F_PACKET(
560 "equalityMatch",
561 None,
562 LDAP_FilterEqual,
563 implicit_tag=ASN1_Class_LDAP_Filter.EqualityMatch,
564 ),
565 ASN1F_PACKET(
566 "substrings",
567 None,
568 LDAP_SubstringFilter,
569 implicit_tag=ASN1_Class_LDAP_Filter.Substrings,
570 ),
571 ASN1F_PACKET(
572 "greaterOrEqual",
573 None,
574 LDAP_FilterGreaterOrEqual,
575 implicit_tag=ASN1_Class_LDAP_Filter.GreaterOrEqual,
576 ),
577 ASN1F_PACKET(
578 "lessOrEqual",
579 None,
580 LDAP_FilterLessOrEqual,
581 implicit_tag=ASN1_Class_LDAP_Filter.LessOrEqual,
582 ),
583 ASN1F_PACKET(
584 "present",
585 None,
586 LDAP_FilterPresent,
587 implicit_tag=ASN1_Class_LDAP_Filter.Present,
588 ),
589 ASN1F_PACKET(
590 "approxMatch",
591 None,
592 LDAP_FilterApproxMatch,
593 implicit_tag=ASN1_Class_LDAP_Filter.ApproxMatch,
594 ),
595 ASN1F_PACKET(
596 "extensibleMatch",
597 None,
598 LDAP_FilterExtensibleMatch,
599 implicit_tag=ASN1_Class_LDAP_Filter.ExtensibleMatch,
600 ),
601 )
603 @staticmethod
604 def from_rfc2254_string(filter: str):
605 """
606 Convert a RFC-2254 filter to LDAP_Filter
607 """
608 # Note: this code is very dumb to be readable.
609 _lerr = "Invalid LDAP filter string: "
610 if filter.lstrip()[0] != "(":
611 filter = "(%s)" % filter
613 # 1. Cheap lexer.
614 tokens = []
615 cur = tokens
616 backtrack = []
617 filterlen = len(filter)
618 i = 0
619 while i < filterlen:
620 c = filter[i]
621 i += 1
622 if c in [" ", "\t", "\n"]:
623 # skip spaces
624 continue
625 elif c == "(":
626 # enclosure
627 cur.append([])
628 backtrack.append(cur)
629 cur = cur[-1]
630 elif c == ")":
631 # end of enclosure
632 if not backtrack:
633 raise ValueError(_lerr + "parenthesis unmatched.")
634 cur = backtrack.pop(-1)
635 elif c in "&|!":
636 # and / or / not
637 cur.append(c)
638 elif c in "=":
639 # filtertype
640 if cur[-1] in "~><:":
641 cur[-1] += c
642 continue
643 cur.append(c)
644 elif c in "~><":
645 # comparisons
646 cur.append(c)
647 elif c == ":":
648 # extensible
649 cur.append(c)
650 elif c == "*":
651 # substring
652 cur.append(c)
653 else:
654 # value
655 v = ""
656 for x in filter[i - 1 :]:
657 if x in "():!|&~<>=*":
658 break
659 v += x
660 if not v:
661 raise ValueError(_lerr + "critical failure (impossible).")
662 i += len(v) - 1
663 cur.append(v)
665 # Check that parenthesis were closed
666 if backtrack:
667 raise ValueError(_lerr + "parenthesis unmatched.")
669 # LDAP filters must have an empty enclosure ()
670 tokens = tokens[0]
672 # 2. Cheap grammar parser.
673 # Doing it recursively is trivial.
674 def _getfld(x):
675 if not x:
676 raise ValueError(_lerr + "empty enclosure.")
677 elif len(x) == 1 and isinstance(x[0], list):
678 # useless enclosure
679 return _getfld(x[0])
680 elif x[0] in "&|":
681 # multinary operator
682 if len(x) < 3:
683 raise ValueError(_lerr + "bad use of multinary operator.")
684 return (LDAP_FilterAnd if x[0] == "&" else LDAP_FilterOr)(
685 vals=[LDAP_Filter(filter=_getfld(y)) for y in x[1:]]
686 )
687 elif x[0] == "!":
688 # unary operator
689 if len(x) != 2:
690 raise ValueError(_lerr + "bad use of unary operator.")
691 return LDAP_FilterNot(
692 val=LDAP_Filter(filter=_getfld(x[1])),
693 )
694 elif "=" in x and "*" in x:
695 # substring
696 if len(x) < 3 or x[1] != "=":
697 raise ValueError(_lerr + "bad use of substring.")
698 return LDAP_SubstringFilter(
699 type=ASN1_STRING(x[0].strip()),
700 filters=[
701 LDAP_SubstringFilterStr(
702 str=(
703 LDAP_SubstringFilterFinal
704 if i == (len(x) - 3)
705 else (
706 LDAP_SubstringFilterInitial
707 if i == 0
708 else LDAP_SubstringFilterAny
709 )
710 )(val=ASN1_STRING(y))
711 )
712 for i, y in enumerate(x[2:])
713 if y != "*"
714 ],
715 )
716 elif ":=" in x:
717 # extensible
718 raise NotImplementedError("Extensible not implemented.")
719 elif any(y in ["<=", ">=", "~=", "="] for y in x):
720 # simple
721 if len(x) != 3 or "=" not in x[1]:
722 raise ValueError(_lerr + "bad use of comparison.")
723 if x[2] == "*":
724 return LDAP_FilterPresent(present=ASN1_STRING(x[0]))
725 return (
726 LDAP_FilterLessOrEqual
727 if "<=" in x
728 else (
729 LDAP_FilterGreaterOrEqual
730 if ">=" in x
731 else LDAP_FilterApproxMatch if "~=" in x else LDAP_FilterEqual
732 )
733 )(
734 attributeType=ASN1_STRING(x[0].strip()),
735 attributeValue=ASN1_STRING(x[2]),
736 )
737 else:
738 raise ValueError(_lerr + "invalid filter.")
740 return LDAP_Filter(filter=_getfld(tokens))
743class LDAP_SearchRequestAttribute(ASN1_Packet):
744 ASN1_codec = ASN1_Codecs.BER
745 ASN1_root = AttributeType("type", "")
748class LDAP_SearchRequest(ASN1_Packet):
749 ASN1_codec = ASN1_Codecs.BER
750 ASN1_root = ASN1F_SEQUENCE(
751 LDAPDN("baseObject", ""),
752 ASN1F_ENUMERATED(
753 "scope", 0, {0: "baseObject", 1: "singleLevel", 2: "wholeSubtree"}
754 ),
755 ASN1F_ENUMERATED(
756 "derefAliases",
757 0,
758 {
759 0: "neverDerefAliases",
760 1: "derefInSearching",
761 2: "derefFindingBaseObj",
762 3: "derefAlways",
763 },
764 ),
765 ASN1F_INTEGER("sizeLimit", 0),
766 ASN1F_INTEGER("timeLimit", 0),
767 ASN1F_BOOLEAN("attrsOnly", False),
768 ASN1F_PACKET("filter", LDAP_Filter(), LDAP_Filter),
769 ASN1F_SEQUENCE_OF("attributes", [], LDAP_SearchRequestAttribute),
770 implicit_tag=ASN1_Class_LDAP.SearchRequest,
771 )
774class LDAP_AttributeValue(ASN1_Packet):
775 ASN1_codec = ASN1_Codecs.BER
776 ASN1_root = AttributeValue("value", "")
779class LDAP_PartialAttribute(ASN1_Packet):
780 ASN1_codec = ASN1_Codecs.BER
781 ASN1_root = ASN1F_SEQUENCE(
782 AttributeType("type", ""),
783 ASN1F_SET_OF("values", [], LDAP_AttributeValue),
784 )
787class LDAP_SearchResponseEntry(ASN1_Packet):
788 ASN1_codec = ASN1_Codecs.BER
789 ASN1_root = ASN1F_SEQUENCE(
790 LDAPDN("objectName", ""),
791 ASN1F_SEQUENCE_OF(
792 "attributes",
793 LDAP_PartialAttribute(),
794 LDAP_PartialAttribute,
795 ),
796 implicit_tag=ASN1_Class_LDAP.SearchResultEntry,
797 )
800class LDAP_SearchResponseResultDone(ASN1_Packet):
801 ASN1_codec = ASN1_Codecs.BER
802 ASN1_root = ASN1F_SEQUENCE(
803 *LDAPResult,
804 implicit_tag=ASN1_Class_LDAP.SearchResultDone,
805 )
808class LDAP_SearchResponseReference(ASN1_Packet):
809 ASN1_codec = ASN1_Codecs.BER
810 ASN1_root = ASN1F_SEQUENCE_OF(
811 "uris",
812 [],
813 URI,
814 implicit_tag=ASN1_Class_LDAP.SearchResultReference,
815 )
818# Modify Operation
819# https://datatracker.ietf.org/doc/html/rfc4511#section-4.6
822class LDAP_ModifyRequestChange(ASN1_Packet):
823 ASN1_codec = ASN1_Codecs.BER
824 ASN1_root = ASN1F_SEQUENCE(
825 ASN1F_ENUMERATED(
826 "operation",
827 0,
828 {
829 0: "add",
830 1: "delete",
831 2: "replace",
832 },
833 ),
834 ASN1F_PACKET("modification", LDAP_PartialAttribute(), LDAP_PartialAttribute),
835 )
838class LDAP_ModifyRequest(ASN1_Packet):
839 ASN1_codec = ASN1_Codecs.BER
840 ASN1_root = ASN1F_SEQUENCE(
841 LDAPDN("object", ""),
842 ASN1F_SEQUENCE_OF("changes", [], LDAP_ModifyRequestChange),
843 implicit_tag=ASN1_Class_LDAP.ModifyRequest,
844 )
847class LDAP_ModifyResponse(ASN1_Packet):
848 ASN1_codec = ASN1_Codecs.BER
849 ASN1_root = ASN1F_SEQUENCE(
850 *LDAPResult,
851 implicit_tag=ASN1_Class_LDAP.ModifyResponse,
852 )
855# Add Operation
856# https://datatracker.ietf.org/doc/html/rfc4511#section-4.7
859class LDAP_Attribute(ASN1_Packet):
860 ASN1_codec = ASN1_Codecs.BER
861 ASN1_root = LDAP_PartialAttribute.ASN1_root
864class LDAP_AddRequest(ASN1_Packet):
865 ASN1_codec = ASN1_Codecs.BER
866 ASN1_root = ASN1F_SEQUENCE(
867 LDAPDN("entry", ""),
868 ASN1F_SEQUENCE_OF(
869 "attributes",
870 LDAP_Attribute(),
871 LDAP_Attribute,
872 ),
873 implicit_tag=ASN1_Class_LDAP.AddRequest,
874 )
877class LDAP_AddResponse(ASN1_Packet):
878 ASN1_codec = ASN1_Codecs.BER
879 ASN1_root = ASN1F_SEQUENCE(
880 *LDAPResult,
881 implicit_tag=ASN1_Class_LDAP.AddResponse,
882 )
885# Delete Operation
886# https://datatracker.ietf.org/doc/html/rfc4511#section-4.8
889class LDAP_DelRequest(ASN1_Packet):
890 ASN1_codec = ASN1_Codecs.BER
891 ASN1_root = LDAPDN(
892 "entry",
893 "",
894 implicit_tag=ASN1_Class_LDAP.DelRequest,
895 )
898class LDAP_DelResponse(ASN1_Packet):
899 ASN1_codec = ASN1_Codecs.BER
900 ASN1_root = ASN1F_SEQUENCE(
901 *LDAPResult,
902 implicit_tag=ASN1_Class_LDAP.DelResponse,
903 )
906# Modify DN Operation
907# https://datatracker.ietf.org/doc/html/rfc4511#section-4.9
910class LDAP_ModifyDNRequest(ASN1_Packet):
911 ASN1_codec = ASN1_Codecs.BER
912 ASN1_root = ASN1F_SEQUENCE(
913 LDAPDN("entry", ""),
914 LDAPDN("newrdn", ""),
915 ASN1F_BOOLEAN("deleteoldrdn", ASN1_BOOLEAN(False)),
916 ASN1F_optional(LDAPDN("newSuperior", None, implicit_tag=0xA0)),
917 implicit_tag=ASN1_Class_LDAP.ModifyDNRequest,
918 )
921class LDAP_ModifyDNResponse(ASN1_Packet):
922 ASN1_codec = ASN1_Codecs.BER
923 ASN1_root = ASN1F_SEQUENCE(
924 *LDAPResult,
925 implicit_tag=ASN1_Class_LDAP.ModifyDNResponse,
926 )
929# Abandon Operation
930# https://datatracker.ietf.org/doc/html/rfc4511#section-4.11
933class LDAP_AbandonRequest(ASN1_Packet):
934 ASN1_codec = ASN1_Codecs.BER
935 ASN1_root = ASN1F_SEQUENCE(
936 ASN1F_INTEGER("messageID", 0),
937 implicit_tag=ASN1_Class_LDAP.AbandonRequest,
938 )
941# LDAP v3
943# RFC 4511 sect 4.12 - Extended Operation
946class LDAP_ExtendedResponse(ASN1_Packet):
947 ASN1_codec = ASN1_Codecs.BER
948 ASN1_root = ASN1F_SEQUENCE(
949 *(
950 LDAPResult
951 + (
952 ASN1F_optional(LDAPOID("responseName", None, implicit_tag=0x8A)),
953 ASN1F_optional(ASN1F_STRING("responseValue", None, implicit_tag=0x8B)),
954 )
955 ),
956 implicit_tag=ASN1_Class_LDAP.ExtendedResponse,
957 )
959 def do_dissect(self, x):
960 # Note: Windows builds this packet with a buggy sequence size, that does not
961 # include the optional fields. Do another pass of dissection on the optionals.
962 s = super(LDAP_ExtendedResponse, self).do_dissect(x)
963 if not s:
964 return s
965 for obj in self.ASN1_root.seq[-2:]: # only on the 2 optional fields
966 try:
967 s = obj.dissect(self, s)
968 except ASN1F_badsequence:
969 break
970 return s
973# RFC 4511 sect 4.1.11
975_LDAP_CONTROLS = {}
978class _ControlValue_Field(ASN1F_STRING_PacketField):
979 def m2i(self, pkt, s):
980 val = super(_ControlValue_Field, self).m2i(pkt, s)
981 if not val[0].val:
982 return val
983 controlType = pkt.controlType.val.decode()
984 if controlType in _LDAP_CONTROLS:
985 return (
986 _LDAP_CONTROLS[controlType](val[0].val, _underlayer=pkt),
987 val[1],
988 )
989 return val
992class LDAP_Control(ASN1_Packet):
993 ASN1_codec = ASN1_Codecs.BER
994 ASN1_root = ASN1F_SEQUENCE(
995 LDAPOID("controlType", ""),
996 ASN1F_optional(
997 ASN1F_BOOLEAN("criticality", False),
998 ),
999 ASN1F_optional(_ControlValue_Field("controlValue", "")),
1000 )
1003# RFC 2696 - LDAP Control Extension for Simple Paged Results Manipulation
1006class LDAP_realSearchControlValue(ASN1_Packet):
1007 ASN1_codec = ASN1_Codecs.BER
1008 ASN1_root = ASN1F_SEQUENCE(
1009 ASN1F_INTEGER("size", 0),
1010 ASN1F_STRING("cookie", ""),
1011 )
1014_LDAP_CONTROLS["1.2.840.113556.1.4.319"] = LDAP_realSearchControlValue
1017# [MS-ADTS]
1020class LDAP_serverSDFlagsControl(ASN1_Packet):
1021 ASN1_codec = ASN1_Codecs.BER
1022 ASN1_root = ASN1F_SEQUENCE(
1023 ASN1F_FLAGS(
1024 "flags",
1025 None,
1026 [
1027 "OWNER",
1028 "GROUP",
1029 "DACL",
1030 "SACL",
1031 ],
1032 )
1033 )
1036_LDAP_CONTROLS["1.2.840.113556.1.4.801"] = LDAP_serverSDFlagsControl
1039# LDAP main class
1042class LDAP(ASN1_Packet):
1043 ASN1_codec = ASN1_Codecs.BER
1044 ASN1_root = ASN1F_SEQUENCE(
1045 ASN1F_INTEGER("messageID", 0),
1046 ASN1F_CHOICE(
1047 "protocolOp",
1048 LDAP_SearchRequest(),
1049 LDAP_BindRequest,
1050 LDAP_BindResponse,
1051 LDAP_SearchRequest,
1052 LDAP_SearchResponseEntry,
1053 LDAP_SearchResponseResultDone,
1054 LDAP_AbandonRequest,
1055 LDAP_SearchResponseReference,
1056 LDAP_ModifyRequest,
1057 LDAP_ModifyResponse,
1058 LDAP_AddRequest,
1059 LDAP_AddResponse,
1060 LDAP_DelRequest,
1061 LDAP_DelResponse,
1062 LDAP_ModifyDNRequest,
1063 LDAP_ModifyDNResponse,
1064 LDAP_UnbindRequest,
1065 LDAP_ExtendedResponse,
1066 ),
1067 # LDAP v3 only
1068 ASN1F_optional(
1069 ASN1F_SEQUENCE_OF("Controls", None, LDAP_Control, implicit_tag=0xA0)
1070 ),
1071 )
1073 show_indent = 0
1075 @classmethod
1076 def dispatch_hook(cls, _pkt=None, *args, **kargs):
1077 if _pkt and len(_pkt) >= 4:
1078 # Heuristic to detect SASL_Buffer
1079 if _pkt[0] != 0x30:
1080 if struct.unpack("!I", _pkt[:4])[0] + 4 == len(_pkt):
1081 return LDAP_SASL_Buffer
1082 return conf.raw_layer
1083 return cls
1085 @classmethod
1086 def tcp_reassemble(cls, data, metadata, *args, **kwargs):
1087 if len(data) < 4:
1088 return None
1089 # For LDAP, we would prefer to have the entire LDAP response
1090 # (multiple LDAP concatenated) in one go, to stay consistent with
1091 # what you get when using SASL.
1092 remaining = data
1093 while remaining:
1094 try:
1095 length, x = BER_len_dec(BER_id_dec(remaining)[1])
1096 except (BER_Decoding_Error, IndexError):
1097 return None
1098 if length and len(x) >= length:
1099 remaining = x[length:]
1100 if not remaining:
1101 pkt = cls(data)
1102 # Packet can be a whole response yet still miss some content.
1103 if (
1104 LDAP_SearchResponseEntry in pkt
1105 and LDAP_SearchResponseResultDone not in pkt
1106 ):
1107 return None
1108 return pkt
1109 else:
1110 if length:
1111 metadata["tcp_min_len"] = len(data) - len(x) + length
1112 return None
1113 return None
1115 def hashret(self):
1116 return b"ldap"
1118 @property
1119 def unsolicited(self):
1120 # RFC4511 sect 4.4. - Unsolicited Notification
1121 return self.messageID == 0 and isinstance(
1122 self.protocolOp, LDAP_ExtendedResponse
1123 )
1125 def answers(self, other):
1126 if self.unsolicited:
1127 return True
1128 return isinstance(other, LDAP) and other.messageID == self.messageID
1130 def mysummary(self):
1131 if not self.protocolOp or not self.messageID:
1132 return ""
1133 return (
1134 "%s(%s)"
1135 % (
1136 self.protocolOp.__class__.__name__.replace("_", " "),
1137 self.messageID.val,
1138 ),
1139 [LDAP],
1140 )
1143bind_layers(LDAP, LDAP)
1145bind_bottom_up(TCP, LDAP, dport=389)
1146bind_bottom_up(TCP, LDAP, sport=389)
1147bind_bottom_up(TCP, LDAP, dport=3268)
1148bind_bottom_up(TCP, LDAP, sport=3268)
1149bind_layers(TCP, LDAP, sport=389, dport=389)
1151# CLDAP - rfc1798
1154class CLDAP(ASN1_Packet):
1155 ASN1_codec = ASN1_Codecs.BER
1156 ASN1_root = ASN1F_SEQUENCE(
1157 LDAP.ASN1_root.seq[0], # messageID
1158 ASN1F_optional(
1159 LDAPDN("user", ""),
1160 ),
1161 LDAP.ASN1_root.seq[1], # protocolOp
1162 )
1164 def answers(self, other):
1165 return isinstance(other, CLDAP) and other.messageID == self.messageID
1168bind_layers(CLDAP, CLDAP)
1170bind_bottom_up(UDP, CLDAP, dport=389)
1171bind_bottom_up(UDP, CLDAP, sport=389)
1172bind_layers(UDP, CLDAP, sport=389, dport=389)
1174# [MS-ADTS] sect 3.1.1.2.3.3
1176LDAP_PROPERTY_SET = {
1177 uuid.UUID(
1178 "C7407360-20BF-11D0-A768-00AA006E0529"
1179 ): "Domain Password & Lockout Policies",
1180 uuid.UUID("59BA2F42-79A2-11D0-9020-00C04FC2D3CF"): "General Information",
1181 uuid.UUID("4C164200-20C0-11D0-A768-00AA006E0529"): "Account Restrictions",
1182 uuid.UUID("5F202010-79A5-11D0-9020-00C04FC2D4CF"): "Logon Information",
1183 uuid.UUID("BC0AC240-79A9-11D0-9020-00C04FC2D4CF"): "Group Membership",
1184 uuid.UUID("E45795B2-9455-11D1-AEBD-0000F80367C1"): "Phone and Mail Options",
1185 uuid.UUID("77B5B886-944A-11D1-AEBD-0000F80367C1"): "Personal Information",
1186 uuid.UUID("E45795B3-9455-11D1-AEBD-0000F80367C1"): "Web Information",
1187 uuid.UUID("E48D0154-BCF8-11D1-8702-00C04FB96050"): "Public Information",
1188 uuid.UUID("037088F8-0AE1-11D2-B422-00A0C968F939"): "Remote Access Information",
1189 uuid.UUID("B8119FD0-04F6-4762-AB7A-4986C76B3F9A"): "Other Domain Parameters",
1190 uuid.UUID("72E39547-7B18-11D1-ADEF-00C04FD8D5CD"): "DNS Host Name Attributes",
1191 uuid.UUID("FFA6F046-CA4B-4FEB-B40D-04DFEE722543"): "MS-TS-GatewayAccess",
1192 uuid.UUID("91E647DE-D96F-4B70-9557-D63FF4F3CCD8"): "Private Information",
1193 uuid.UUID("5805BC62-BDC9-4428-A5E2-856A0F4C185E"): "Terminal Server License Server",
1194}
1196# [MS-ADTS] sect 5.1.3.2.1
1198LDAP_CONTROL_ACCESS_RIGHTS = {
1199 uuid.UUID("ee914b82-0a98-11d1-adbb-00c04fd8d5cd"): "Abandon-Replication",
1200 uuid.UUID("440820ad-65b4-11d1-a3da-0000f875ae0d"): "Add-GUID",
1201 uuid.UUID("1abd7cf8-0a99-11d1-adbb-00c04fd8d5cd"): "Allocate-Rids",
1202 uuid.UUID("68b1d179-0d15-4d4f-ab71-46152e79a7bc"): "Allowed-To-Authenticate",
1203 uuid.UUID("edacfd8f-ffb3-11d1-b41d-00a0c968f939"): "Apply-Group-Policy",
1204 uuid.UUID("0e10c968-78fb-11d2-90d4-00c04f79dc55"): "Certificate-Enrollment",
1205 uuid.UUID("a05b8cc2-17bc-4802-a710-e7c15ab866a2"): "Certificate-AutoEnrollment",
1206 uuid.UUID("014bf69c-7b3b-11d1-85f6-08002be74fab"): "Change-Domain-Master",
1207 uuid.UUID("cc17b1fb-33d9-11d2-97d4-00c04fd8d5cd"): "Change-Infrastructure-Master",
1208 uuid.UUID("bae50096-4752-11d1-9052-00c04fc2d4cf"): "Change-PDC",
1209 uuid.UUID("d58d5f36-0a98-11d1-adbb-00c04fd8d5cd"): "Change-Rid-Master",
1210 uuid.UUID("e12b56b6-0a95-11d1-adbb-00c04fd8d5cd"): "Change-Schema-Master",
1211 uuid.UUID("e2a36dc9-ae17-47c3-b58b-be34c55ba633"): "Create-Inbound-Forest-Trust",
1212 uuid.UUID("fec364e0-0a98-11d1-adbb-00c04fd8d5cd"): "Do-Garbage-Collection",
1213 uuid.UUID("ab721a52-1e2f-11d0-9819-00aa0040529b"): "Domain-Administer-Server",
1214 uuid.UUID("69ae6200-7f46-11d2-b9ad-00c04f79f805"): "DS-Check-Stale-Phantoms",
1215 uuid.UUID("2f16c4a5-b98e-432c-952a-cb388ba33f2e"): "DS-Execute-Intentions-Script",
1216 uuid.UUID("9923a32a-3607-11d2-b9be-0000f87a36b2"): "DS-Install-Replica",
1217 uuid.UUID("4ecc03fe-ffc0-4947-b630-eb672a8a9dbc"): "DS-Query-Self-Quota",
1218 uuid.UUID("1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Get-Changes",
1219 uuid.UUID("1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Get-Changes-All",
1220 uuid.UUID(
1221 "89e95b76-444d-4c62-991a-0facbeda640c"
1222 ): "DS-Replication-Get-Changes-In-Filtered-Set",
1223 uuid.UUID("1131f6ac-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Manage-Topology",
1224 uuid.UUID(
1225 "f98340fb-7c5b-4cdb-a00b-2ebdfa115a96"
1226 ): "DS-Replication-Monitor-Topology",
1227 uuid.UUID("1131f6ab-9c07-11d1-f79f-00c04fc2dcd2"): "DS-Replication-Synchronize",
1228 uuid.UUID(
1229 "05c74c5e-4deb-43b4-bd9f-86664c2a7fd5"
1230 ): "Enable-Per-User-Reversibly-Encrypted-Password",
1231 uuid.UUID("b7b1b3de-ab09-4242-9e30-9980e5d322f7"): "Generate-RSoP-Logging",
1232 uuid.UUID("b7b1b3dd-ab09-4242-9e30-9980e5d322f7"): "Generate-RSoP-Planning",
1233 uuid.UUID("7c0e2a7c-a419-48e4-a995-10180aad54dd"): "Manage-Optional-Features",
1234 uuid.UUID("ba33815a-4f93-4c76-87f3-57574bff8109"): "Migrate-SID-History",
1235 uuid.UUID("b4e60130-df3f-11d1-9c86-006008764d0e"): "msmq-Open-Connector",
1236 uuid.UUID("06bd3201-df3e-11d1-9c86-006008764d0e"): "msmq-Peek",
1237 uuid.UUID("4b6e08c3-df3c-11d1-9c86-006008764d0e"): "msmq-Peek-computer-Journal",
1238 uuid.UUID("4b6e08c1-df3c-11d1-9c86-006008764d0e"): "msmq-Peek-Dead-Letter",
1239 uuid.UUID("06bd3200-df3e-11d1-9c86-006008764d0e"): "msmq-Receive",
1240 uuid.UUID("4b6e08c2-df3c-11d1-9c86-006008764d0e"): "msmq-Receive-computer-Journal",
1241 uuid.UUID("4b6e08c0-df3c-11d1-9c86-006008764d0e"): "msmq-Receive-Dead-Letter",
1242 uuid.UUID("06bd3203-df3e-11d1-9c86-006008764d0e"): "msmq-Receive-journal",
1243 uuid.UUID("06bd3202-df3e-11d1-9c86-006008764d0e"): "msmq-Send",
1244 uuid.UUID("a1990816-4298-11d1-ade2-00c04fd8d5cd"): "Open-Address-Book",
1245 uuid.UUID(
1246 "1131f6ae-9c07-11d1-f79f-00c04fc2dcd2"
1247 ): "Read-Only-Replication-Secret-Synchronization",
1248 uuid.UUID("45ec5156-db7e-47bb-b53f-dbeb2d03c40f"): "Reanimate-Tombstones",
1249 uuid.UUID("0bc1554e-0a99-11d1-adbb-00c04fd8d5cd"): "Recalculate-Hierarchy",
1250 uuid.UUID(
1251 "62dd28a8-7f46-11d2-b9ad-00c04f79f805"
1252 ): "Recalculate-Security-Inheritance",
1253 uuid.UUID("ab721a56-1e2f-11d0-9819-00aa0040529b"): "Receive-As",
1254 uuid.UUID("9432c620-033c-4db7-8b58-14ef6d0bf477"): "Refresh-Group-Cache",
1255 uuid.UUID("1a60ea8d-58a6-4b20-bcdc-fb71eb8a9ff8"): "Reload-SSL-Certificate",
1256 uuid.UUID("7726b9d5-a4b4-4288-a6b2-dce952e80a7f"): "Run-Protect_Admin_Groups-Task",
1257 uuid.UUID("91d67418-0135-4acc-8d79-c08e857cfbec"): "SAM-Enumerate-Entire-Domain",
1258 uuid.UUID("ab721a54-1e2f-11d0-9819-00aa0040529b"): "Send-As",
1259 uuid.UUID("ab721a55-1e2f-11d0-9819-00aa0040529b"): "Send-To",
1260 uuid.UUID("ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501"): "Unexpire-Password",
1261 uuid.UUID(
1262 "280f369c-67c7-438e-ae98-1d46f3c6f541"
1263 ): "Update-Password-Not-Required-Bit",
1264 uuid.UUID("be2bb760-7f46-11d2-b9ad-00c04f79f805"): "Update-Schema-Cache",
1265 uuid.UUID("ab721a53-1e2f-11d0-9819-00aa0040529b"): "User-Change-Password",
1266 uuid.UUID("00299570-246d-11d0-a768-00aa006e0529"): "User-Force-Change-Password",
1267 uuid.UUID("3e0f7e18-2c7a-4c10-ba82-4d926db99a3e"): "DS-Clone-Domain-Controller",
1268 uuid.UUID("084c93a2-620d-4879-a836-f0ae47de0e89"): "DS-Read-Partition-Secrets",
1269 uuid.UUID("94825a8d-b171-4116-8146-1e34d8f54401"): "DS-Write-Partition-Secrets",
1270 uuid.UUID("4125c71f-7fac-4ff0-bcb7-f09a41325286"): "DS-Set-Owner",
1271 uuid.UUID("88a9933e-e5c8-4f2a-9dd7-2527416b8092"): "DS-Bypass-Quota",
1272 uuid.UUID("9b026da6-0d3c-465c-8bee-5199d7165cba"): "DS-Validated-Write-Computer",
1273}
1275# [MS-ADTS] sect 5.1.3.2 and
1276# https://learn.microsoft.com/en-us/windows/win32/secauthz/directory-services-access-rights
1278LDAP_DS_ACCESS_RIGHTS = {
1279 0x00000001: "CREATE_CHILD",
1280 0x00000002: "DELETE_CHILD",
1281 0x00000004: "LIST_CONTENTS",
1282 0x00000008: "WRITE_PROPERTY_EXTENDED",
1283 0x00000010: "READ_PROP",
1284 0x00000020: "WRITE_PROP",
1285 0x00000040: "DELETE_TREE",
1286 0x00000080: "LIST_OBJECT",
1287 0x00000100: "CONTROL_ACCESS",
1288 0x00010000: "DELETE",
1289 0x00020000: "READ_CONTROL",
1290 0x00040000: "WRITE_DAC",
1291 0x00080000: "WRITE_OWNER",
1292 0x00100000: "SYNCHRONIZE",
1293 0x01000000: "ACCESS_SYSTEM_SECURITY",
1294 0x80000000: "GENERIC_READ",
1295 0x40000000: "GENERIC_WRITE",
1296 0x20000000: "GENERIC_EXECUTE",
1297 0x10000000: "GENERIC_ALL",
1298}
1301# Small CLDAP Answering machine: [MS-ADTS] 6.3.3 - Ldap ping
1304class LdapPing_am(AnsweringMachine):
1305 function_name = "ldappingd"
1306 filter = "udp port 389 or 138"
1307 send_function = staticmethod(send)
1309 def parse_options(
1310 self,
1311 NetbiosDomainName="DOMAIN",
1312 DomainGuid=uuid.UUID("192bc4b3-0085-4521-83fe-062913ef59f2"),
1313 DcSiteName="Default-First-Site-Name",
1314 NetbiosComputerName="SRV1",
1315 DnsForestName=None,
1316 DnsHostName=None,
1317 src_ip=None,
1318 src_ip6=None,
1319 ):
1320 self.NetbiosDomainName = NetbiosDomainName
1321 self.DnsForestName = DnsForestName or (NetbiosDomainName + ".LOCAL")
1322 self.DomainGuid = DomainGuid
1323 self.DcSiteName = DcSiteName
1324 self.NetbiosComputerName = NetbiosComputerName
1325 self.DnsHostName = DnsHostName or (
1326 NetbiosComputerName + "." + self.DnsForestName
1327 )
1328 self.src_ip = src_ip
1329 self.src_ip6 = src_ip6
1331 def is_request(self, req):
1332 # [MS-ADTS] 6.3.3 - Example:
1333 # (&(DnsDomain=abcde.corp.microsoft.com)(Host=abcdefgh-dev)(User=abcdefgh-
1334 # dev$)(AAC=\80\00\00\00)(DomainGuid=\3b\b0\21\ca\d3\6d\d1\11\8a\7d\b8\df\b1\56\87\1f)(NtVer
1335 # =\06\00\00\00))
1336 if NBTDatagram in req:
1337 # special case: mailslot ping
1338 from scapy.layers.smb import SMBMailslot_Write, NETLOGON_SAM_LOGON_REQUEST
1340 try:
1341 return (
1342 SMBMailslot_Write in req and NETLOGON_SAM_LOGON_REQUEST in req.Data
1343 )
1344 except AttributeError:
1345 return False
1346 if CLDAP not in req or not isinstance(req.protocolOp, LDAP_SearchRequest):
1347 return False
1348 req = req.protocolOp
1349 return (
1350 req.attributes
1351 and req.attributes[0].type.val.lower() == b"netlogon"
1352 and req.filter
1353 and isinstance(req.filter.filter, LDAP_FilterAnd)
1354 and any(
1355 x.filter.attributeType.val == b"NtVer" for x in req.filter.filter.vals
1356 )
1357 )
1359 def make_reply(self, req):
1360 if NBTDatagram in req:
1361 # Special case
1362 return self.make_mailslot_ping_reply(req)
1363 if IPv6 in req:
1364 resp = IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst)
1365 else:
1366 resp = IP(dst=req[IP].src, src=self.src_ip or req[IP].dst)
1367 resp /= UDP(sport=req.dport, dport=req.sport)
1368 # get the DnsDomainName from the request
1369 try:
1370 DnsDomainName = next(
1371 x.filter.attributeValue.val
1372 for x in req.protocolOp.filter.filter.vals
1373 if x.filter.attributeType.val == b"DnsDomain"
1374 )
1375 except StopIteration:
1376 return
1377 return (
1378 resp
1379 / CLDAP(
1380 protocolOp=LDAP_SearchResponseEntry(
1381 attributes=[
1382 LDAP_PartialAttribute(
1383 values=[
1384 LDAP_AttributeValue(
1385 value=ASN1_STRING(
1386 val=bytes(
1387 NETLOGON_SAM_LOGON_RESPONSE_EX(
1388 # Mandatory fields
1389 DnsDomainName=DnsDomainName,
1390 NtVersion="V1+V5",
1391 LmNtToken=65535,
1392 Lm20Token=65535,
1393 # Below can be customized
1394 Flags=0x3F3FD,
1395 DomainGuid=self.DomainGuid,
1396 DnsForestName=self.DnsForestName,
1397 DnsHostName=self.DnsHostName,
1398 NetbiosDomainName=self.NetbiosDomainName, # noqa: E501
1399 NetbiosComputerName=self.NetbiosComputerName, # noqa: E501
1400 UserName=b".",
1401 DcSiteName=self.DcSiteName,
1402 ClientSiteName=self.DcSiteName,
1403 )
1404 )
1405 )
1406 )
1407 ],
1408 type=ASN1_STRING(b"Netlogon"),
1409 )
1410 ],
1411 ),
1412 messageID=req.messageID,
1413 user=None,
1414 )
1415 / CLDAP(
1416 protocolOp=LDAP_SearchResponseResultDone(
1417 referral=None,
1418 resultCode=0,
1419 ),
1420 messageID=req.messageID,
1421 user=None,
1422 )
1423 )
1425 def make_mailslot_ping_reply(self, req):
1426 # type: (Packet) -> Packet
1427 from scapy.layers.smb import (
1428 SMBMailslot_Write,
1429 SMB_Header,
1430 DcSockAddr,
1431 NETLOGON_SAM_LOGON_RESPONSE_EX,
1432 )
1434 resp = IP(dst=req[IP].src) / UDP(
1435 sport=req.dport,
1436 dport=req.sport,
1437 )
1438 address = self.src_ip or get_if_addr(self.optsniff.get("iface", conf.iface))
1439 resp /= (
1440 NBTDatagram(
1441 SourceName=req.DestinationName,
1442 SUFFIX1=req.SUFFIX2,
1443 DestinationName=req.SourceName,
1444 SUFFIX2=req.SUFFIX1,
1445 SourceIP=address,
1446 )
1447 / SMB_Header()
1448 / SMBMailslot_Write(
1449 Name=req.Data.MailslotName,
1450 )
1451 )
1452 NetbiosDomainName = req.DestinationName.strip()
1453 resp.Data = NETLOGON_SAM_LOGON_RESPONSE_EX(
1454 # Mandatory fields
1455 NetbiosDomainName=NetbiosDomainName,
1456 DcSockAddr=DcSockAddr(
1457 sin_addr=address,
1458 ),
1459 NtVersion="V1+V5EX+V5EX_WITH_IP",
1460 LmNtToken=65535,
1461 Lm20Token=65535,
1462 # Below can be customized
1463 Flags=0x3F3FD,
1464 DomainGuid=self.DomainGuid,
1465 DnsForestName=self.DnsForestName,
1466 DnsDomainName=self.DnsForestName,
1467 DnsHostName=self.DnsHostName,
1468 NetbiosComputerName=self.NetbiosComputerName,
1469 DcSiteName=self.DcSiteName,
1470 ClientSiteName=self.DcSiteName,
1471 )
1472 return resp
1475_located_dc = collections.namedtuple("LocatedDC", ["ip", "samlogon"])
1476_dclocatorcache = conf.netcache.new_cache("dclocator", 600)
1479@conf.commands.register
1480def dclocator(
1481 realm: str,
1482 site: str = "",
1483 qtype: str = "A",
1484 mode: str = "ldap",
1485 port: int = None,
1486 timeout: int = 1,
1487 NtVersion: int = None,
1488 debug: int = 0,
1489):
1490 """
1491 Perform a DC Locator as per [MS-ADTS] sect 6.3.6 or RFC4120.
1493 :param realm: the kerberos realm to locate
1494 :param site: if provided, a Site name
1495 :param mode: Detect if a server is up and joinable thanks to one of:
1497 - 'nocheck': Do not check that servers are online.
1498 - 'ldap': Use the LDAP ping (CLDAP) per [MS-ADTS]. Default.
1499 This will however not work with MIT Kerberos servers.
1500 - 'connect': connect to specified port to test the connection.
1502 :param mode: in connect mode, the port to connect to. (e.g. 88)
1503 :param debug: print debug logs
1505 This is cached in conf.netcache.dclocator.
1506 """
1507 if NtVersion is None:
1508 # Windows' default
1509 NtVersion = (
1510 0x00000002 # V5
1511 | 0x00000004 # V5EX
1512 | 0x00000010 # V5EX_WITH_CLOSEST_SITE
1513 | 0x01000000 # AVOID_NT4EMUL
1514 | 0x20000000 # IP
1515 )
1516 # Check cache
1517 cache_ident = ";".join([realm, qtype, mode, str(NtVersion), site]).lower()
1518 if cache_ident in _dclocatorcache:
1519 return _dclocatorcache[cache_ident]
1520 # Perform DNS-Based discovery (6.3.6.1)
1521 # 1. SRV records
1522 if site:
1523 qname = ("_kerberos._tcp.%s._sites.dc._msdcs.%s" % (site, realm)).lower()
1524 else:
1525 qname = "_kerberos._tcp.dc._msdcs.%s" % realm.lower()
1526 if debug:
1527 log_runtime.info("DC Locator: requesting SRV for '%s' ..." % qname)
1528 try:
1529 hosts = [
1530 x.target
1531 for x in dns_resolve(
1532 qname=qname,
1533 qtype="SRV",
1534 timeout=timeout,
1535 )
1536 ]
1537 except TimeoutError:
1538 raise TimeoutError("Resolution of %s timed out" % qname)
1539 if not hosts:
1540 raise ValueError("No DNS record found for %s" % qname)
1541 elif debug:
1542 log_runtime.info(
1543 "DC Locator: got %s. Resolving %s records ..." % (hosts, qtype)
1544 )
1545 # 2. A records
1546 ips = []
1547 for host in hosts:
1548 arec = dns_resolve(
1549 qname=host,
1550 qtype=qtype,
1551 timeout=timeout,
1552 )
1553 if arec:
1554 ips.extend(x.rdata for x in arec)
1555 if not ips:
1556 raise ValueError("Could not get any %s records for %s" % (qtype, hosts))
1557 elif debug:
1558 log_runtime.info("DC Locator: got %s . Mode: %s" % (ips, mode))
1559 # Pick first online host. We have three options
1560 if mode == "nocheck":
1561 # Don't check anything. Not recommended
1562 return _located_dc(ips[0], None)
1563 elif mode == "connect":
1564 assert port is not None, "Must provide a port in connect mode !"
1565 # Compatibility with MIT Kerberos servers
1566 for ip in ips: # TODO: "addresses in weighted random order [RFC2782]"
1567 if debug:
1568 log_runtime.info("DC Locator: connecting to %s on %s ..." % (ip, port))
1569 try:
1570 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1571 sock.settimeout(timeout)
1572 sock.connect((ip, port))
1573 # Success
1574 result = _located_dc(ip, None)
1575 # Cache
1576 _dclocatorcache[cache_ident] = result
1577 return result
1578 except OSError:
1579 # Host timed out, No route to host, etc.
1580 if debug:
1581 log_runtime.info("DC Locator: %s timed out." % ip)
1582 continue
1583 finally:
1584 sock.close()
1585 raise ValueError("No host was reachable on port %s among %s" % (port, ips))
1586 elif mode == "ldap":
1587 # Real 'LDAP Ping' per [MS-ADTS]
1588 for ip in ips: # TODO: "addresses in weighted random order [RFC2782]"
1589 if debug:
1590 log_runtime.info("DC Locator: LDAP Ping %s on ..." % ip)
1591 try:
1592 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1593 sock.settimeout(timeout)
1594 sock.connect((ip, 389))
1595 sock = SimpleSocket(sock, CLDAP)
1596 pkt = sock.sr1(
1597 CLDAP(
1598 protocolOp=LDAP_SearchRequest(
1599 filter=LDAP_Filter(
1600 filter=LDAP_FilterAnd(
1601 vals=[
1602 LDAP_Filter(
1603 filter=LDAP_FilterEqual(
1604 attributeType=ASN1_STRING(b"DnsDomain"),
1605 attributeValue=ASN1_STRING(realm),
1606 )
1607 ),
1608 LDAP_Filter(
1609 filter=LDAP_FilterEqual(
1610 attributeType=ASN1_STRING(b"NtVer"),
1611 attributeValue=ASN1_STRING(
1612 struct.pack("<I", NtVersion)
1613 ),
1614 )
1615 ),
1616 ]
1617 )
1618 ),
1619 attributes=[
1620 LDAP_SearchRequestAttribute(
1621 type=ASN1_STRING(b"Netlogon")
1622 )
1623 ],
1624 ),
1625 user=None,
1626 ),
1627 timeout=timeout,
1628 verbose=0,
1629 )
1630 if pkt:
1631 # Check if we have a search response
1632 response = None
1633 if isinstance(pkt.protocolOp, LDAP_SearchResponseEntry):
1634 try:
1635 response = next(
1636 NETLOGON(x.values[0].value.val)
1637 for x in pkt.protocolOp.attributes
1638 if x.type.val.lower() == b"netlogon"
1639 )
1640 except StopIteration:
1641 pass
1642 result = _located_dc(ip, response)
1643 # Cache
1644 _dclocatorcache[cache_ident] = result
1645 return result
1646 except OSError:
1647 # Host timed out, No route to host, etc.
1648 if debug:
1649 log_runtime.info("DC Locator: %s timed out." % ip)
1650 continue
1651 finally:
1652 sock.close()
1653 raise ValueError("No LDAP ping succeeded on any of %s. Try another mode?" % ips)
1656#####################
1657# Basic LDAP client #
1658#####################
1661class LDAP_BIND_MECHS(StrEnum):
1662 NONE = "ANONYMOUS"
1663 SIMPLE = "SIMPLE"
1664 SASL_GSSAPI = "GSSAPI"
1665 SASL_GSS_SPNEGO = "GSS-SPNEGO"
1666 SASL_EXTERNAL = "EXTERNAL"
1667 SASL_DIGEST_MD5 = "DIGEST-MD5"
1668 # [MS-ADTS] extension
1669 SICILY = "SICILY"
1672class LDAP_SASL_GSSAPI_SsfCap(Packet):
1673 """
1674 RFC2222 sect 7.2.1 and 7.2.2 negotiate token
1675 """
1677 fields_desc = [
1678 FlagsField(
1679 "supported_security_layers",
1680 0,
1681 -8,
1682 {
1683 # https://github.com/cyrusimap/cyrus-sasl/blob/7e2feaeeb2e37d38cb5fa957d0e8a599ced22612/plugins/gssapi.c#L221
1684 0x01: "NONE",
1685 0x02: "INTEGRITY",
1686 0x04: "CONFIDENTIALITY",
1687 },
1688 ),
1689 ThreeBytesField("max_output_token_size", 0),
1690 ]
1693class LDAP_SASL_Buffer(Packet):
1694 """
1695 RFC 4422 sect 3.7
1696 """
1698 # "Each buffer of protected data is transferred over the underlying
1699 # transport connection as a sequence of octets prepended with a four-
1700 # octet field in network byte order that represents the length of the
1701 # buffer."
1703 fields_desc = [
1704 FieldLenField("BufferLength", None, fmt="!I", length_of="Buffer"),
1705 _GSSAPI_Field("Buffer", LDAP),
1706 ]
1708 def hashret(self):
1709 return b"ldap"
1711 def answers(self, other):
1712 return isinstance(other, LDAP_SASL_Buffer)
1714 @classmethod
1715 def tcp_reassemble(cls, data, *args, **kwargs):
1716 if len(data) < 4:
1717 return None
1718 if data[0] == 0x30:
1719 # Add a heuristic to detect LDAP errors
1720 try:
1721 xlen, x = BER_len_dec(BER_id_dec(data)[1])
1722 except (BER_Decoding_Error, IndexError):
1723 return None
1724 if xlen and xlen == len(x):
1725 return LDAP(data)
1726 # Check BufferLength
1727 length = struct.unpack("!I", data[:4])[0] + 4
1728 if len(data) >= length:
1729 return cls(data)
1732class LDAP_Exception(RuntimeError):
1733 __slots__ = ["resultCode", "diagnosticMessage"]
1735 def __init__(self, *args, **kwargs):
1736 resp = kwargs.pop("resp", None)
1737 if resp:
1738 self.resultCode = resp.protocolOp.sprintf("%resultCode%")
1739 self.diagnosticMessage = resp.protocolOp.diagnosticMessage.val.rstrip(
1740 b"\x00"
1741 ).decode(errors="backslashreplace")
1742 else:
1743 self.resultCode = kwargs.pop("resultCode", None)
1744 self.diagnosticMessage = kwargs.pop("diagnosticMessage", None)
1745 super(LDAP_Exception, self).__init__(*args, **kwargs)
1746 # If there's a 'data' string argument, attempt to parse the error code.
1747 try:
1748 m = re.match(r"(\d+): LdapErr.*", self.diagnosticMessage)
1749 if m:
1750 errstr = m.group(1)
1751 err = int(errstr, 16)
1752 if err in STATUS_ERREF:
1753 self.diagnosticMessage = self.diagnosticMessage.replace(
1754 errstr, errstr + " (%s)" % STATUS_ERREF[err], 1
1755 )
1756 except ValueError:
1757 pass
1758 # Add note if this exception is raised
1759 self.add_note(self.diagnosticMessage)
1762class LDAP_Client(object):
1763 """
1764 A basic LDAP client
1766 The complete documentation is available at
1767 https://scapy.readthedocs.io/en/latest/layers/ldap.html
1769 Example 1 - SICILY - NTLM (with encryption)::
1771 client = LDAP_Client()
1772 client.connect("192.168.0.100")
1773 ssp = NTLMSSP(UPN="Administrator", PASSWORD="Password1!")
1774 client.bind(
1775 LDAP_BIND_MECHS.SICILY,
1776 ssp=ssp,
1777 encrypt=True,
1778 )
1780 Example 2 - SASL_GSSAPI - Kerberos (with signing)::
1782 client = LDAP_Client()
1783 client.connect("192.168.0.100")
1784 ssp = KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!",
1785 SPN="ldap/dc1.domain.local")
1786 client.bind(
1787 LDAP_BIND_MECHS.SASL_GSSAPI,
1788 ssp=ssp,
1789 sign=True,
1790 )
1792 Example 3 - SASL_GSS_SPNEGO - NTLM / Kerberos::
1794 client = LDAP_Client()
1795 client.connect("192.168.0.100")
1796 ssp = SPNEGOSSP([
1797 NTLMSSP(UPN="Administrator", PASSWORD="Password1!"),
1798 KerberosSSP(UPN="Administrator@domain.local", PASSWORD="Password1!",
1799 SPN="ldap/dc1.domain.local"),
1800 ])
1801 client.bind(
1802 LDAP_BIND_MECHS.SASL_GSS_SPNEGO,
1803 ssp=ssp,
1804 )
1806 Example 4 - Simple bind over TLS::
1808 client = LDAP_Client()
1809 client.connect("192.168.0.100", use_ssl=True)
1810 client.bind(
1811 LDAP_BIND_MECHS.SIMPLE,
1812 simple_username="Administrator",
1813 simple_password="Password1!",
1814 )
1815 """
1817 def __init__(
1818 self,
1819 verb=True,
1820 ):
1821 self.sock = None
1822 self.host = None
1823 self.verb = verb
1824 self.ssl = False
1825 self.sslcontext = None
1826 self.ssp = None
1827 self.sspcontext = None
1828 self.encrypt = False
1829 self.sign = False
1830 # Session status
1831 self.sasl_wrap = False
1832 self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS
1833 self.bound = False
1834 self.messageID = 0
1836 def connect(
1837 self,
1838 host,
1839 port=None,
1840 use_ssl=False,
1841 sslcontext=None,
1842 sni=None,
1843 no_check_certificate=False,
1844 timeout=5,
1845 ):
1846 """
1847 Initiate a connection
1849 :param host: the IP or hostname to connect to.
1850 :param port: the port to connect to. (Default: 389 or 636)
1852 :param use_ssl: whether to use LDAPS or not. (Default: False)
1853 :param sslcontext: an optional SSLContext to use.
1854 :param sni: (optional) specify the SNI to use if LDAPS, otherwise use ip.
1855 :param no_check_certificate: with SSL, do not check the certificate
1856 """
1857 self.ssl = use_ssl
1858 self.sslcontext = sslcontext
1859 self.timeout = timeout
1860 self.host = host
1862 if port is None:
1863 if self.ssl:
1864 port = 636
1865 else:
1866 port = 389
1868 # Create and configure socket
1869 sock = socket.socket()
1870 sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
1871 sock.settimeout(timeout)
1873 # Connect
1874 if self.verb:
1875 print(
1876 "\u2503 Connecting to %s on port %s%s..."
1877 % (
1878 host,
1879 port,
1880 " with SSL" if self.ssl else "",
1881 )
1882 )
1883 sock.connect((host, port))
1884 if self.verb:
1885 print(
1886 conf.color_theme.green(
1887 "\u2514 Connected from %s" % repr(sock.getsockname())
1888 )
1889 )
1891 # For SSL, build and apply SSLContext
1892 if self.ssl:
1893 if self.sslcontext is None:
1894 if no_check_certificate:
1895 context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
1896 context.check_hostname = False
1897 context.verify_mode = ssl.CERT_NONE
1898 else:
1899 context = ssl.create_default_context()
1900 else:
1901 context = self.sslcontext
1902 sock = context.wrap_socket(sock, server_hostname=sni or host)
1904 # Wrap the socket in a Scapy socket
1905 if self.ssl:
1906 # Compute the channel binding token (CBT)
1907 self.chan_bindings = GssChannelBindings.fromssl(
1908 ChannelBindingType.TLS_SERVER_END_POINT,
1909 sslsock=sock,
1910 )
1912 self.sock = SSLStreamSocket(sock, LDAP)
1913 else:
1914 self.sock = StreamSocket(sock, LDAP)
1916 def sr1(self, protocolOp, controls: List[LDAP_Control] = None, **kwargs):
1917 self.messageID += 1
1918 if self.verb:
1919 print(conf.color_theme.opening(">> %s" % protocolOp.__class__.__name__))
1921 # Build packet
1922 pkt = LDAP(
1923 messageID=self.messageID,
1924 protocolOp=protocolOp,
1925 Controls=controls,
1926 )
1928 # If signing / encryption is used, apply
1929 if self.sasl_wrap:
1930 pkt = LDAP_SASL_Buffer(
1931 Buffer=self.ssp.GSS_Wrap(
1932 self.sspcontext,
1933 bytes(pkt),
1934 conf_req_flag=self.encrypt,
1935 # LDAP on Windows doesn't use SECBUFFER_PADDING, which
1936 # isn't supported by GSS_WrapEx. We add our own flag to
1937 # tell it.
1938 qop_req=GSS_QOP_REQ_FLAGS.GSS_S_NO_SECBUFFER_PADDING,
1939 )
1940 )
1942 # Send / Receive
1943 resp = self.sock.sr1(
1944 pkt,
1945 verbose=0,
1946 **kwargs,
1947 )
1948 # Check for unsolicited notification
1949 if resp and LDAP in resp and resp[LDAP].unsolicited:
1950 if self.verb:
1951 resp.show()
1952 print(conf.color_theme.fail("! Got unsolicited notification."))
1953 return resp
1955 # If signing / encryption is used, unpack
1956 if self.sasl_wrap:
1957 if resp.Buffer:
1958 resp = LDAP(
1959 self.ssp.GSS_Unwrap(
1960 self.sspcontext,
1961 resp.Buffer,
1962 )
1963 )
1964 if not resp.unsolicited and resp.messageID != self.messageID:
1965 raise ValueError(
1966 "LDAP response message ID does not match request !"
1967 )
1968 else:
1969 resp = None
1971 # Verbose display
1972 if self.verb:
1973 if not resp:
1974 print(conf.color_theme.fail("! Bad response."))
1975 return
1976 else:
1977 print(
1978 conf.color_theme.success(
1979 "<< %s"
1980 % (
1981 resp.protocolOp.__class__.__name__
1982 if LDAP in resp
1983 else resp.__class__.__name__
1984 )
1985 )
1986 )
1987 return resp
1989 def bind(
1990 self,
1991 mech,
1992 ssp=None,
1993 sign: Optional[bool] = None,
1994 encrypt: Optional[bool] = None,
1995 simple_username=None,
1996 simple_password=None,
1997 ):
1998 """
1999 Send Bind request.
2001 :param mech: one of LDAP_BIND_MECHS
2002 :param ssp: the SSP object to use for binding
2004 :param sign: request signing when binding
2005 :param encrypt: request encryption when binding
2007 :
2008 This acts differently based on the :mech: provided during initialization.
2009 """
2010 # Bind default values: if NTLM then encrypt, else sign unless anonymous/simple
2011 if encrypt is None:
2012 encrypt = mech == LDAP_BIND_MECHS.SICILY
2013 if sign is None and not encrypt:
2014 sign = mech not in [LDAP_BIND_MECHS.NONE, LDAP_BIND_MECHS.SIMPLE]
2016 # Store and check consistency
2017 self.mech = mech
2018 self.ssp = ssp # type: SSP
2019 self.sign = sign
2020 self.encrypt = encrypt
2021 self.sspcontext = None
2023 if mech is None or not isinstance(mech, LDAP_BIND_MECHS):
2024 raise ValueError(
2025 "'mech' attribute is required and must be one of LDAP_BIND_MECHS."
2026 )
2028 if mech == LDAP_BIND_MECHS.SASL_GSSAPI:
2029 from scapy.layers.kerberos import KerberosSSP
2031 if not isinstance(self.ssp, KerberosSSP):
2032 raise ValueError("Only raw KerberosSSP is supported with SASL_GSSAPI !")
2033 elif mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO:
2034 from scapy.layers.spnego import SPNEGOSSP
2036 if not isinstance(self.ssp, SPNEGOSSP):
2037 if WINDOWS:
2038 from scapy.arch.windows.sspi import WinSSP
2040 if not isinstance(self.ssp, WinSSP):
2041 raise ValueError(
2042 "Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !"
2043 )
2044 else:
2045 raise ValueError(
2046 "Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !"
2047 )
2048 elif mech == LDAP_BIND_MECHS.SICILY:
2049 from scapy.layers.ntlm import NTLMSSP
2051 if not isinstance(self.ssp, NTLMSSP):
2052 raise ValueError("Only raw NTLMSSP is supported with SICILY !")
2053 if self.sign and not self.encrypt:
2054 raise ValueError(
2055 "NTLM on LDAP does not support signing without encryption !"
2056 )
2057 elif mech in [LDAP_BIND_MECHS.NONE, LDAP_BIND_MECHS.SIMPLE]:
2058 if self.sign or self.encrypt:
2059 raise ValueError("Cannot use 'sign' or 'encrypt' with NONE or SIMPLE !")
2060 else:
2061 raise ValueError("Mech %s is still unimplemented !" % mech)
2063 if self.ssp is not None and mech in [
2064 LDAP_BIND_MECHS.NONE,
2065 LDAP_BIND_MECHS.SIMPLE,
2066 ]:
2067 raise ValueError("%s cannot be used with a ssp !" % mech.value)
2069 # Now perform the bind, depending on the mech
2070 if self.mech == LDAP_BIND_MECHS.SIMPLE:
2071 # Simple binding
2072 resp = self.sr1(
2073 LDAP_BindRequest(
2074 bind_name=ASN1_STRING(simple_username or ""),
2075 authentication=LDAP_Authentication_simple(
2076 simple_password or "",
2077 ),
2078 )
2079 )
2080 if (
2081 LDAP not in resp
2082 or not isinstance(resp.protocolOp, LDAP_BindResponse)
2083 or resp.protocolOp.resultCode != 0
2084 ):
2085 raise LDAP_Exception(
2086 "LDAP simple bind failed !",
2087 resp=resp,
2088 )
2089 status = GSS_S_COMPLETE
2090 elif self.mech == LDAP_BIND_MECHS.SICILY:
2091 # [MS-ADTS] sect 5.1.1.1.3
2092 # 1. Package Discovery
2093 resp = self.sr1(
2094 LDAP_BindRequest(
2095 bind_name=ASN1_STRING(b""),
2096 authentication=LDAP_Authentication_sicilyPackageDiscovery(b""),
2097 )
2098 )
2099 if resp.protocolOp.resultCode != 0:
2100 raise LDAP_Exception(
2101 "Sicily package discovery failed !",
2102 resp=resp,
2103 )
2104 # 2. First exchange: Negotiate
2105 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
2106 self.sspcontext,
2107 target_name="ldap/" + self.host,
2108 req_flags=(
2109 GSS_C_FLAGS.GSS_C_REPLAY_FLAG
2110 | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG
2111 | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
2112 | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.sign else 0)
2113 | (GSS_C_FLAGS.GSS_C_CONF_FLAG if self.encrypt else 0)
2114 ),
2115 )
2116 resp = self.sr1(
2117 LDAP_BindRequest(
2118 bind_name=ASN1_STRING(b"NTLM"),
2119 authentication=LDAP_Authentication_sicilyNegotiate(
2120 bytes(token),
2121 ),
2122 )
2123 )
2124 val = resp.protocolOp.serverCreds
2125 if not val:
2126 raise LDAP_Exception(
2127 "Sicily negotiate failed !",
2128 resp=resp,
2129 )
2130 # 3. Second exchange: Response
2131 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
2132 self.sspcontext,
2133 input_token=GSSAPI_BLOB(val),
2134 target_name="ldap/" + self.host,
2135 chan_bindings=self.chan_bindings,
2136 )
2137 resp = self.sr1(
2138 LDAP_BindRequest(
2139 bind_name=ASN1_STRING(b"NTLM"),
2140 authentication=LDAP_Authentication_sicilyResponse(
2141 bytes(token),
2142 ),
2143 )
2144 )
2145 if resp.protocolOp.resultCode != 0:
2146 raise LDAP_Exception(
2147 "Sicily response failed !",
2148 resp=resp,
2149 )
2150 elif self.mech in [
2151 LDAP_BIND_MECHS.SASL_GSS_SPNEGO,
2152 LDAP_BIND_MECHS.SASL_GSSAPI,
2153 ]:
2154 # GSSAPI or SPNEGO
2155 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
2156 self.sspcontext,
2157 target_name="ldap/" + self.host,
2158 req_flags=(
2159 # Required flags for GSSAPI: RFC4752 sect 3.1
2160 GSS_C_FLAGS.GSS_C_REPLAY_FLAG
2161 | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG
2162 | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
2163 | (GSS_C_FLAGS.GSS_C_INTEG_FLAG if self.sign else 0)
2164 | (GSS_C_FLAGS.GSS_C_CONF_FLAG if self.encrypt else 0)
2165 ),
2166 chan_bindings=self.chan_bindings,
2167 )
2168 if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]:
2169 raise RuntimeError(
2170 "%s: GSS_Init_sec_context failed with %s !"
2171 % (self.mech.name, repr(status)),
2172 )
2173 while token:
2174 resp = self.sr1(
2175 LDAP_BindRequest(
2176 bind_name=ASN1_STRING(b""),
2177 authentication=LDAP_Authentication_SaslCredentials(
2178 mechanism=ASN1_STRING(self.mech.value),
2179 credentials=ASN1_STRING(bytes(token)),
2180 ),
2181 )
2182 )
2183 if not isinstance(resp.protocolOp, LDAP_BindResponse):
2184 raise LDAP_Exception(
2185 "%s bind failed !" % self.mech.name,
2186 resp=resp,
2187 )
2188 val = resp.protocolOp.serverSaslCredsData
2189 if resp.protocolOp.resultCode not in [0, 14]:
2190 raise LDAP_Exception(
2191 "SASL authentication failed !",
2192 resp=resp,
2193 )
2194 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
2195 self.sspcontext,
2196 input_token=GSSAPI_BLOB(val),
2197 target_name="ldap/" + self.host,
2198 chan_bindings=self.chan_bindings,
2199 )
2200 else:
2201 status = GSS_S_COMPLETE
2202 if status != GSS_S_COMPLETE:
2203 raise RuntimeError(
2204 "%s: GSS_Init_sec_context failed with %s !"
2205 % (self.mech.name, repr(status)),
2206 )
2207 elif self.mech == LDAP_BIND_MECHS.SASL_GSSAPI:
2208 # GSSAPI has 2 extra exchanges
2209 # https://datatracker.ietf.org/doc/html/rfc2222#section-7.2.1
2210 resp = self.sr1(
2211 LDAP_BindRequest(
2212 bind_name=ASN1_STRING(b""),
2213 authentication=LDAP_Authentication_SaslCredentials(
2214 mechanism=ASN1_STRING(self.mech.value),
2215 credentials=None,
2216 ),
2217 )
2218 )
2219 # Parse server-supported layers
2220 saslOptions = LDAP_SASL_GSSAPI_SsfCap(
2221 self.ssp.GSS_Unwrap(
2222 self.sspcontext,
2223 GSSAPI_BLOB_SIGNATURE(resp.protocolOp.serverSaslCredsData),
2224 )
2225 )
2226 if self.sign and not saslOptions.supported_security_layers.INTEGRITY:
2227 raise RuntimeError("GSSAPI SASL failed to negotiate INTEGRITY !")
2228 if (
2229 self.encrypt
2230 and not saslOptions.supported_security_layers.CONFIDENTIALITY
2231 ):
2232 raise RuntimeError("GSSAPI SASL failed to negotiate CONFIDENTIALITY !")
2233 # Announce client-supported layers
2234 saslOptions = LDAP_SASL_GSSAPI_SsfCap(
2235 supported_security_layers=(
2236 "+".join(
2237 (["INTEGRITY"] if self.sign else [])
2238 + (["CONFIDENTIALITY"] if self.encrypt else [])
2239 )
2240 if (self.sign or self.encrypt)
2241 else "NONE"
2242 ),
2243 # Same as server
2244 max_output_token_size=saslOptions.max_output_token_size,
2245 )
2246 resp = self.sr1(
2247 LDAP_BindRequest(
2248 bind_name=ASN1_STRING(b""),
2249 authentication=LDAP_Authentication_SaslCredentials(
2250 mechanism=ASN1_STRING(self.mech.value),
2251 credentials=self.ssp.GSS_Wrap(
2252 self.sspcontext,
2253 bytes(saslOptions),
2254 # We still haven't finished negotiating
2255 conf_req_flag=False,
2256 ),
2257 ),
2258 )
2259 )
2260 if resp.protocolOp.resultCode != 0:
2261 raise LDAP_Exception(
2262 "GSSAPI SASL failed to negotiate client security flags !",
2263 resp=resp,
2264 )
2266 # If we use SPNEGO and NTLMSSP was used, understand we can't use sign
2267 if self.mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO:
2268 from scapy.layers.ntlm import NTLMSSP
2270 if isinstance(self.sspcontext.ssp, NTLMSSP):
2271 if self.sign and not self.encrypt:
2272 raise ValueError(
2273 "NTLM with GSS-SPNEGO cannot sign without encryption !"
2274 )
2275 self.sign = False
2277 # SASL wrapping is now available.
2278 self.sasl_wrap = self.encrypt or self.sign
2279 if self.sasl_wrap:
2280 self.sock.closed = True # prevent closing by marking it as already closed.
2281 self.sock = StreamSocket(self.sock.ins, LDAP_SASL_Buffer)
2283 # Success.
2284 if self.verb:
2285 print("%s bind succeeded !" % self.mech.name)
2286 self.bound = True
2288 _TEXT_REG = re.compile(b"^[%s]*$" % re.escape(string.printable.encode()))
2290 def search(
2291 self,
2292 baseObject: str = "",
2293 filter: str = "",
2294 scope=0,
2295 derefAliases=0,
2296 sizeLimit=300000,
2297 timeLimit=3000,
2298 attrsOnly=0,
2299 attributes: List[str] = [],
2300 controls: List[LDAP_Control] = [],
2301 ) -> Dict[str, List[Any]]:
2302 """
2303 Perform a LDAP search.
2305 :param baseObject: the dn of the base object to search in.
2306 :param filter: the filter to apply to the search (currently unsupported)
2307 :param scope: 0=baseObject, 1=singleLevel, 2=wholeSubtree
2308 """
2309 if baseObject == "rootDSE":
2310 baseObject = ""
2311 if filter:
2312 filter = LDAP_Filter.from_rfc2254_string(filter)
2313 else:
2314 # Default filter: (objectClass=*)
2315 filter = LDAP_Filter(
2316 filter=LDAP_FilterPresent(
2317 present=ASN1_STRING(b"objectClass"),
2318 )
2319 )
2320 # we loop as we might need more than one packet thanks to paging
2321 cookie = b""
2322 entries = {}
2323 while True:
2324 resp = self.sr1(
2325 LDAP_SearchRequest(
2326 filter=filter,
2327 attributes=[
2328 LDAP_SearchRequestAttribute(type=ASN1_STRING(attr))
2329 for attr in attributes
2330 ],
2331 baseObject=ASN1_STRING(baseObject),
2332 scope=ASN1_ENUMERATED(scope),
2333 derefAliases=ASN1_ENUMERATED(derefAliases),
2334 sizeLimit=ASN1_INTEGER(sizeLimit),
2335 timeLimit=ASN1_INTEGER(timeLimit),
2336 attrsOnly=ASN1_BOOLEAN(attrsOnly),
2337 ),
2338 controls=(
2339 controls
2340 + (
2341 [
2342 # This control is only usable when bound.
2343 LDAP_Control(
2344 controlType="1.2.840.113556.1.4.319",
2345 criticality=True,
2346 controlValue=LDAP_realSearchControlValue(
2347 size=100, # paging to 100 per 100
2348 cookie=cookie,
2349 ),
2350 )
2351 ]
2352 if self.bound
2353 else []
2354 )
2355 ),
2356 timeout=self.timeout,
2357 )
2358 if LDAP_SearchResponseResultDone not in resp:
2359 resp.show()
2360 raise TimeoutError("Search timed out.")
2361 # Now, reassemble the results
2363 def _s(x):
2364 try:
2365 return x.decode()
2366 except UnicodeDecodeError:
2367 return x
2369 def _ssafe(x):
2370 if self._TEXT_REG.match(x):
2371 return x.decode()
2372 else:
2373 return x
2375 # For each individual packet response
2376 while resp:
2377 # Find all 'LDAP' layers
2378 if LDAP not in resp:
2379 log_runtime.warning("Invalid response: %s", repr(resp))
2380 break
2381 if LDAP_SearchResponseEntry in resp.protocolOp:
2382 attrs = {
2383 _s(attr.type.val): [_ssafe(x.value.val) for x in attr.values]
2384 for attr in resp.protocolOp.attributes
2385 }
2386 entries[_s(resp.protocolOp.objectName.val)] = attrs
2387 elif LDAP_SearchResponseResultDone in resp.protocolOp:
2388 resultCode = resp.protocolOp.resultCode
2389 if resultCode != 0x0: # != success
2390 log_runtime.warning(
2391 resp.protocolOp.sprintf("Got response: %resultCode%")
2392 )
2393 raise LDAP_Exception(
2394 "LDAP search failed !",
2395 resp=resp,
2396 )
2397 else:
2398 # success
2399 if resp.Controls:
2400 # We have controls back
2401 realSearchControlValue = next(
2402 (
2403 c.controlValue
2404 for c in resp.Controls
2405 if isinstance(
2406 c.controlValue, LDAP_realSearchControlValue
2407 )
2408 ),
2409 None,
2410 )
2411 if realSearchControlValue is not None:
2412 # has paging !
2413 cookie = realSearchControlValue.cookie.val
2414 break
2415 break
2416 resp = resp.payload
2417 # If we have a cookie, continue
2418 if not cookie:
2419 break
2420 return entries
2422 def modify(
2423 self,
2424 object: str,
2425 changes: List[LDAP_ModifyRequestChange],
2426 controls: List[LDAP_Control] = [],
2427 ) -> None:
2428 """
2429 Perform a LDAP modify request.
2431 :returns:
2432 """
2433 resp = self.sr1(
2434 LDAP_ModifyRequest(
2435 object=object,
2436 changes=changes,
2437 ),
2438 controls=controls,
2439 timeout=self.timeout,
2440 )
2441 if (
2442 LDAP_ModifyResponse not in resp.protocolOp
2443 or resp.protocolOp.resultCode != 0
2444 ):
2445 raise LDAP_Exception(
2446 "LDAP modify failed !",
2447 resp=resp,
2448 )
2450 def add(
2451 self,
2452 entry: str,
2453 attributes: Union[Dict[str, List[Any]], List[ASN1_Packet]],
2454 controls: List[LDAP_Control] = [],
2455 ):
2456 """
2457 Perform a LDAP add request.
2459 :param attributes: the attributes to add. We support two formats:
2460 - a list of LDAP_Attribute (or LDAP_PartialAttribute)
2461 - a dict following {attribute: [list of values]}
2463 :returns:
2464 """
2465 # We handle the two cases in the type of attributes
2466 if isinstance(attributes, dict):
2467 attributes = [
2468 LDAP_Attribute(
2469 type=ASN1_STRING(k),
2470 values=[
2471 LDAP_AttributeValue(
2472 value=ASN1_STRING(x),
2473 )
2474 for x in v
2475 ],
2476 )
2477 for k, v in attributes.items()
2478 ]
2480 resp = self.sr1(
2481 LDAP_AddRequest(
2482 entry=ASN1_STRING(entry),
2483 attributes=attributes,
2484 ),
2485 controls=controls,
2486 timeout=self.timeout,
2487 )
2488 if LDAP_AddResponse not in resp.protocolOp or resp.protocolOp.resultCode != 0:
2489 raise LDAP_Exception(
2490 "LDAP add failed !",
2491 resp=resp,
2492 )
2494 def modifydn(
2495 self,
2496 entry: str,
2497 newdn: str,
2498 deleteoldrdn=True,
2499 controls: List[LDAP_Control] = [],
2500 ):
2501 """
2502 Perform a LDAP modify DN request.
2504 ..note:: This functions calculates the relative DN and superior required for
2505 LDAP ModifyDN automatically.
2507 :param entry: the DN of the entry to rename.
2508 :param newdn: the new FULL DN of the entry.
2509 :returns:
2510 """
2511 # RFC4511 sect 4.9
2512 # Calculate the newrdn (relative DN) and superior
2513 newrdn, newSuperior = newdn.split(",", 1)
2514 _, cur_superior = entry.split(",", 1)
2515 # If the superior hasn't changed, don't update it.
2516 if cur_superior == newSuperior:
2517 newSuperior = None
2518 # Send the request
2519 resp = self.sr1(
2520 LDAP_ModifyDNRequest(
2521 entry=entry,
2522 newrdn=newrdn,
2523 newSuperior=newSuperior,
2524 deleteoldrdn=deleteoldrdn,
2525 ),
2526 controls=controls,
2527 timeout=self.timeout,
2528 )
2529 if (
2530 LDAP_ModifyDNResponse not in resp.protocolOp
2531 or resp.protocolOp.resultCode != 0
2532 ):
2533 raise LDAP_Exception(
2534 "LDAP modify failed !",
2535 resp=resp,
2536 )
2538 def close(self):
2539 if self.verb:
2540 print("X Connection closed\n")
2541 self.sock.close()
2542 self.bound = False
2543 self.sspcontext = None