Skip to content

Conversation

@DaahtKing
Copy link
Contributor

New module to list active user accounts with Password Never Expires :

Poc

…nabled

Signed-off-by: Daahtk <61582785+DaahtKing@users.noreply.github.com>
@NeffIsBack
Copy link
Member

NeffIsBack commented Dec 9, 2025

Thanks for the PR, but (un-)fortunately this functionality already exists.
image
You could add the lastPW timestamp tho and format it nicely, similar to other LDAP flags.

@NeffIsBack NeffIsBack added the duplicate This issue or pull request already exists label Dec 9, 2025
@Dfte
Copy link
Contributor

Dfte commented Dec 12, 2025

These are two differents things @NeffIsBack :

  • Password never expires means what it is
  • Password not requried imples that user accounts can have blank password and most of all, they are not affected by the password policy allowing having a pass pol with min char 16 and still & 1234 password

@NeffIsBack
Copy link
Member

NeffIsBack commented Dec 12, 2025

These are two differents things @NeffIsBack :

  • Password never expires means what it is
  • Password not requried imples that user accounts can have blank password and most of all, they are not affected by the password policy allowing having a pass pol with min char 16 and still & 1234 password

Thanks for pointing it out, i can't read apparently. Should have compared the bits that are queried.

@DaahtKing please substitute the bits with the constants from impacket. E.g. see:

search_filter = f"(&(UserAccountControl:1.2.840.113556.1.4.803:={UF_DONT_REQUIRE_PREAUTH})(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))(!(objectCategory=computer)))"

@NeffIsBack NeffIsBack removed the duplicate This issue or pull request already exists label Dec 12, 2025
Copy link
Member

@NeffIsBack NeffIsBack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick review :)

I think a formatted output such as --users has would be nice as well, see:

self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}")
for user in resp_parsed:
pwd_last_set = user.get("pwdLastSet", "")
if pwd_last_set:
pwd_last_set = "<never>" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S")
# We default attributes to blank strings if they don't exist in the dict
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', ''):<60}")

Comment on lines +23 to +31
def ldap_time_to_datetime(self, ldap_time):
"""Convert an LDAP timestamp to a datetime object."""
if ldap_time == "0":
return "Never"
try:
epoch = datetime(1601, 1, 1) + timedelta(seconds=int(ldap_time) / 10000000)
return epoch.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return "Conversion Error"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the connection.getUnixTime() function for conversion. E.g.:

pwd_last_set = "<never>" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S")

context.log.debug(f"Search Filter={search_filter}")

# Executing the LDAP query
resp = connection.ldap_connection.search(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the internal connection.search function. Debug logging for attributes, error handling etc is also handled in there.


if not resp:
context.log.display("No accounts found with Password Never Expires")
return True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just do a normal return, nothing checks if the module has executed "successfully" or not.

Comment on lines +73 to +97
for item in resp:
if "attributes" not in item:
continue

account_data = {}

for attribute in item["attributes"]:
attr_type = str(attribute["type"])

if attr_type == "sAMAccountName":
account_data['username'] = str(attribute["vals"][0])
elif attr_type == "distinguishedName":
account_data['dn'] = str(attribute["vals"][0])
elif attr_type == "userAccountControl":
account_data['uac'] = int(attribute["vals"][0])
elif attr_type == "whenCreated":
account_data['created'] = str(attribute["vals"][0])
elif attr_type == "pwdLastSet" and attribute["vals"]:
pwd_last_set = str(attribute["vals"][0])
account_data['pwdLastSet'] = self.ldap_time_to_datetime(pwd_last_set)
elif attr_type == "description" and attribute["vals"]:
account_data['description'] = str(attribute["vals"][0])

if 'username' in account_data:
accounts.append(account_data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the parse_result_attributes function.

# Saving and displaying results
if accounts:
account_count = len(accounts)
filename = f"{NXC_PATH}/logs/{connection.domain}.pwd_never_expires.txt"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the "NXC_PATH/modules/<module_name>" path

Comment on lines +126 to +128
return False

return True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These returns can be removed

Comment on lines +47 to +48
"(!(userAccountControl:1.2.840.113556.1.4.803:=2))"
"(userAccountControl:1.2.840.113556.1.4.803:=65536))")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use constants form impacket, e.g. "UF_ACCOUNTDISABLE"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants