Skip to content

Commit

Permalink
Merge pull request #215 from Pennyw0rth/marshall-ldap-users
Browse files Browse the repository at this point in the history
Update LDAP users lookup to match SMB
  • Loading branch information
Marshall-Hallenbeck authored Mar 22, 2024
2 parents fb8c4bc + 40e9991 commit 3dc84e1
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 37 deletions.
13 changes: 13 additions & 0 deletions nxc/parsers/ldap_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket

def parse_result_attributes(ldap_response):
parsed_response = []
for entry in ldap_response:
# SearchResultReferences may be returned
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
continue
attribute_map = {}
for attribute in entry["attributes"]:
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
parsed_response.append(attribute_map)
return parsed_response
78 changes: 46 additions & 32 deletions nxc/protocols/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
import socket
from binascii import hexlify
from datetime import datetime
from datetime import datetime, timedelta
from re import sub, I
from zipfile import ZipFile
from termcolor import colored
Expand Down Expand Up @@ -38,6 +38,7 @@
from nxc.protocols.ldap.bloodhound import BloodHound
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

ldap_error_status = {
"1": "STATUS_NOT_SUPPORTED",
Expand Down Expand Up @@ -372,7 +373,6 @@ def kerberos_login(
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"


self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
Expand Down Expand Up @@ -753,37 +753,51 @@ def search(self, searchFilter, attributes, sizeLimit=0):
return False

def users(self):
# Building the search filter
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
attributes = [
"sAMAccountName",
"description",
"badPasswordTime",
"badPwdCount",
"pwdLastSet",
]
"""
Retrieves user information from the LDAP server.
Args:
----
input_attributes (list): Optional. List of attributes to retrieve for each user.
Returns:
-------
None
"""
if len(self.args.users) > 0:
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
else:
self.logger.debug("Trying to dump all users")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"

# default to these attributes to mirror the SMB --users functionality
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
resp = self.search(search_filter, request_attributes, sizeLimit=0)

resp = self.search(search_filter, attributes, sizeLimit=0)
if resp:
self.logger.display(f"Total of records returned {len(resp):d}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
description = ""
try:
if self.username == "":
self.logger.highlight(f"{item['objectName']}")
else:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "description":
description = str(attribute["vals"][0])
self.logger.highlight(f"{sAMAccountName:<30} {description}")
except Exception as e:
self.logger.debug(f"Skipping item, cannot process due to error {e}")
return
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
if self.username == "":
self.logger.display(f"Total records returned: {len(resp):d}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
self.logger.highlight(f"{item['objectName']}")
return

users = parse_result_attributes(resp)
# we print the total records after we parse the results since often SearchResultReferences are returned
self.logger.display(f"Total records returned: {len(users):d}")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for user in users:
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
timestamp_seconds = int(user.get("pwdLastSet", "")) / 10**7
start_date = datetime(1601, 1, 1)
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
if parsed_pw_last_set == "1601-01-01 00:00:00":
parsed_pw_last_set = "<never>"
# we default attributes to blank strings if they don't exist in the dict
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")

def groups(self):
# Building the search filter
Expand Down Expand Up @@ -853,7 +867,7 @@ def active_users(self):
elif str(attribute["type"]) == "userAccountControl":
userAccountControl = int(attribute["vals"][0])
account_disabled = userAccountControl & 2
if not account_disabled:
if not account_disabled:
self.logger.highlight(f"{sAMAccountName}")
except Exception as e:
self.logger.debug(f"Skipping item, cannot process due to error {e}")
Expand Down
2 changes: 1 addition & 1 deletion nxc/protocols/ldap/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def proto_args(parser, std_parser, module_parser):
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
Expand Down
6 changes: 2 additions & 4 deletions nxc/protocols/smb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,10 +1000,8 @@ def groups(self):
return groups

def users(self):
if len(self.args.users) > 1:
self.logger.display(f"Dumping users: {', '.join(self.args.users)}")
else:
self.logger.info("Trying to dump local users with SAMRPC protocol")
if len(self.args.users) > 0:
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")

return UserSamrDump(self).dump(self.args.users)

Expand Down

0 comments on commit 3dc84e1

Please sign in to comment.