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