11import socket
22import ssl
33import asyncio
4+ import hashlib
5+ import random
46
57from msldap .connection import MSLDAPClientConnection
68from msldap .commons .target import MSLDAPTarget
1012from asyauth .common .credentials .kerberos import KerberosCredential
1113
1214from asysocks .unicomm .common .target import UniTarget , UniProto
13- import sys
15+ import contextlib
1416
1517
1618class NXCModule :
1719 """
18- Checks whether LDAP signing and channelbinding are required.
20+ Checks whether LDAP signing and LDAPS channel binding are required and/or enforced .
1921
20- Module by LuemmelSec (@theluemmel), updated by @zblurx
22+ Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
2123 Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
2224 """
23-
2425 name = "ldap-checker"
25- description = "Checks whether LDAP signing and binding are required and / or enforced"
26+ description = "Checks whether LDAP signing and channel binding are required and / or enforced"
2627 supported_protocols = ["ldap" ]
2728 opsec_safe = True
2829 multiple_hosts = True
2930
3031 def options (self , context , module_options ):
3132 """No options available."""
3233
33- def on_login (self , context , connection ):
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 (target , credential ):
40- ldapsClientConn = MSLDAPClientConnection (target , credential )
41- _ , err = await ldapsClientConn .connect ()
42-
43- # Required step to try to bind without channel binding
44- ldapsClientConn .cb_data = None
45-
46- if err is not None :
47- context .log .fail ("ERROR while connecting to " + str (connection .domain ) + ": " + str (err ))
48- sys .exit ()
49-
50- valid , err = await ldapsClientConn .bind ()
51- if "data 80090346" in str (err ):
52- return True # channel binding IS enforced
53- elif "data 52e" in str (err ):
54- return False # channel binding not enforced
55- elif err is None :
56- # LDAPS bind successful
57- # because channel binding is not enforced
58- return False
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
5946
60- # Conduct a bind to LDAPS with channel binding supported
61- # but intentionally miscalculated. In the case that and
62- # LDAPS bind has without channel binding supported has occurred,
63- # you can determine whether the policy is set to "never" or
64- # if it's set to "when supported" based on the potential
65- # error received from the bind attempt.
66- async def run_ldaps_withEPA (target , credential ):
67- ldapsClientConn = MSLDAPClientConnection (target , credential )
68- _ , err = await ldapsClientConn .connect ()
69- if err is not None :
70- context .log .fail ("ERROR while connecting to " + str (connection .domain ) + ": " + str (err ))
71- sys .exit ()
72- # forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
73- ldapsClientConn .cb_data = b"\x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 "
74- _ , err = await ldapsClientConn .bind ()
75- if "data 80090346" in str (err ):
76- return True
77- elif "data 52e" in str (err ):
78- return False
79- elif err is not None :
80- context .log .fail ("ERROR while connecting to " + str (connection .domain ) + ": " + str (err ))
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
8153 elif err is None :
82- return False
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
8378
84- # Domain Controllers do not have a certificate setup for
85- # LDAPS on port 636 by default. If this has not been setup,
86- # the TLS handshake will hang and you will not be able to
87- # interact with LDAPS. The condition for the certificate
88- # existing as it should is either an error regarding
89- # the fact that the certificate is self-signed, or
90- # no error at all. Any other "successful" edge cases
91- # not yet accounted for.
92- def DoesLdapsCompleteHandshake (dcIp ):
93- s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
94- s .settimeout (5 )
95- ssl_context = ssl .create_default_context ()
96- ssl_context .check_hostname = False
97- ssl_sock = ssl_context .wrap_socket (
98- s ,
99- do_handshake_on_connect = False ,
100- suppress_ragged_eofs = False ,
101- )
102- try :
103- ssl_sock .connect ((dcIp , 636 ))
104- ssl_sock .do_handshake ()
105- ssl_sock .close ()
106- return True
107- except Exception as e :
108- if "CERTIFICATE_VERIFY_FAILED" in str (e ):
109- ssl_sock .close ()
110- return True
111- if "handshake operation timed out" in str (e ):
112- ssl_sock .close ()
113- return False
114- else :
115- context .log .fail ("Unexpected error during LDAPS handshake: " + str (e ))
116- ssl_sock .close ()
117- return False
118-
119- # Conduct and LDAP bind and determine if server signing
120- # requirements are enforced based on potential errors
121- # during the bind attempt.
122- async def run_ldap (target , credential ):
12379 try :
124- ldapsClientConn = MSLDAPClientConnection (target , credential )
125- ldapsClientConn ._disable_signing = True
126- _ , err = await ldapsClientConn .connect ()
127- if err is not None :
128- context .log .fail (str (err ))
129- return None
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
13084
131- _ , err = await ldapsClientConn .bind ()
132- if err is not None :
133- errstr = str (err ).lower ()
134- if "stronger" in errstr :
135- return True
136- # because LDAP server signing requirements ARE enforced
137- else :
138- context .log .fail (str (err ))
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 )
13997 else :
140- # LDAPS bind successful
141- return False
142- # because LDAP server signing requirements are not enforced
98+ client .cb_data = b"\x00 " * 64
14399 except Exception as e :
144- context .log .debug (str (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+
116+ # Domain Controllers do not have a certificate setup for
117+ # LDAPS on port 636 by default. If this has not been setup,
118+ # the TLS handshake will hang and you will not be able to
119+ # interact with LDAPS. The condition for the certificate
120+ # existing as it should is either an error regarding
121+ # the fact that the certificate is self-signed, or
122+ # no error at all. Any other "successful" edge cases
123+ # not yet accounted for.
124+ def does_ldaps_complete_handshake (self , context , dc_ip ):
125+ s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
126+ s .settimeout (5 )
127+ ssl_context = ssl .create_default_context ()
128+ ssl_context .check_hostname = False
129+ ssl_sock = ssl_context .wrap_socket (s , do_handshake_on_connect = False , suppress_ragged_eofs = False )
130+ try :
131+ ssl_sock .connect ((dc_ip , 636 ))
132+ ssl_sock .do_handshake ()
133+ return True
134+ except Exception as e :
135+ if "CERTIFICATE_VERIFY_FAILED" in str (e ):
136+ return True
137+ elif "handshake operation timed out" in str (e ):
138+ return False
139+ else :
140+ context .log .fail (f"Unexpected error during LDAPS handshake: { e } " )
141+ return False
142+ finally :
143+ ssl_sock .close ()
144+
145+ # Conduct an LDAP bind and determine if server signing
146+ # requirements are enforced based on potential errors
147+ # during the bind attempt.
148+ async def run_ldap (self , context , target , credential ):
149+ try :
150+ client = MSLDAPClientConnection (target , credential )
151+ client ._disable_signing = True # deliberately disable LDAP signing on client connection
152+ _ , err = await client .connect ()
153+ if err :
154+ context .log .fail (f"Error connecting for LDAP bind: { err } " )
145155 return None
146-
147156
148- # Run trough all our code blocks to determine LDAP signing and channel binding settings.
157+ _ , err = await client .bind ()
158+ if err :
159+ errstr = str (err ).lower ()
160+ if "stronger" in errstr :
161+ return True
162+ # because LDAP server signing requirements ARE enforced
163+ else :
164+ context .log .fail (f"LDAP bind error: { err } " )
165+ return None
166+ else :
167+ # LDAPS bind successful
168+ return False
169+ # because LDAP server signing requirements are not enforced
170+ except Exception as e :
171+ context .log .debug (f"Exception during LDAP bind: { e } " )
172+ return None
173+
174+ # Determine authentication context and proceed to
175+ # enumerate LDAP signing and channel binding settings
176+ def on_login (self , context , connection ):
149177 stype = asyauthSecret .PASS
150178 secret = connection .password
151179 if connection .nthash :
@@ -154,21 +182,24 @@ async def run_ldap(target, credential):
154182 if connection .aesKey :
155183 stype = asyauthSecret .AES
156184 secret = connection .aesKey
157- if connection .username == "" and secret == "" :
158- credential = NTLMCredential (
159- secret = None ,
160- username = "Guest" ,
161- domain = None ,
162- stype = stype ,
163- )
164- context .log .info ("No username used, skipping LDAP signing check" )
185+
186+ anon_credential = NTLMCredential (
187+ secret = "" ,
188+ username = "" ,
189+ domain = connection .domain ,
190+ stype = asyauthSecret .PASS
191+ )
192+
193+ if not connection .username and not secret :
194+ context .log .highlight ("No credentials provided, skipping LDAP signing check" )
195+ credential = anon_credential
165196 else :
166197 if not connection .kerberos :
167198 credential = NTLMCredential (
168199 secret = secret ,
169200 username = connection .username ,
170201 domain = connection .domain ,
171- stype = stype ,
202+ stype = stype
172203 )
173204 else :
174205 kerberos_target = UniTarget (
@@ -189,29 +220,40 @@ async def run_ldap(target, credential):
189220 stype = stype ,
190221 )
191222
192- target = MSLDAPTarget (connection .host , 389 , hostname = connection .remoteName , domain = connection .domain , dc_ip = connection .kdcHost )
193- ldapIsProtected = asyncio .run (run_ldap (target , credential ))
194- if ldapIsProtected is False :
195- context .log .highlight ("LDAP Signing NOT Enforced!" )
196- elif ldapIsProtected is True :
197- context .log .fail ("LDAP Signing IS Enforced" )
223+ ldap_signing_status = None
224+ if connection .username or secret :
225+ target = MSLDAPTarget (
226+ connection .host , 389 ,
227+ hostname = connection .remoteName ,
228+ domain = connection .domain ,
229+ dc_ip = connection .kdcHost ,
230+ )
231+ ldap_signing_status = asyncio .run (self .run_ldap (context , target , credential ))
232+ if ldap_signing_status is True :
233+ context .log .highlight ("LDAP signing IS enforced" )
234+ elif ldap_signing_status is False :
235+ context .log .highlight ("LDAP signing NOT enforced" )
198236 else :
199- context .log .fail ("Connection fail, exiting now" )
200- sys .exit ()
201-
202- if DoesLdapsCompleteHandshake (connection .host ) is True :
203- target = MSLDAPTarget (connection .host , 636 , UniProto .CLIENT_SSL_TCP , hostname = connection .remoteName , domain = connection .domain , dc_ip = connection .kdcHost )
204- ldapsChannelBindingAlwaysCheck = asyncio .run (run_ldaps_noEPA (target , credential ))
205- target = MSLDAPTarget (connection .host , 636 , UniProto .CLIENT_SSL_TCP , hostname = connection .remoteName , domain = connection .domain , dc_ip = connection .kdcHost )
206- ldapsChannelBindingWhenSupportedCheck = asyncio .run (run_ldaps_withEPA (target , credential ))
207- if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True :
208- context .log .highlight ('LDAPS Channel Binding is set to "When Supported"' )
209- elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False :
210- context .log .highlight ('LDAPS Channel Binding is set to "NEVER"' )
211- elif ldapsChannelBindingAlwaysCheck is True :
212- context .log .fail ('LDAPS Channel Binding is set to "Required"' )
237+ context .log .fail ("Could not determine LDAP signing requirement." )
238+
239+ if self .does_ldaps_complete_handshake (context , connection .host ):
240+ target = MSLDAPTarget (
241+ connection .host , 636 ,
242+ UniProto .CLIENT_SSL_TCP ,
243+ hostname = connection .remoteName ,
244+ domain = connection .domain ,
245+ dc_ip = connection .kdcHost ,
246+ )
247+ ldaps_noEPA = asyncio .run (self .run_ldaps_noEPA (context , connection , target , anon_credential ))
248+ ldaps_withEPA = asyncio .run (self .run_ldaps_withEPA (context , connection , target , anon_credential ))
249+
250+ if ldaps_noEPA is False and ldaps_withEPA is True :
251+ context .log .highlight ("LDAPS channel binding is set to: When Supported" )
252+ elif ldaps_noEPA is False and ldaps_withEPA is False :
253+ context .log .highlight ("LDAPS channel binding is set to: Never" )
254+ elif ldaps_noEPA is True :
255+ context .log .highlight ("LDAPS channel binding is set to: Required" )
213256 else :
214- context .log .fail ("\n Something went wrong..." )
215- sys .exit ()
257+ context .log .fail ("Could not determine LDAPS channel binding settings" )
216258 else :
217- context .log .fail (connection .domain + " - cannot complete TLS handshake, cert likely not configured" )
259+ context .log .fail (f" { connection .domain } - TLS handshake failed; certificate likely not configured" )
0 commit comments