Skip to content

Commit

Permalink
Merge pull request #508 from Pennyw0rth/ldap_wt_smb
Browse files Browse the repository at this point in the history
Remove smb from ldap proto
  • Loading branch information
mpgn authored Dec 19, 2024
2 parents d05aeb5 + 4767762 commit ecd3fcd
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 143 deletions.
197 changes: 55 additions & 142 deletions nxc/protocols/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,22 @@
from OpenSSL.SSL import SysCallError
from bloodhound.ad.authentication import ADAuthentication
from bloodhound.ad.domain import AD
from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5.samr import (
UF_ACCOUNTDISABLE,
UF_DONT_REQUIRE_PREAUTH,
UF_TRUSTED_FOR_DELEGATION,
UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
UF_SERVER_TRUST_ACCOUNT,
)
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
from impacket.krb5 import constants
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
from impacket.krb5.types import Principal, KerberosException
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldaptypes
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPFilterSyntaxError
from impacket.smb import SMB_DIALECT
from impacket.smbconnection import SMBConnection, SessionError
from impacket.smbconnection import SessionError
from impacket.ntlm import getNTLMSSPType1

from nxc.config import process_secret, host_info_colors
from nxc.connection import connection
Expand All @@ -42,6 +39,7 @@
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from nxc.protocols.ldap.kerberos import KerberosAttacks
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.ntlm_parser import parse_challenge

ldap_error_status = {
"1": "STATUS_NOT_SUPPORTED",
Expand Down Expand Up @@ -136,7 +134,7 @@ def __init__(self, args, db, host):
self.server_os = None
self.os_arch = 0
self.hash = None
self.ldapConnection = None
self.ldap_connection = None
self.lmhash = ""
self.nthash = ""
self.baseDN = ""
Expand All @@ -163,25 +161,25 @@ def proto_logger(self):
}
)

def get_ldap_info(self, host):
def create_conn_obj(self):
try:
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{host}"
ldap_url = f"{proto}://{self.host}"
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
try:
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
if ldap_connection:
self.logger.debug(f"ldap_connection: {ldap_connection}")
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
if self.ldap_connection:
self.logger.debug(f"ldap_connection: {self.ldap_connection}")
except SysCallError as e:
if proto == "ldaps":
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority
self.logger.fail("Even if the port is open, LDAPS may not be configured")
else:
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
exit(1)
return False

resp = ldap_connection.search(
resp = self.ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
attributes=["defaultNamingContext", "dnsHostName"],
sizeLimit=0,
Expand All @@ -208,42 +206,18 @@ def get_ldap_info(self, host):
self.logger.debug("Exception:", exc_info=True)
self.logger.info(f"Skipping item, cannot process due to error {e}")
except OSError:
return [None, None, None]
return False
self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}")
return [target, target_domain, base_dn]

def get_os_arch(self):
try:
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
transport = DCERPCTransportFactory(string_binding)
transport.setRemoteHost(self.host)
transport.set_connect_timeout(5)
dce = transport.get_dce_rpc()
if self.args.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
try:
dce.bind(
MSRPC_UUID_PORTMAP,
transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"),
)
except DCERPCException as e:
if str(e).find("syntaxes_not_supported") >= 0:
dce.disconnect()
return 32
else:
dce.disconnect()
return 64
except Exception as e:
self.logger.fail(f"Error retrieving os arch of {self.host}: {e!s}")

return 0
self.target = target
self.targetDomain = target_domain
self.baseDN = base_dn
return True

def get_ldap_username(self):
extended_request = ldapasn1_impacket.ExtendedRequest()
extended_request["requestName"] = "1.3.6.1.4.1.4203.1.11.3" # whoami

response = self.ldapConnection.sendReceive(extended_request)
response = self.ldap_connection.sendReceive(extended_request)
for message in response:
search_result = message["protocolOp"].getComponent()
if search_result["resultCode"] == ldapasn1_impacket.ResultCode("success"):
Expand All @@ -254,46 +228,26 @@ def get_ldap_username(self):
return ""

def enum_host_info(self):
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
self.baseDN = self.args.base_dn if self.args.base_dn else self.baseDN # Allow overwriting baseDN from args
self.hostname = self.target
self.hostname = self.target.split(".")[0].upper()
self.remoteName = self.target
self.domain = self.targetDomain
# smb no open, specify the domain
if not self.args.no_smb:
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]

try:
self.conn.login("", "")
except BrokenPipeError as e:
self.logger.fail(f"Broken Pipe Error while attempting to login: {e}")
except Exception as e:
if "STATUS_NOT_SUPPORTED" in str(e):
self.no_ntlm = True
if not self.no_ntlm:
self.hostname = self.conn.getServerName()
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
self.server_os = self.conn.getServerOS()
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
self.os_arch = self.get_os_arch()
self.logger.extra["hostname"] = self.hostname

