Skip to content

Commit 486fc20

Browse files
committed
update passpol
1 parent 824dbff commit 486fc20

2 files changed

Lines changed: 118 additions & 66 deletions

File tree

nxc/helpers/misc.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66

77
from ipaddress import ip_address
8+
from time import strftime, gmtime
89

910

1011
def identify_target_file(target_file):
@@ -145,3 +146,59 @@ def detect_if_ip(target):
145146
return True
146147
except Exception:
147148
return False
149+
150+
151+
def d2b(a):
152+
tbin = []
153+
while a:
154+
tbin.append(a % 2)
155+
a //= 2
156+
157+
t2bin = tbin[::-1]
158+
if len(t2bin) != 8:
159+
for _x in range(6 - len(t2bin)):
160+
t2bin.insert(0, 0)
161+
return "".join([str(g) for g in t2bin])
162+
163+
164+
def convert(low, high, lockout=False):
165+
time = ""
166+
tmp = 0
167+
168+
if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000):
169+
return "Not Set"
170+
if low == 0 and high == 0:
171+
return "None"
172+
173+
if not lockout:
174+
if low != 0:
175+
high = abs(high + 1)
176+
else:
177+
high = abs(high)
178+
low = abs(low)
179+
180+
tmp = low + (high << 32) # convert to 64bit int
181+
tmp *= 1e-7 # convert to seconds
182+
else:
183+
tmp = abs(high) * (1e-7)
184+
185+
try:
186+
minutes = int(strftime("%M", gmtime(tmp)))
187+
hours = int(strftime("%H", gmtime(tmp)))
188+
days = int(strftime("%j", gmtime(tmp))) - 1
189+
except ValueError:
190+
return "[-] Invalid TIME"
191+
192+
if days > 1:
193+
time += f"{days} days "
194+
elif days == 1:
195+
time += f"{days} day "
196+
if hours > 1:
197+
time += f"{hours} hours "
198+
elif hours == 1:
199+
time += f"{hours} hour "
200+
if minutes > 1:
201+
time += f"{minutes} minutes "
202+
elif minutes == 1:
203+
time += f"{minutes} minute "
204+
return time

nxc/protocols/ldap.py

Lines changed: 61 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@
3838
from nxc.config import process_secret, host_info_colors
3939
from nxc.connection import connection
4040
from nxc.helpers.bloodhound import add_user_bh
41+
from nxc.helpers.misc import get_bloodhound_info, convert, d2b
4142
from nxc.logger import NXCAdapter, nxc_logger
4243
from nxc.protocols.ldap.bloodhound import BloodHound
4344
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
4445
from nxc.protocols.ldap.kerberos import KerberosAttacks
4546
from nxc.parsers.ldap_results import parse_result_attributes
4647
from nxc.helpers.ntlm_parser import parse_challenge
47-
from nxc.helpers.misc import get_bloodhound_info
4848
from nxc.paths import CONFIG_PATH, NXC_PATH
4949

