Skip to content

Commit 60b2bbc

Browse files
authored
Merge branch 'main' into list-snapshot
2 parents 35118c4 + a3d4216 commit 60b2bbc

3 files changed

Lines changed: 52 additions & 35 deletions

File tree

nxc/modules/change-password.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def options(self, context, module_options):
2626
2727
Examples
2828
--------
29-
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
29+
If STATUS_PASSWORD_MUST_CHANGE, STATUS_PASSWORD_EXPIRED or STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT (Change password for current user)
3030
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
3131
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
3232
@@ -95,14 +95,12 @@ def on_login(self, context, connection):
9595
new_nthash = self.newhash
9696

9797
try:
98-
self.anonymous = False
99-
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
98+
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=False)
10099
except Exception as e:
101100
# Handle specific errors like password expiration or must be change
102-
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
101+
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e) or "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT" in str(e):
103102
context.log.warning("Password must be changed. Trying with null session.")
104-
self.anonymous = True
105-
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
103+
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=True)
106104
elif "STATUS_LOGON_FAILURE" in str(e):
107105
context.log.fail("Authentication failure: wrong credentials.")
108106
return False

nxc/protocols/ldap.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -683,30 +683,37 @@ def get_sid(self):
683683

684684
def check_if_admin(self):
685685
# 1. get SID of the domaine
686-
search_filter = "(userAccountControl:1.2.840.113556.1.4.803:=8192)"
686+
search_filter = f"(userAccountControl:1.2.840.113556.1.4.803:={UF_SERVER_TRUST_ACCOUNT})"
687687
attributes = ["objectSid"]
688-
resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN)
688+
resp = self.search(search_filter, attributes, baseDN=self.baseDN)
689689
resp_parsed = parse_result_attributes(resp)
690-
answers = []
690+
691691
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "" or self.use_kcache) and self.username != "":
692692
for item in resp_parsed:
693693
self.sid_domain = "-".join(item["objectSid"].split("-")[:-1])
694694

695695
# 2. get all group cn name
696-
search_filter = f"(|(objectSid={self.sid_domain}-512)(objectSid={self.sid_domain}-544)(objectSid={self.sid_domain}-519)(objectSid=S-1-5-32-549)(objectSid=S-1-5-32-551))"
696+
search_filter = (f"(|(objectSid={self.sid_domain}-512)"
697+
f"(objectSid={self.sid_domain}-519)"
698+
f"(objectSid={self.sid_domain}-544)"
699+
"(objectSid=S-1-5-32-549)"
700+
"(objectSid=S-1-5-32-551))")
697701
attributes = ["distinguishedName"]
698-
resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN)
702+
resp = self.search(search_filter, attributes, baseDN=self.baseDN)
699703
resp_parsed = parse_result_attributes(resp)
700-
answers = []
701-
for item in resp_parsed:
702-
answers.append(f"(memberOf:1.2.840.113556.1.4.1941:={item['distinguishedName']})")
704+
answers = [f"(memberOf:1.2.840.113556.1.4.1941:={item['distinguishedName']})" for item in resp_parsed]
703705
if len(answers) == 0:
704706
self.logger.debug("No groups with default privileged RID were found. Assuming user is not a Domain Administrator.")
705707
return
706708

707-
# 3. get member of these groups
709+
# 3. Build a filter to query if the primaryGroupID is one of these groups
710+
group_ids = ["512", "519", "544", "549", "551"]
711+
primaryGroupID_filters = [f"(primaryGroupID={group_id})" for group_id in group_ids]
712+
answers.extend(primaryGroupID_filters)
713+
714+
# 4. Check if the user is member of one of these groups OR has one of these primaryGroupID
708715
search_filter = f"(&(objectCategory=user)(sAMAccountName={self.username})(|{''.join(answers)}))"
709-
resp = self.search(search_filter, attributes=[], sizeLimit=0, baseDN=self.baseDN)
716+
resp = self.search(search_filter, attributes=[], baseDN=self.baseDN)
710717
resp_parsed = parse_result_attributes(resp)
711718
for item in resp_parsed:
712719
if item:
@@ -801,27 +808,39 @@ def groups(self):
801808
# Building the search filter
802809
if self.args.groups:
803810
self.logger.debug(f"Dumping group: {self.args.groups}")
804-
search_filter = f"(cn={self.args.groups})"
805-
attributes = ["member"]
811+
812+
# Resolve group DN and primaryGroupID (objectSid)
813+
group_resp = self.search(f"(cn={self.args.groups})", ["distinguishedName", "objectSid"])
814+
group_parsed = parse_result_attributes(group_resp)
815+
816+
if not group_parsed:
817+
self.logger.fail(f"Group '{self.args.groups}' not found")
818+
return
819+
else:
820+
group = group_parsed[0]
821+
822+
# Search filter: user must have membership OR primaryGroupID
823+
search_filter = f"(|(memberOf={group['distinguishedName']})(primaryGroupID={group['objectSid'].split('-')[-1]}))"
824+
attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"]
825+
806826
else:
807827
search_filter = "(objectCategory=group)"
808828
attributes = ["cn", "member", "description"]
809-
resp = self.search(search_filter, attributes, 0)
829+
830+
resp = self.search(search_filter, attributes)
810831
resp_parsed = parse_result_attributes(resp)
811832
self.logger.debug(f"Total of records returned {len(resp_parsed)}")
812833

