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