NetExec/nxc/modules/ldap-checker.py

189 lines
8.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2023-05-07 22:51:01 +00:00
import socket
import ssl
import asyncio
2023-02-12 20:59:52 +00:00
2022-10-20 18:53:36 +00:00
from msldap.connection import MSLDAPClientConnection
2023-02-12 20:59:52 +00:00
from msldap.commons.target import MSLDAPTarget
from asyauth.common.constants import asyauthSecret
2023-02-12 20:59:52 +00:00
from asyauth.common.credentials.ntlm import NTLMCredential
from asyauth.common.credentials.kerberos import KerberosCredential
from asysocks.unicomm.common.target import UniTarget, UniProto
class NXCModule:
"""
Checks whether LDAP signing and channelbinding are required.
2023-02-28 15:37:11 +00:00
Module by LuemmelSec (@theluemmel), updated by @zblurx
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
"""
2023-05-02 15:17:59 +00:00
name = "ldap-checker"
2023-05-08 18:39:36 +00:00
description = "Checks whether LDAP signing and binding are required and / or enforced"
2023-05-02 15:17:59 +00:00
supported_protocols = ["ldap"]
2023-02-12 20:59:52 +00:00
opsec_safe = True
2023-05-02 15:17:59 +00:00
multiple_hosts = True
def options(self, context, module_options):
"""
No options available.
"""
pass
def on_login(self, context, connection):
2023-05-02 15:17:59 +00:00
# Conduct a bind to LDAPS and determine if channel
# binding is enforced based on the contents of potential
# errors returned. This can be determined unauthenticated,
# because the error indicating channel binding enforcement
# will be returned regardless of a successful LDAPS bind.
async def run_ldaps_noEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
2023-05-25 08:00:22 +00:00
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
exit()
_, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True # channel binding IS enforced
elif "data 52e" in str(err):
return False # channel binding not enforced
elif err is None:
# LDAPS bind successful
# because channel binding is not enforced
return False
2023-05-02 15:17:59 +00:00
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that and
# LDAPS bind has without channel binding supported has occured,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error recieved from the bind attempt.
async def run_ldaps_withEPA(target, credential):
2023-02-12 20:59:52 +00:00
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
2023-05-25 08:00:22 +00:00
exit()
2023-05-02 15:17:59 +00:00
# forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
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"
2023-02-12 20:59:52 +00:00
_, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True
elif "data 52e" in str(err):
return False
elif err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
2023-02-12 20:59:52 +00:00
elif err is None:
return False
2023-05-02 15:17:59 +00:00
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
# interact with LDAPS. The condition for the certificate
# existing as it should is either an error regarding
# the fact that the certificate is self-signed, or
# no error at all. Any other "successful" edge cases
# not yet accounted for.
def DoesLdapsCompleteHandshake(dcIp):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
2023-05-02 15:17:59 +00:00
ssl_sock = ssl.wrap_socket(
s,
cert_reqs=ssl.CERT_OPTIONAL,
suppress_ragged_eofs=False,
do_handshake_on_connect=False,
)
ssl_sock.connect((dcIp, 636))
try:
ssl_sock.do_handshake()
ssl_sock.close()
return True
except Exception as e:
if "CERTIFICATE_VERIFY_FAILED" in str(e):
ssl_sock.close()
return True
if "handshake operation timed out" in str(e):
ssl_sock.close()
return False
else:
2023-05-08 18:39:36 +00:00
context.log.fail("Unexpected error during LDAPS handshake: " + str(e))
ssl_sock.close()
2022-07-08 11:58:14 +00:00
return False
2023-05-02 15:17:59 +00:00
# Conduct and LDAP bind and determine if server signing
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
2023-05-25 08:00:22 +00:00
if err is None:
_, err = await ldapsClientConn.bind()
if "stronger" in str(err):
return True # because LDAP server signing requirements ARE enforced
elif ("data 52e" or "data 532") in str(err):
context.log.fail("Not connected... exiting")
exit()
elif err is None:
return False
else:
context.log.fail(str(err))
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
secret = connection.password if not connection.nthash else connection.nthash
if not connection.kerberos:
credential = NTLMCredential(
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype,
)
else:
kerberos_target = UniTarget(
connection.hostname + '.' + connection.domain,
88,
UniProto.CLIENT_TCP,
proxies=None,
dns=None,
dc_ip=connection.domain,
domain=connection.domain
)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype,
)
2023-02-12 20:59:52 +00:00
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
2023-05-02 15:17:59 +00:00
2023-02-12 20:59:52 +00:00
if ldapIsProtected == False:
context.log.highlight("LDAP Signing NOT Enforced!")
elif ldapIsProtected == True:
context.log.fail("LDAP Signing IS Enforced")
else:
context.log.fail("Connection fail, exiting now")
exit()
2023-08-08 14:25:21 +00:00
if DoesLdapsCompleteHandshake(connection.host) == True:
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
2023-05-08 18:39:36 +00:00
if ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
elif ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == False:
2023-05-02 15:17:59 +00:00
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
2023-02-12 20:59:52 +00:00
elif ldapsChannelBindingAlwaysCheck == True:
2023-05-02 15:17:59 +00:00
context.log.fail('LDAPS Channel Binding is set to "Required"')
2022-07-08 11:58:14 +00:00
else:
context.log.fail("\nSomething went wrong...")
2023-05-02 15:17:59 +00:00
exit()
2023-02-12 20:59:52 +00:00
else:
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")