813834
if self.args.groups:
835+
# Display group members
814836
if not resp_parsed:
815-
self.logger.fail(f"Group {self.args.groups} not found")
816-
elif not resp_parsed[0]:
817-
self.logger.fail(f"Group {self.args.groups} has no members")
837+
self.logger.fail(f"Group '{self.args.groups}' has no members")
818838
else:
819-
# Fix if group has only one member
820-
if not isinstance(resp_parsed[0]["member"], list):
821-
resp_parsed[0]["member"] = [resp_parsed[0]["member"]]
822-
for user in resp_parsed[0]["member"]:
823-
self.logger.highlight(user.split(",")[0].split("=")[1])
839+
for item in resp_parsed:
840+
# Display sAMAccountName or CN if sAMAccountName not present (could be a group)
841+
self.logger.highlight(item["sAMAccountName"] if "group" not in item["objectClass"] else item["cn"])
824842
else:
843+
# Display all groups
825844
self.logger.highlight(f"{'-Group-':<40} {'-Members-':<9} {'-Description-':<60}")
826845
for item in resp_parsed:
827846
try:
@@ -1297,7 +1316,7 @@ def password_not_required(self):
12971316
"sAMAccountName",
12981317
"userAccountControl",
12991318
]
1300-
resp = self.search(searchFilter, attributes, sizeLimit=0, baseDN=self.baseDN)
1319+
resp = self.search(searchFilter, attributes, baseDN=self.baseDN)
13011320
resp_parsed = parse_result_attributes(resp)
13021321
self.logger.debug(f"Total of records returned {len(resp_parsed):d}")
13031322

@@ -1478,10 +1497,10 @@ def pso_mins(ldap_time):
14781497
# Let's find out even more details!
14791498
self.logger.info("Attempting to enumerate details...\n")
14801499
resp = self.search(searchFilter="(objectclass=msDS-PasswordSettings)",
1481-
attributes=["name", "msds-lockoutthreshold", "msds-psoappliesto", "msds-minimumpasswordlength",
1482-
"msds-passwordhistorylength", "msds-lockoutobservationwindow", "msds-lockoutduration",
1483-
"msds-passwordsettingsprecedence", "msds-passwordcomplexityenabled", "Description",
1484-
"msds-passwordreversibleencryptionenabled", "msds-minimumpasswordage", "msds-maximumpasswordage"])
1500+
attributes=["name", "msds-lockoutthreshold", "msds-psoappliesto", "msds-minimumpasswordlength",
1501+
"msds-passwordhistorylength", "msds-lockoutobservationwindow", "msds-lockoutduration",
1502+
"msds-passwordsettingsprecedence", "msds-passwordcomplexityenabled", "Description",
1503+
"msds-passwordreversibleencryptionenabled", "msds-minimumpasswordage", "msds-maximumpasswordage"])
14851504
resp_parsed = parse_result_attributes(resp)
14861505
for attrs in resp_parsed:
14871506
policyName = attrs.get("name", "")
@@ -1534,7 +1553,7 @@ def pass_pol(self):
15341553
"pwdProperties"
15351554
]
15361555

1537-
resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN)
1556+
resp = self.search(search_filter, attributes, baseDN=self.baseDN)
15381557
resp_parsed = parse_result_attributes(resp)
15391558

15401559
if not resp_parsed:

nxc/protocols/smb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def plaintext_login(self, domain, username, password):
473473
f'{domain}\\{self.username}:{process_secret(self.password)} {error} {f"({desc})" if self.args.verbose else ""}',
474474
color="magenta" if error in smb_error_status else "red",
475475
)
476-
if error in ["STATUS_PASSWORD_MUST_CHANGE", "STATUS_PASSWORD_EXPIRED"] and self.args.module == ["change-password"]:
476+
if error in ["STATUS_PASSWORD_MUST_CHANGE", "STATUS_PASSWORD_EXPIRED", "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT"] and self.args.module == ["change-password"]:
477477
return True
478478
if error not in smb_error_status:
479479
self.inc_failed_login(username)
@@ -537,7 +537,7 @@ def hash_login(self, domain, username, ntlm_hash):
537537
f"{domain}\\{self.username}:{process_secret(self.hash)} {error} {f'({desc})' if self.args.verbose else ''}",
538538
color="magenta" if error in smb_error_status else "red",
539539
)
540-
if error in ["STATUS_PASSWORD_MUST_CHANGE", "STATUS_PASSWORD_EXPIRED"] and self.args.module == ["change-password"]:
540+
if error in ["STATUS_PASSWORD_MUST_CHANGE", "STATUS_PASSWORD_EXPIRED", "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT"] and self.args.module == ["change-password"]:
541541
return True
542542
if error not in smb_error_status:
543543
self.inc_failed_login(self.username)

0 commit comments

Comments
 (0)