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