|
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 = { |
@@ -1481,6 +1481,92 @@ def pso_mins(ldap_time): |
1481 | 1481 | self.logger.highlight(f"\t{policyApplies}") |
1482 | 1482 | self.logger.highlight("") |
1483 | 1483 |
|
| 1484 | + def pass_pol(self): |
| 1485 | + search_filter = "(objectClass=domainDNS)" |
| 1486 | + attributes = [ |
| 1487 | + "minPwdLength", |
| 1488 | + "pwdHistoryLength", |
| 1489 | + "maxPwdAge", |
| 1490 | + "minPwdAge", |
| 1491 | + "lockoutThreshold", |
| 1492 | + "lockoutDuration", |
| 1493 | + "lockOutObservationWindow", |
| 1494 | + "forceLogoff", |
| 1495 | + "pwdProperties" |
| 1496 | + ] |
| 1497 | + |
| 1498 | + resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN) |
| 1499 | + resp_parsed = parse_result_attributes(resp) |
| 1500 | + |
| 1501 | + if not resp_parsed: |
| 1502 | + self.logger.fail("No domain password policy found!") |
| 1503 | + return |
| 1504 | + |
| 1505 | + for policy in resp_parsed: |
| 1506 | + def ldap_to_filetime(ldap_time): |
| 1507 | + """Convert LDAP time to FILETIME format for convert function""" |
| 1508 | + if not ldap_time or ldap_time == "0": |
| 1509 | + return 0, 0 |
| 1510 | + |
| 1511 | + time_int = int(ldap_time) |
| 1512 | + if time_int < 0: |
| 1513 | + time_int = abs(time_int) |
| 1514 | + |
| 1515 | + low = time_int & 0xFFFFFFFF |
| 1516 | + high = (time_int >> 32) & 0xFFFFFFFF |
| 1517 | + if ldap_time.startswith("-") or int(ldap_time) < 0: |
| 1518 | + high = -high |
| 1519 | + |
| 1520 | + return low, high |
| 1521 | + |
| 1522 | + min_pass_len = policy.get("minPwdLength", "None") |
| 1523 | + pass_hist_len = policy.get("pwdHistoryLength", "None") |
| 1524 | + max_pwd_age_low, max_pwd_age_high = ldap_to_filetime(policy.get("maxPwdAge", "0")) |
| 1525 | + max_pass_age = convert(max_pwd_age_low, max_pwd_age_high) |
| 1526 | + min_pwd_age_low, min_pwd_age_high = ldap_to_filetime(policy.get("minPwdAge", "0")) |
| 1527 | + min_pass_age = convert(min_pwd_age_low, min_pwd_age_high) |
| 1528 | + accnt_lock_thres = policy.get("lockoutThreshold", "None") |
| 1529 | + lockout_duration_val = policy.get("lockoutDuration", "0") |
| 1530 | + lock_accnt_dur = convert(0, int(lockout_duration_val) if lockout_duration_val != "0" else 0, lockout=True) |
| 1531 | + lockout_obs_val = policy.get("lockOutObservationWindow", "0") |
| 1532 | + rst_accnt_lock_counter = convert(0, int(lockout_obs_val) if lockout_obs_val != "0" else 0, lockout=True) |
| 1533 | + force_logoff_low, force_logoff_high = ldap_to_filetime(policy.get("forceLogoff", "0")) |
| 1534 | + force_logoff_time = convert(force_logoff_low, force_logoff_high) |
| 1535 | + |
| 1536 | + # Convert password properties using existing d2b function |
| 1537 | + pwd_properties = policy.get("pwdProperties", "0") |
| 1538 | + pass_prop = d2b(int(pwd_properties)) if pwd_properties != "0" else "000000" |
| 1539 | + |
| 1540 | + # Use the same formatting and constants as SMB passpol |
| 1541 | + PASSCOMPLEX = { |
| 1542 | + 5: "Domain Password Complex:", |
| 1543 | + 4: "Domain Password No Anon Change:", |
| 1544 | + 3: "Domain Password No Clear Change:", |
| 1545 | + 2: "Domain Password Lockout Admins:", |
| 1546 | + 1: "Domain Password Store Cleartext:", |
| 1547 | + 0: "Domain Refuse Password Change:", |
| 1548 | + } |
| 1549 | + |
| 1550 | + # Pretty print using same format as SMB |
| 1551 | + self.logger.success(f"Dumping password info for domain: {self.domain}") |
| 1552 | + self.logger.highlight(f"Minimum password length: {min_pass_len}") |
| 1553 | + self.logger.highlight(f"Password history length: {pass_hist_len}") |
| 1554 | + self.logger.highlight(f"Maximum password age: {max_pass_age}") |
| 1555 | + self.logger.highlight("") |
| 1556 | + self.logger.highlight(f"Password Complexity Flags: {pass_prop or 'None'}") |
| 1557 | + |
| 1558 | + for i, a in enumerate(pass_prop): |
| 1559 | + self.logger.highlight(f"\t{PASSCOMPLEX[i]} {a!s}") |
| 1560 | + |
| 1561 | + self.logger.highlight("") |
| 1562 | + self.logger.highlight(f"Minimum password age: {min_pass_age}") |
| 1563 | + self.logger.highlight(f"Reset Account Lockout Counter: {rst_accnt_lock_counter}") |
| 1564 | + self.logger.highlight(f"Locked Account Duration: {lock_accnt_dur}") |
| 1565 | + self.logger.highlight(f"Account Lockout Threshold: {accnt_lock_thres}") |
| 1566 | + self.logger.highlight(f"Forced Log off Time: {force_logoff_time}") |
| 1567 | + |
| 1568 | + break # Only process first policy result |
| 1569 | + |
1484 | 1570 | def bloodhound(self): |
1485 | 1571 | # Check which version is desired |
1486 | 1572 | use_bhce = self.config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) |
|
0 commit comments