1+ import socket
2+ import ssl
3+ import asyncio
4+ import hashlib
5+ import random
6+
7+ from msldap .connection import MSLDAPClientConnection
8+ from msldap .commons .target import MSLDAPTarget
9+
10+ from asyauth .common .constants import asyauthSecret
11+ from asyauth .common .credentials .ntlm import NTLMCredential
12+ from asyauth .common .credentials .kerberos import KerberosCredential
13+
14+ from asysocks .unicomm .common .target import UniTarget , UniProto
15+ import contextlib
16+
17+
18+ class NXCModule :
19+ """
20+ Checks whether LDAP signing and LDAPS channel binding are required and/or enforced.
21+
22+ Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
23+ Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
24+ """
25+ name = "ldap-checker"
26+ description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced"
27+ supported_protocols = ["ldap" ]
28+ opsec_safe = True
29+ multiple_hosts = True
30+
31+ def options (self , context , module_options ):
32+ """No options available."""
33+
34+ # Conduct a bind to LDAPS and determine if channel
35+ # binding is enforced based on the contents of potential
36+ # errors returned. This can be determined unauthenticated,
37+ # because the error indicating channel binding enforcement
38+ # will be returned regardless of a successful LDAPS bind.
39+ async def run_ldaps_noEPA (self , context , connection , target , credential ):
40+ try :
41+ client = MSLDAPClientConnection (target , credential )
42+ _ , err = await client .connect ()
43+ if err :
44+ context .log .debug (f"Error connecting to { connection .domain } : { err } " )
45+ return None
46+
47+ client .cb_data = None
48+ _ , err = await client .bind ()
49+ if err and "data 80090346" in str (err ):
50+ return True # -> channel binding IS enforced
51+ elif err and "data 52e" in str (err ):
52+ return False # -> channel binding not enforced
53+ elif err is None :
54+ return False # LDAPS bind successful -> channel binding not enforced
55+ else :
56+ context .log .debug (f"Unexpected error during LDAPS bind (noEPA): { err } " )
57+ return None
58+ except Exception as e :
59+ context .log .debug (f"Exception in run_ldaps_noEPA: { e } " )
60+ return None
61+ finally :
62+ with contextlib .suppress (Exception ):
63+ await client .disconnect ()
64+
65+ # Conduct a bind to LDAPS with channel binding supported
66+ # but intentionally miscalculated. In the case that an
67+ # LDAPS bind without channel binding supported has occurred,
68+ # you can determine whether the policy is set to "never" or
69+ # if it's set to "when supported" based on the potential
70+ # error received from the bind attempt.
71+ async def run_ldaps_withEPA (self , context , connection , target , credential ):
72+ try :
73+ client = MSLDAPClientConnection (target , credential )
74+ _ , err = await client .connect ()
75+ if err :
76+ context .log .fail (f"Error connecting to { connection .domain } : { err } " )
77+ return None
78+
79+ try :
80+ context .log .debug ("Retrieving TLS certificate hash..." )
81+ ssl_context = ssl .create_default_context ()
82+ ssl_context .check_hostname = False
83+ ssl_context .verify_mode = ssl .CERT_NONE
84+
85+ with socket .create_connection ((connection .host , 636 )) as sock , ssl_context .wrap_socket (sock , server_hostname = connection .host ) as ssl_sock :
86+ cert = ssl_sock .getpeercert (binary_form = True )
87+
88+ if cert :
89+ cert_hash = hashlib .sha256 (cert ).digest ()
90+ context .log .debug (f"Original certificate hash: { cert_hash .hex ()} " )
91+ pos = random .randint (0 , len (cert_hash ) - 1 )
92+ tampered_bytes = bytearray (cert_hash )
93+ tampered_bytes [pos ] = (tampered_bytes [pos ] + 1 ) % 256
94+ context .log .debug (f"Tampered certificate hash: { bytes (tampered_bytes ).hex ()} " )
95+ context .log .debug (f"Modified byte at position { pos } " )
96+ client .cb_data = b"tls-server-end-point:" + bytes (tampered_bytes )
97+ else :
98+ client .cb_data = b"\x00 " * 64
99+ except Exception as e :
100+ context .log .debug (f"Failed to retrieve TLS certificate hash: { e } " )
101+ client .cb_data = b"\x00 " * 64
102+
103+ _ , err = await client .bind ()
104+ if err and "data 80090346" in str (err ):
105+ return True
106+ elif (err and "data 52e" in str (err )) or err is None :
107+ return False
108+ else :
109+ context .log .fail (f"Unexpected error during LDAPS bind (withEPA): { err } " )
110+ return None
111+ except Exception as e :
112+ context .log .fail (f"Exception in run_ldaps_withEPA: { e } " )
113+ return None
114+
115+ # Domain Controllers do not have a certificate setup for
116+ # LDAPS on port 636 by default. If this has not been setup,
117+ # the TLS handshake will hang and you will not be able to
118+ # interact with LDAPS. The condition for the certificate
119+ # existing as it should is either an error regarding
120+ # the fact that the certificate is self-signed, or
121+ # no error at all. Any other "successful" edge cases
122+ # not yet accounted for.
123+ def does_ldaps_complete_handshake (self , context , dc_ip ):
124+ s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
125+ s .settimeout (5 )
126+ ssl_context = ssl .create_default_context ()
127+ ssl_context .check_hostname = False
128+ ssl_sock = ssl_context .wrap_socket (s , do_handshake_on_connect = False , suppress_ragged_eofs = False )
129+ try :
130+ ssl_sock .connect ((dc_ip , 636 ))
131+ ssl_sock .do_handshake ()
132+ return True
133+ except Exception as e :
134+ if "CERTIFICATE_VERIFY_FAILED" in str (e ):
135+ return True
136+ elif "handshake operation timed out" in str (e ):
137+ return False
138+ else :
139+ context .log .fail (f"Unexpected error during LDAPS handshake: { e } " )
140+ return False
141+ finally :
142+ ssl_sock .close ()
143+
144+ # Conduct an LDAP bind and determine if server signing
145+ # requirements are enforced based on potential errors
146+ # during the bind attempt.
147+ async def run_ldap (self , context , target , credential ):
148+ try :
149+ client = MSLDAPClientConnection (target , credential )
150+ client ._disable_signing = True # deliberately disable LDAP signing on client connection
151+ _ , err = await client .connect ()
152+ if err :
153+ context .log .fail (f"Error connecting for LDAP bind: { err } " )
154+ return None
155+
156+ _ , err = await client .bind ()
157+ if err :
158+ errstr = str (err ).lower ()
159+ if "stronger" in errstr :
160+ return True
161+ # because LDAP server signing requirements ARE enforced
162+ else :
163+ context .log .fail (f"LDAP bind error: { err } " )
164+ return None
165+ else :
166+ # LDAPS bind successful
167+ return False
168+ # because LDAP server signing requirements are not enforced
169+ except Exception as e :
170+ context .log .debug (f"Exception during LDAP bind: { e } " )
171+ return None
172+
173+ # Determine authentication context and proceed to
174+ # enumerate LDAP signing and channel binding settings
175+ def on_login (self , context , connection ):
176+ context .log .fail ("[REMOVED] Now natively supported in the host banner" )
177+ return
178+ stype = asyauthSecret .PASS
179+ secret = connection .password
180+ if connection .nthash :
181+ stype = asyauthSecret .NT
182+ secret = connection .nthash
183+ if connection .aesKey :
184+ stype = asyauthSecret .AES
185+ secret = connection .aesKey
186+
187+ anon_credential = NTLMCredential (
188+ secret = "" ,
189+ username = "" ,
190+ domain = connection .domain ,
191+ stype = asyauthSecret .PASS
192+ )
193+
194+ if not connection .username and not secret :
195+ context .log .highlight ("No credentials provided, skipping LDAP signing check" )
196+ credential = anon_credential
197+ else :
198+ if not connection .kerberos :
199+ credential = NTLMCredential (
200+ secret = secret ,
201+ username = connection .username ,
202+ domain = connection .domain ,
203+ stype = stype
204+ )
205+ else :
206+ kerberos_target = UniTarget (
207+ connection .host ,
208+ 88 ,
209+ UniProto .CLIENT_TCP ,
210+ hostname = connection .remoteName ,
211+ dc_ip = connection .kdcHost ,
212+ domain = connection .domain ,
213+ proxies = None ,
214+ dns = None ,
215+ )
216+ credential = KerberosCredential (
217+ target = kerberos_target ,
218+ secret = secret ,
219+ username = connection .username ,
220+ domain = connection .domain ,
221+ stype = stype ,
222+ )
223+
224+ ldap_signing_status = None
225+ if connection .username or secret :
226+ target = MSLDAPTarget (
227+ connection .host , 389 ,
228+ hostname = connection .remoteName ,
229+ domain = connection .domain ,
230+ dc_ip = connection .kdcHost ,
231+ )
232+ ldap_signing_status = asyncio .run (self .run_ldap (context , target , credential ))
233+ if ldap_signing_status is True :
234+ context .log .highlight ("LDAP signing IS enforced" )
235+ elif ldap_signing_status is False :
236+ context .log .highlight ("LDAP signing NOT enforced" )
237+ else :
238+ context .log .fail ("Could not determine LDAP signing requirement." )
239+
240+ if self .does_ldaps_complete_handshake (context , connection .host ):
241+ target = MSLDAPTarget (
242+ connection .host , 636 ,
243+ UniProto .CLIENT_SSL_TCP ,
244+ hostname = connection .remoteName ,
245+ domain = connection .domain ,
246+ dc_ip = connection .kdcHost ,
247+ )
248+ ldaps_noEPA = asyncio .run (self .run_ldaps_noEPA (context , connection , target , anon_credential ))
249+ ldaps_withEPA = asyncio .run (self .run_ldaps_withEPA (context , connection , target , anon_credential ))
250+
251+ if ldaps_noEPA is False and ldaps_withEPA is True :
252+ context .log .highlight ("LDAPS channel binding is set to: When Supported" )
253+ elif ldaps_noEPA is False and ldaps_withEPA is False :
254+ context .log .highlight ("LDAPS channel binding is set to: Never" )
255+ elif ldaps_noEPA is True :
256+ context .log .highlight ("LDAPS channel binding is set to: Required" )
257+ else :
258+ context .log .fail ("Could not determine LDAPS channel binding settings" )
259+ else :
260+ context .log .fail (f"{ connection .domain } - TLS handshake failed; certificate likely not configured" )
0 commit comments