|
38 | 38 | from nxc.config import process_secret, host_info_colors |
39 | 39 | from nxc.connection import connection |
40 | 40 | from nxc.helpers.bloodhound import add_user_bh |
| 41 | +from nxc.helpers.misc import get_bloodhound_info, convert, d2b |
41 | 42 | from nxc.logger import NXCAdapter, nxc_logger |
42 | 43 | from nxc.protocols.ldap.bloodhound import BloodHound |
43 | 44 | from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB |
44 | 45 | from nxc.protocols.ldap.kerberos import KerberosAttacks |
45 | 46 | from nxc.parsers.ldap_results import parse_result_attributes |
46 | 47 | from nxc.helpers.ntlm_parser import parse_challenge |
47 | | -from nxc.helpers.misc import get_bloodhound_info |
48 | 48 | from nxc.paths import CONFIG_PATH, NXC_PATH |
49 | 49 |
|
50 | 50 | ldap_error_status = { |
@@ -1498,75 +1498,70 @@ def pass_pol(self): |
1498 | 1498 | self.logger.fail("No domain password policy found!") |
1499 | 1499 | return |
1500 | 1500 |
|
1501 | | - self.logger.highlight("Domain Password Policy:") |
1502 | | - self.logger.highlight("") |
1503 | | - |
1504 | 1501 | 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""" |
1515 | 1504 | 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 |
1531 | 1533 | 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 | + } |
1532 | 1545 |
|
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 |
1570 | 1565 |
|
1571 | 1566 | def bloodhound(self): |
1572 | 1567 | # Check which version is desired |
|
0 commit comments