5050
ldap_error_status = {
@@ -1498,75 +1498,70 @@ def pass_pol(self):
14981498
self.logger.fail("No domain password policy found!")
14991499
return
15001500

1501-
self.logger.highlight("Domain Password Policy:")
1502-
self.logger.highlight("")
1503-
15041501
for policy in resp_parsed:
1505-
# Helper function to convert LDAP time to human readable format
1506-
def ldap_time_to_days(ldap_time):
1507-
if not ldap_time or ldap_time == "0":
1508-
return "Never"
1509-
# LDAP time is in 100-nanosecond intervals, negative for time intervals
1510-
seconds = abs(int(ldap_time)) / 10000000
1511-
days = int(seconds / 86400) # 86400 seconds in a day
1512-
return f"{days} days"
1513-
1514-
def ldap_time_to_minutes(ldap_time):
1502+
def ldap_to_filetime(ldap_time):
1503+
"""Convert LDAP time to FILETIME format for convert function"""
15151504
if not ldap_time or ldap_time == "0":
1516-
return "Never"
1517-
# LDAP time is in 100-nanosecond intervals
1518-
seconds = abs(int(ldap_time)) / 10000000
1519-
minutes = int(seconds / 60)
1520-
return f"{minutes} minutes"
1521-
1522-
# Display password policy information
1523-
min_pwd_length = policy.get("minPwdLength", "Not set")
1524-
pwd_history_length = policy.get("pwdHistoryLength", "Not set")
1525-
max_pwd_age = ldap_time_to_days(policy.get("maxPwdAge", "0"))
1526-
min_pwd_age = ldap_time_to_days(policy.get("minPwdAge", "0"))
1527-
lockout_threshold = policy.get("lockoutThreshold", "Not set")
1528-
lockout_duration = ldap_time_to_minutes(policy.get("lockoutDuration", "0"))
1529-
lockout_observation_window = ldap_time_to_minutes(policy.get("lockOutObservationWindow", "0"))
1530-
force_logoff = ldap_time_to_minutes(policy.get("forceLogoff", "0"))
1505+
return 0, 0
1506+
1507+
time_int = int(ldap_time)
1508+
if time_int < 0:
1509+
time_int = abs(time_int)
1510+
1511+
low = time_int & 0xFFFFFFFF
1512+
high = (time_int >> 32) & 0xFFFFFFFF
1513+
if ldap_time.startswith("-") or int(ldap_time) < 0:
1514+
high = -high
1515+
1516+
return low, high
1517+
1518+
min_pass_len = policy.get("minPwdLength", "None")
1519+
pass_hist_len = policy.get("pwdHistoryLength", "None")
1520+
max_pwd_age_low, max_pwd_age_high = ldap_to_filetime(policy.get("maxPwdAge", "0"))
1521+
max_pass_age = convert(max_pwd_age_low, max_pwd_age_high)
1522+
min_pwd_age_low, min_pwd_age_high = ldap_to_filetime(policy.get("minPwdAge", "0"))
1523+
min_pass_age = convert(min_pwd_age_low, min_pwd_age_high)
1524+
accnt_lock_thres = policy.get("lockoutThreshold", "None")
1525+
lockout_duration_val = policy.get("lockoutDuration", "0")
1526+
lock_accnt_dur = convert(0, int(lockout_duration_val) if lockout_duration_val != "0" else 0, lockout=True)
1527+
lockout_obs_val = policy.get("lockOutObservationWindow", "0")
1528+
rst_accnt_lock_counter = convert(0, int(lockout_obs_val) if lockout_obs_val != "0" else 0, lockout=True)
1529+
force_logoff_low, force_logoff_high = ldap_to_filetime(policy.get("forceLogoff", "0"))
1530+
force_logoff_time = convert(force_logoff_low, force_logoff_high)
1531+
1532+
# Convert password properties using existing d2b function
15311533
pwd_properties = policy.get("pwdProperties", "0")
1534+
pass_prop = d2b(int(pwd_properties)) if pwd_properties != "0" else "None"
1535+
1536+
# Use the same formatting and constants as SMB passpol
1537+
PASSCOMPLEX = {
1538+
5: "Domain Password Complex:",
1539+
4: "Domain Password No Anon Change:",
1540+
3: "Domain Password No Clear Change:",
1541+
2: "Domain Password Lockout Admins:",
1542+
1: "Domain Password Store Cleartext:",
1543+
0: "Domain Refuse Password Change:",
1544+
}
15321545

1533-
self.logger.highlight(f"Minimum Password Length: {min_pwd_length}")
1534-
self.logger.highlight(f"Password History Length: {pwd_history_length}")
1535-
self.logger.highlight(f"Maximum Password Age: {max_pwd_age}")
1536-
self.logger.highlight(f"Minimum Password Age: {min_pwd_age}")
1537-
self.logger.highlight(f"Account Lockout Threshold: {lockout_threshold}")
1538-
self.logger.highlight(f"Account Lockout Duration: {lockout_duration}")
1539-
self.logger.highlight(f"Account Lockout Observation Window: {lockout_observation_window}")
1540-
self.logger.highlight(f"Force Logoff: {force_logoff}")
1541-
1542-
# Decode password properties flags
1543-
if pwd_properties and pwd_properties != "0":
1544-
pwd_props_int = int(pwd_properties)
1545-
properties = []
1546-
1547-
if pwd_props_int & 0x1:
1548-
properties.append("Password complexity enabled")
1549-
if pwd_props_int & 0x2:
1550-
properties.append("Store passwords using reversible encryption")
1551-
if pwd_props_int & 0x4:
1552-
properties.append("No anonymous password changes")
1553-
if pwd_props_int & 0x8:
1554-
properties.append("No clear change password")
1555-
if pwd_props_int & 0x10:
1556-
properties.append("Lockout admins")
1557-
if pwd_props_int & 0x20:
1558-
properties.append("Store password with weaker obfuscation")
1559-
if pwd_props_int & 0x40:
1560-
properties.append("Refuse password change")
1561-
1562-
if properties:
1563-
self.logger.highlight("Password Properties:")
1564-
for prop in properties:
1565-
self.logger.highlight(f" - {prop}")
1566-
else:
1567-
self.logger.highlight(f"Password Properties: {pwd_properties} (Unknown flags)")
1568-
else:
1569-
self.logger.highlight("Password Properties: None")
1546+
# Pretty print using same format as SMB
1547+
self.logger.success(f"Dumping password info for domain: {self.domain}")
1548+
self.logger.highlight(f"Minimum password length: {min_pass_len}")
1549+
self.logger.highlight(f"Password history length: {pass_hist_len}")
1550+
self.logger.highlight(f"Maximum password age: {max_pass_age}")
1551+
self.logger.highlight("")
1552+
self.logger.highlight(f"Password Complexity Flags: {pass_prop or 'None'}")
1553+
1554+
for i, a in enumerate(pass_prop):
1555+
self.logger.highlight(f"\t{PASSCOMPLEX[i]} {a!s}")
1556+
1557+
self.logger.highlight("")
1558+
self.logger.highlight(f"Minimum password age: {min_pass_age}")
1559+
self.logger.highlight(f"Reset Account Lockout Counter: {rst_accnt_lock_counter}")
1560+
self.logger.highlight(f"Locked Account Duration: {lock_accnt_dur}")
1561+
self.logger.highlight(f"Account Lockout Threshold: {accnt_lock_thres}")
1562+
self.logger.highlight(f"Forced Log off Time: {force_logoff_time}")
1563+
1564+
break # Only process first policy result
15701565

15711566
def bloodhound(self):
15721567
# Check which version is desired

0 commit comments

Comments
 (0)