Skip to content

Commit f3d5f8d

Browse files
committed
Readd ldap-checker module with 'REMOVED' note
1 parent f36c53f commit f3d5f8d

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

nxc/modules/ldap-checker.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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

Comments
 (0)