NetExec/nxc/modules/pso.py

99 lines
3.7 KiB
Python
Raw Normal View History

2023-06-27 12:23:43 +00:00
#!/usr/bin/env python3
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from math import fabs
class NXCModule:
2023-09-22 03:42:54 +00:00
"""
Created by fplazar and wanetty
Module by @gm_eduard and @ferranplaza
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
2023-09-22 03:42:54 +00:00
"""
2023-06-27 12:23:43 +00:00
2023-09-22 03:42:54 +00:00
name = "pso"
2023-06-27 12:23:43 +00:00
description = "Query to get PSO from LDAP"
2023-09-22 03:42:54 +00:00
supported_protocols = ["ldap"]
2023-06-27 12:23:43 +00:00
opsec_safe = True
multiple_hosts = True
2023-06-27 12:23:43 +00:00
pso_fields = [
"cn",
"msDS-PasswordReversibleEncryptionEnabled",
"msDS-PasswordSettingsPrecedence",
"msDS-MinimumPasswordLength",
"msDS-PasswordHistoryLength",
"msDS-PasswordComplexityEnabled",
"msDS-LockoutObservationWindow",
"msDS-LockoutDuration",
"msDS-LockoutThreshold",
"msDS-MinimumPasswordAge",
"msDS-MaximumPasswordAge",
"msDS-PSOAppliesTo",
]
def options(self, context, module_options):
2023-10-12 19:13:16 +00:00
"""No options available."""
2023-06-27 12:23:43 +00:00
pass
2023-06-27 12:23:43 +00:00
def convert_time_field(self, field, value):
time_fields = {"msDS-LockoutObservationWindow": (60, "mins"), "msDS-MinimumPasswordAge": (86400, "days"), "msDS-MaximumPasswordAge": (86400, "days"), "msDS-LockoutDuration": (60, "mins")}
2023-06-27 12:23:43 +00:00
if field in time_fields.keys():
value = f"{int((fabs(float(value)) / (10000000 * time_fields[field][0])))} {time_fields[field][1]}"
2023-06-27 12:23:43 +00:00
return value
2023-06-27 12:23:43 +00:00
def on_login(self, context, connection):
2023-09-22 03:42:54 +00:00
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
2023-06-27 12:23:43 +00:00
# Building the search filter
2023-09-22 03:42:54 +00:00
search_filter = "(objectClass=msDS-PasswordSettings)"
2023-06-27 12:23:43 +00:00
try:
2023-09-22 03:42:54 +00:00
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=self.pso_fields, sizeLimit=0)
2023-06-27 12:23:43 +00:00
except ldap_impacket.LDAPSearchError as e:
2023-09-22 03:42:54 +00:00
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
2023-06-27 12:23:43 +00:00
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
resp = e.getAnswers()
pass
else:
2023-09-22 03:42:54 +00:00
context.log.debug(e)
2023-06-27 12:23:43 +00:00
return False
pso_list = []
2023-09-22 03:42:54 +00:00
context.log.debug(f"Total of records returned {len(resp)}")
2023-06-27 12:23:43 +00:00
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
pso_info = {}
try:
2023-09-22 03:42:54 +00:00
for attribute in item["attributes"]:
attr_name = str(attribute["type"])
2023-06-27 12:23:43 +00:00
if attr_name in self.pso_fields:
2023-09-22 03:42:54 +00:00
pso_info[attr_name] = attribute["vals"][0]._value.decode("utf-8")
2023-06-27 12:23:43 +00:00
pso_list.append(pso_info)
except Exception as e:
context.log.debug("Exception:", exc_info=True)
2023-09-22 03:42:54 +00:00
context.log.debug(f"Skipping item, cannot process due to error {e}")
2023-06-27 12:23:43 +00:00
pass
if len(pso_list) > 0:
2023-09-22 03:42:54 +00:00
context.log.success("Password Settings Objects (PSO) found:")
2023-06-27 12:23:43 +00:00
for pso in pso_list:
for field in self.pso_fields:
if field in pso:
value = self.convert_time_field(field, pso[field])
2023-09-22 03:42:54 +00:00
context.log.highlight(f"{field}: {value}")
context.log.highlight("-----")
2023-06-27 12:23:43 +00:00
else:
2023-09-22 03:42:54 +00:00
context.log.info("No Password Settings Objects (PSO) found.")