if not self.domain:
self.domain = self.hostname
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"

try: # noqa: SIM105
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
self.conn.logoff()
except Exception:
pass

# Re-connect since we logged off
self.create_conn_obj()
ntlm_challenge = None
bindRequest = ldapasn1_impacket.BindRequest()
bindRequest["version"] = 3
bindRequest["name"] = ""
negotiate = getNTLMSSPType1()
bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData()
try:
response = self.ldap_connection.sendReceive(bindRequest)[0]["protocolOp"]
ntlm_challenge = bytes(response["bindResponse"]["matchedDN"])
except Exception as e:
self.logger.debug(f"Failed to get target {self.host} ntlm challenge, error: {e!s}")

if ntlm_challenge:
ntlm_info = parse_challenge(ntlm_challenge)
self.server_os = ntlm_info["os_version"]

if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
Expand All @@ -304,17 +258,10 @@ def enum_host_info(self):

def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
if self.args.no_smb:
self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS"
self.logger.extra["port"] = self.port
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
else:
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS"
self.logger.extra["port"] = self.port
self.logger.extra["hostname"] = self.hostname
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")

def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.username = username
Expand Down Expand Up @@ -355,8 +302,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()

Expand Down Expand Up @@ -400,8 +347,8 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()

Expand Down Expand Up @@ -457,8 +404,8 @@ def plaintext_login(self, domain, username, password):
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()

# Prepare success credential text
Expand All @@ -478,8 +425,8 @@ def plaintext_login(self, domain, username, password):
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()

# Prepare success credential text
Expand Down Expand Up @@ -543,8 +490,8 @@ def hash_login(self, domain, username, ntlm_hash):
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()

# Prepare success credential text
Expand All @@ -564,8 +511,8 @@ def hash_login(self, domain, username, ntlm_hash):
self.logger.extra["port"] = "636"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()

# Prepare success credential text
Expand Down Expand Up @@ -594,40 +541,6 @@ def hash_login(self, domain, username, ntlm_hash):
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
return False

def create_smbv1_conn(self):
self.logger.debug("Creating smbv1 connection object")
try:
self.conn = SMBConnection(self.host, self.host, None, 445, preferredDialect=SMB_DIALECT)
self.smbv1 = True
if self.conn:
self.logger.debug("SMBv1 Connection successful")
except OSError as e:
if str(e).find("Connection reset by peer") != -1:
self.logger.debug(f"SMBv1 might be disabled on {self.host}")
return False
except Exception as e:
self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}")
return False
return True

def create_smbv3_conn(self):
self.logger.debug("Creating smbv3 connection object")
try:
self.conn = SMBConnection(self.host, self.host, None, 445)
self.smbv1 = False
if self.conn:
self.logger.debug("SMBv3 Connection successful")
except OSError:
return False
except Exception as e:
self.logger.debug(f"Error creating SMBv3 connection to {self.host}: {e}")
return False

return True

def create_conn_obj(self):
return bool(self.args.no_smb or self.create_smbv1_conn() or self.create_smbv3_conn())

def get_sid(self):
self.logger.highlight(f"Domain SID {self.sid_domain}")

Expand Down Expand Up @@ -692,12 +605,12 @@ def getUnixTime(self, t):

def search(self, searchFilter, attributes, sizeLimit=0) -> list:
try:
if self.ldapConnection:
if self.ldap_connection:
self.logger.debug(f"Search Filter={searchFilter}")

# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)
return self.ldapConnection.search(
return self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=searchFilter,
attributes=attributes,
Expand Down Expand Up @@ -1245,7 +1158,7 @@ def password_not_required(self):
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)"
try:
self.logger.debug(f"Search Filter={searchFilter}")
resp = self.ldapConnection.search(
resp = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=searchFilter,
attributes=[
Expand Down Expand Up @@ -1373,7 +1286,7 @@ def admin_count(self):
def gmsa(self):
self.logger.display("Getting GMSA Passwords")
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldapConnection.search(
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=[
Expand Down Expand Up @@ -1426,7 +1339,7 @@ def gmsa_convert_id(self):
else:
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldapConnection.search(
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
Expand Down Expand Up @@ -1456,7 +1369,7 @@ def gmsa_decrypt_lsa(self):
gmsa_pass = gmsa[1]
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldapConnection.search(
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
Expand Down
1 change: 0 additions & 1 deletion nxc/protocols/ldap/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ def proto_args(parser, parents):
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone)
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port")
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")

dgroup = ldap_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
Expand Down

0 comments on commit ecd3fcd

Please sign in to comment.